In-page Progress Bar, ControlAddIn from scratch in AL

After showcasing Henrik’s ASCII-art progress bar, several viewers asked about an in-page progress bar, so in this video, I set out to create that. Check out the video:

https://youtu.be/yens8izw09g

In this video, Erik walks through building an in-page progress bar as a Control Add-in from scratch in AL and Business Central. Rather than using the dialog-based progress indicators, this approach embeds a visual progress bar directly on a page — perfect for scenarios like showing completion percentage on a Job Card. Erik starts from a W3Schools example, translates it into a Control Add-in, and wires everything together with AL, JavaScript, and CSS.

The Inspiration: A Simple HTML/CSS Progress Bar

Erik starts by looking up a simple progress bar implementation on W3Schools. The concept is straightforward: a div inside a div, where the inner div’s width represents the progress percentage, and the outer div provides the background track. By changing the width of the inner div and updating its text content, you get a functional progress bar with no fancy libraries required.

Setting Up the AL Project

The first step is to add a custom field to the Job table so we have a value to drive the progress bar. Erik creates a table extension with a simple integer field called “My Progress”:

tableextension 50100 "My Job" extends Job
{
    fields
    {
        field(50100; "My Progress"; Integer) { }
    }
}

Then a page extension on the Job Card (page 88) adds this field to the General group so it’s visible and editable:

pageextension 50100 "My Job Card" extends "Job Card"
{
    layout
    {
        addlast(General)
        {
            field("My Progress"; Rec."My Progress")
            {
                ApplicationArea = All;
                Caption = 'Progress';
            }
        }
    }
}

Creating the Control Add-in

To embed JavaScript into a Business Central page, you need a Control Add-in. Control Add-ins don’t have numbers — they’re identified by name. Erik creates a file called progress.al with the Control Add-in definition:

controladdin "My Progress Bar"
{
    Scripts = 'script.js';
    StartupScript = 'startup.js';
    StyleSheets = 'progress.css';

    MinimumHeight = 50;
    MaximumHeight = 50;
    HorizontalStretch = true;

    event IAmReady();
    procedure SetProgress(progress: Integer);
}

Key things to note here:

  • Scripts — the main JavaScript file containing your functions
  • StartupScript — a script that runs automatically when the control add-in loads
  • StyleSheets — CSS files for styling
  • IAmReady() — an event that fires back to AL when the control is loaded and ready
  • SetProgress() — a procedure that AL can call to update the progress bar

The CSS

The CSS is essentially copied straight from the W3Schools example. It defines the outer container (the track) and the inner bar (the fill):

#myProgress {
    width: 100%;
    background-color: #ddd;
}

#myBar {
    width: 0%;
    height: 30px;
    background-color: #4CAF50;
    text-align: center;
    line-height: 30px;
    color: white;
}

The outer #myProgress div gets a light gray background, while the inner #myBar div gets the green fill color. The width of #myBar starts at 0% and will be updated dynamically.

The JavaScript

startup.js — Signaling Readiness

Because the Control Add-in lives inside an iframe, it loads asynchronously from the main page. The first thing you need to do is tell Business Central that the control is ready. This is done in the startup script:

Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('IAmReady', []);

This calls back into BC using the event we defined in the Control Add-in object. The method name 'IAmReady' must match the event name exactly.

script.js — Rendering and Updating

The main script file handles two things: injecting the HTML into the control’s div, and providing the SetProgress function that AL will call:

var addin = document.getElementById('controlAddIn');
addin.innerHTML = '
0%
'; function SetProgress(progress) { var elem = document.getElementById("myBar"); elem.style.width = progress + "%"; elem.innerHTML = progress + "%"; }

Erik explains the key insight here: you can’t add HTML directly through AL, but you can use JavaScript to find the control’s container div (which always has the id controlAddIn) and set its innerHTML. The SetProgress function simply updates the width style and the text content of the inner bar element.

Wiring It All Together on the Page

Back in the page extension, Erik adds the control add-in as a “user control” (the somewhat unintuitive AL keyword for embedding a control add-in) and handles the communication between AL and JavaScript:

pageextension 50100 "My Job Card" extends "Job Card"
{
    layout
    {
        addlast(General)
        {
            field("My Progress"; Rec."My Progress")
            {
                ApplicationArea = All;
                Caption = 'Progress';

                trigger OnValidate()
                begin
                    CurrPage.Bar.SetProgress(Rec."My Progress");
                end;
            }
            usercontrol(Bar; "My Progress Bar")
            {
                ApplicationArea = All;

                trigger IAmReady()
                begin
                    CurrPage.Bar.SetProgress(Rec."My Progress");
                end;
            }
        }
    }
}

There are two important triggers here:

  • IAmReady — When the control add-in signals it’s loaded, we immediately set the progress to the current record’s value. This ensures the bar shows the correct percentage when the page opens.
  • OnValidate on the “My Progress” field — Whenever the user changes the value, we call SetProgress to update the bar in real-time.

Note the syntax for calling a procedure on a control add-in: CurrPage.Bar.SetProgress(...), where Bar is the name given to the user control in the layout.

How It Works: The iframe Architecture

Erik takes a moment to explain an important architectural detail. When you inspect the page in the browser’s developer tools, you’ll see that the control add-in is rendered inside an iframe. This means it’s essentially a web page within a web page. This is why:

  • The control loads asynchronously — you must wait for the IAmReady event before trying to communicate with it
  • Communication goes through the Microsoft.Dynamics.NAV.InvokeExtensibilityMethod bridge
  • CSS and JavaScript are scoped to the iframe, keeping things isolated from the main BC page

Testing It Out

With everything in place, Erik demonstrates the progress bar in action. Opening a Job Card shows the bar at 0%. Typing 25 into the progress field and tabbing out updates the bar to 25%. Changing it to 40, then 20 — the bar responds correctly each time, both growing and shrinking to match the entered value.

Summary

This video demonstrates that building a Control Add-in from scratch doesn’t have to be intimidating. The entire solution consists of just a handful of files:

  1. progress.al — The Control Add-in definition with events and procedures
  2. script.js — The JavaScript that renders the HTML and handles updates
  3. startup.js — A one-liner that signals readiness back to AL
  4. progress.css — Simple CSS copied from W3Schools
  5. Table and page extensions — The AL plumbing to add the field and wire up the control

The approach of starting with a working web example and adapting it into a Control Add-in is a practical strategy. The HTML and CSS transfer almost directly — the main addition is the plumbing to communicate between the JavaScript iframe and AL code through events and procedures. Erik notes that adding smooth animation would be a nice enhancement and invites viewers to share their JavaScript solutions in the comments.