What if AL code could click on anything?

There are several features in the UI that are not accessible from AL code, but maybe we can access them anyway. Check out the video:

https://youtu.be/ZzsnGZaD17A

In this video, Erik explores a creative (and somewhat risky) technique: using JavaScript from AL code to programmatically click on UI elements in Business Central that are normally only accessible through the browser interface. Think of elements like the filter pane toggle, the FactBox toggle, and other built-in client controls that have no corresponding AL API. The result is a working proof of concept that demonstrates both the power and the fragility of this approach.

The Problem: UI-Only Functions

Business Central’s web client has several built-in functions — toggling the filter pane, showing or hiding the FactBox, and others — that are only accessible via the browser UI. There’s no AL function to programmatically trigger these actions. When someone asked Erik if it was possible to control these from AL code, his initial gut reaction was “no.” But then he started thinking…

Since the Business Central client runs in a browser and these controls are just HTML elements rendered via JavaScript, there’s theoretically nothing stopping us from finding those elements in the DOM and calling .click() on them.

Exploring the DOM with Browser Developer Tools

Erik starts by opening the browser developer tools (Ctrl+Shift+I) and using the element inspector to examine the filter pane toggle button. Inspecting the element reveals something like:

<button id="b7k_filterPaneToggle" title="Show filter pane" ...>

The b7k prefix appears to be dynamically generated, so we can’t rely on matching the full ID. Instead, we can use CSS attribute selectors to match the end of the ID. Using document.querySelector with the selector [id$="filterPaneToggle"], we can find the element regardless of the random prefix:

document.querySelector('[id$="filterPaneToggle"]').click();

Running this in the browser console successfully toggles the filter pane open and closed. The same approach works for the FactBox toggle using [id$="factBoxToggle"].

Building the Control Add-in

To call JavaScript from AL, you need a control add-in. Erik sets up a minimal one — a one-pixel iframe that serves as the bridge between AL and JavaScript. The control add-in definition declares the events and procedures:

controladdin JavascriptAccess
{
    Scripts = 'script.js';
    StartupScript = 'startup.js';
    MaximumHeight = 1;
    MinimumHeight = 1;
    MaximumWidth = 1;
    MinimumWidth = 1;
    RequestedHeight = 1;
    RequestedWidth = 1;

    event ControlReady();
    procedure clickanything();
    procedure FilterPaneToggle();
}

The startup script simply signals that the control is ready:

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

The Key Insight: Escaping the Iframe

One critical detail: the control add-in runs inside an iframe. Using document directly would only search within that tiny one-pixel iframe. To access the main Business Central page, you need to jump out of the iframe using window.parent.document:

function clickanything() {
    try {
        let boxes = window.parent.document.querySelectorAll('[id$="factBoxToggle"]');
        boxes[boxes.length - 1].click();
    }
    catch (e) {
        console.log(e);
    }
}

function FilterPaneToggle() {
    try {
        let boxes = window.parent.document.querySelectorAll('[id$="filterPaneToggle"]');
        boxes[boxes.length - 1].click();
    }
    catch (e) {
        console.log(e);
    }
}

Handling Multiple Matching Elements

During testing, Erik discovered a subtle bug: sometimes the click appeared to do nothing. After investigating, he noticed that the outline highlight was appearing on the wrong element — there were actually multiple elements matching the selector, because Business Central keeps background pages in the DOM when you navigate.

The fix is to use querySelectorAll instead of querySelector and always click the last matching element, since the topmost (visible) page’s elements are always rendered last in the DOM:

let boxes = window.parent.document.querySelectorAll('[id$="factBoxToggle"]');
boxes[boxes.length - 1].click();

The Page Extension

The page extension ties everything together, placing the control add-in on the Customer List page and providing action buttons to trigger the JavaScript functions:

pageextension 50128 CustomerListExt extends "Customer List"
{
    layout
    {
        addlast(content)
        {
            usercontrol(JavascriptAccess; JavascriptAccess)
            {
                ApplicationArea = all;
                trigger ControlReady()
                begin
                    //CurrPage.JavascriptAccess.clickanything();
                end;
            }
        }
    }
    actions
    {
        addfirst(processing)
        {
            action(Test)
            {
                Caption = 'Test Javascript';
                ApplicationArea = all;
                trigger OnAction()
                begin
                    CurrPage.JavascriptAccess.clickanything();
                end;
            }
            action(FilterPane)
            {
                Caption = 'Filter Pane Toggle';
                ApplicationArea = all;
                trigger OnAction()
                begin
                    CurrPage.JavascriptAccess.FilterPaneToggle();
                end;
            }
        }
    }
}

Note that Erik initially tried calling the JavaScript from the ControlReady trigger, but this caused issues — likely because the main page elements hadn’t fully rendered yet. Moving the calls to user-initiated actions resolved the timing problem.

Here Be Dragons

Erik is very clear about the risks of this approach. This technique manipulates internal DOM structures that Microsoft controls and can change at any time. Potential breaking scenarios include:

  • Button IDs or naming conventions changing in a future update
  • The click event no longer being the mechanism that triggers the action
  • Changes to how pages are stacked in the DOM, breaking the “grab the last one” logic
  • Unexpected behavior when multiple pages are open in the background

There are also still some unresolved edge cases — for instance, the behavior was inconsistent when navigating between list pages and card pages, and when multiple pages were stacked in the navigation history.

Summary

This video demonstrates that AL code can interact with virtually any UI element in Business Central by using a JavaScript control add-in as a bridge. The technique leverages CSS attribute selectors to find elements by partial ID, escapes the control add-in’s iframe via window.parent.document, and handles overlapping DOM elements by always targeting the last match. While this is an impressive hack and could be useful in specific scenarios, it’s firmly in “here be dragons” territory — it depends on undocumented internal structures that could break with any Business Central update. Use it wisely, and don’t be surprised if it needs maintenance.