What you see is what you get Editor in AL

I just made an update to the WYSIWYG-AL edit control and decided to do a video on how to use it:

https://youtu.be/GI37tlcrX6Y


In this video, Erik walks through how to integrate a WYSIWYG (What You See Is What You Get) rich text editor into Business Central using a custom control add-in built with AL. He demonstrates the full process: adding a new field to the Item table, embedding the editor on the Item Card, handling data loading and saving, and managing the read-only state of the control. The WYSIWYG control is based on CKEditor 5 and is available on Erik’s GitHub.

The WYSIWYG Control Add-in

Erik maintains an open-source WYSIWYG control add-in for AL on GitHub. It provides a way to edit rich text — with bold, italic, tables, fonts, and more — directly inside Business Central pages. This is ideal for scenarios where data needs formatted presentation, such as complex item descriptions.

The control is based on CKEditor 5, a well-documented and comprehensive web-based rich text editor. Erik has wrapped it as a Business Central control add-in and added support for key operations like loading content, saving content, and toggling read-only mode.

Setting Up the Table Extension

The first step is to create a new field on the Item table to store the rich text content. Erik creates a table extension with a Text[2048] field — taking advantage of the large text field sizes now available in AL, which avoids the complexity of working with Blob fields and streams.

tableextension 52100 "Item Description" extends Item
{
    fields
    {
        field(52100; "Item Description"; Text[2048])
        {
            Caption = 'Item Description';
            DataClassification = ToBeClassified;
        }
    }
}

Tip: When you press Ctrl+Space in places where you need to specify an ID (like table extension IDs or field numbers), VS Code’s IntelliSense will suggest the next available number for that object type. This means you don’t have to manually track numbering.

Adding the Editor to the Item Card

Next, Erik creates a page extension for the Item Card and adds the WYSIWYG user control. A useful trick for figuring out page group names is to place your cursor on the page name (e.g., “Item Card”) and press F12 to navigate to the base page’s source. This reveals that the first group on the Item Card is called Item.

Here’s the complete page extension with the user control and all its triggers:

pageextension 52100 "Youtube Item Card" extends "Item Card"
{
    layout
    {
        addafter(Item)
        {
            usercontrol(EditCtl; Wysiwyg)
            {
                ApplicationArea = all;
                trigger ControlReady()
                begin
                    CurrPage.EditCtl.Init();
                end;

                trigger OnAfterInit()
                begin
                    EditorReady := true;
                    if "Item Description" <> '' then
                        CurrPage.EditCtl.Load("Item Description")
                    else
                        CurrPage.EditCtl.Load(Rec.Description);
                    CurrPage.EditCtl.SetReadOnly(not CurrPage.Editable);
                end;

                trigger ContentChanged()
                begin
                    CurrPage.EditCtl.RequestSave();
                end;

                trigger SaveRequested(data: Text)
                begin
                    "Item Description" := Data;
                end;
            }
        }
    }
    trigger OnAfterGetRecord()
    begin
        if EditorReady then begin
            EditorReady := false;
            CurrPage.EditCtl.Init();
        end;
    end;

    var
        EditorReady: Boolean;
}

Understanding the Control’s Event Flow

The WYSIWYG control add-in exposes four events (triggers):

  • ControlReady — Fired when the control’s startup script has loaded and the control is ready to be initialized.
  • OnAfterInit — Fired after the Init() procedure completes, signaling the editor is fully ready for use.
  • ContentChanged — Fired whenever the user modifies content in the editor.
  • SaveRequested(data: Text) — Fired in response to calling RequestSave(), returning the current HTML content of the editor.

An important note about user controls in AL: you cannot call methods on a user control directly by name. Instead, you must use the CurrPage prefix:

// This will NOT work:
EditCtl.Init();

// This is the correct syntax:
CurrPage.EditCtl.Init();

Handling the Initialization Race Condition

One of the trickier aspects of working with user controls is the timing of events. When the page loads, OnAfterGetRecord might fire before ControlReady or OnAfterInit has completed. This creates a potential race condition.

Erik’s solution is to use a global Boolean variable called EditorReady. While he acknowledges that he generally advises against global variables, this is a case where it genuinely makes sense:

  1. OnAfterInit sets EditorReady := true, confirming the editor is fully initialized.
  2. OnAfterGetRecord checks EditorReady before attempting to reinitialize the control — this prevents errors from trying to interact with the editor before it’s ready.
  3. When navigating to a new record, the code sets EditorReady := false and calls Init() again, which resets the control and triggers OnAfterInit with the new record’s data.

Loading Data into the Editor

The editor stores and works with HTML content. In the OnAfterInit trigger, the code checks whether the custom Item Description field has a value. If it does, that value is loaded; otherwise, the standard Description field is used as a starting point:

if "Item Description" <> '' then
    CurrPage.EditCtl.Load("Item Description")
else
    CurrPage.EditCtl.Load(Rec.Description);

Saving Data from the Editor

Saving follows a two-step process. When the user changes content, the ContentChanged trigger fires, which calls RequestSave(). This in turn triggers SaveRequested, which receives the editor’s current HTML content as a text parameter. That value is then written to the Item Description field:

trigger ContentChanged()
begin
    CurrPage.EditCtl.RequestSave();
end;

trigger SaveRequested(data: Text)
begin
    "Item Description" := Data;
end;

Managing Read-Only State

A common issue with user controls is that they don’t automatically respect the page’s edit mode. The editor would be editable even when the page is in view mode, which is incorrect behavior.

The control’s SetReadOnly method handles this. However, there’s a subtlety with CurrPage.Editable — it returns true when the page is editable, but SetReadOnly expects true when it should be read-only. So the logic needs to be inverted:

CurrPage.EditCtl.SetReadOnly(not CurrPage.Editable);

Erik notes that CurrPage.Editable is a somewhat quirky property in AL — you can set it (though it only reliably works in certain triggers like OnOpenPage), and its behavior differs between list and card pages. However, when you read its value, it accurately reflects the page’s current edit state.

A nice side effect of the current architecture: when the user clicks the pencil icon to enter edit mode, Business Central re-triggers OnAfterGetRecord, which reinitializes the editor and automatically picks up the correct editable state. No additional trigger code is needed.

Extending the Control

The WYSIWYG control is built on CKEditor 5, which has extensive documentation and a rich feature set. Erik encourages developers to extend the control to suit their needs. As an example, adding the SetReadOnly functionality required only four lines of JavaScript (leveraging CKEditor’s built-in isReadOnly property) and one line in the control add-in definition file.

If you build useful extensions to the control, Erik welcomes pull requests on GitHub, or you can reach out via email, Twitter, or the comments section.

Summary

Integrating a WYSIWYG editor into Business Central involves a few key steps: creating a field to store HTML content, embedding the control add-in on a page, carefully managing the initialization lifecycle to avoid race conditions, and properly handling the read-only state. The pattern demonstrated here — using a Boolean flag to track editor readiness, reinitializing on record navigation, and leveraging the two-step save process — provides a solid foundation for any control add-in integration in AL. The full source code and the WYSIWYG control itself are available on Erik’s GitHub.