Post 4: Angular inside Dynamics 365 Business Central – Data Transfer

If you’re just joining us, please make sure to check the Table of Contents in the first post here: Post 1 – Angular inside Dynamics 365 Business Central.

Send Our Data Between Embedded Angular and Dynamics 365 Business Central

One of the challenges to doing development work with Add-ins when you’re used to working C/AL or AL is that NAV doesn’t make you think about Synchronous vs Asynchronous exactly. Triggers/events are known, mapped, and happen in order. Code within them is executed in order, and that’s that.

There is a little bit of guidance on the Developer Documentation site, which is short enough to quote here in its entirety:

Writing control add-ins that work on all display targets, you have to consider some limitations regarding asynchronous communication. Compared to the Microsoft Dynamics NAV Windows client control add-in extensibility framework, the Dynamics NAV framework has some limitations regarding the interface of the control add-in. The limitations come from the nature of the asynchronous communication between the clients and the Microsoft Dynamics NAV Server. All calls between the C/AL code running on the Microsoft Dynamics NAV Server and the script function running in the Web browser are asynchronous. This means that methods in the control add-in interface must be of type void and property methods should not be used.

To transfer a result from a C/AL trigger to the calling script function, just add a method to the control add-in interface that the C/AL trigger can invoke to send the result to the script.

To transfer a result from a script function to C/AL trigger, just add an event to the control add-in interface that the script function can use to invoke a C/AL trigger that receives the result.

To rephrase:

  • To send data to JavaScript, we must have a known function to call
  • To receive data from JavaScript (or for JavaScript to SEND data to NAV as we need to think about it), we must create a NAV event and listen for it.

For our extension, we have a tiny to-do list:

  1. When the control loads or is refreshed, we need to take the data and send it to Angular to display
  2. When Angular changes the data, we should fire an event to AL to receive and save it.

Send the Data

Sounds straight-forward, and in pure JavaScript, it is. But Angular has that complex (but good) Zone controlled processing ecosystem. Function calls are encapsulated with lots of events, and we need to update a data model inside that.

Our flow needs to be this, which we’ll develop from end back to start:

AL Page -> Control Addin AL -> JavaScript -> Angular

Angular Update

We have to build all those connections ourselves, all the way back. For Angular to be reachable from JS, we need to create some hooks into the Global scope outside of Angular.

Dive back into your the-editor.component.ts file. We’ll first add a new function to update the value of our HTML string:

 setHTML(newHTML:string) {

   this.htmlContent = newHTML;

  }

We’ll also replace the constructor with this odd bit of code:

  constructor(private zone:NgZone) {

    window[‘angularHTMLEditor’] = {

      zone: this.zone,

      setHTML: (value) => this.setHTML(value),

      component: this

    };

    //@ts-ignore

    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(“ControlReady”,[]);

  }

Easy mental shortcut: Zone is akin to Threading or AppPools.

In this, we’re defining a new element on the window object called angularHTMLEditor. We assign that the properties of the Zone, the component, and the function we’re exposing. If you’re following step by step, you’ll have to want to accept the Refactor suggestions to update the Imports for Zone. This is now a reachable function/object from JavaScript.

You’ll also noticed we added the ControlReady call here – we don’t want to send the ControlReady event from the Wrapper anymore – we want to know Angular is doing initializing. Also, since TypeScript is trying to help us out, and it doesn’t know about the NAV Extensibility, we’ll tell it to ignore the line.

JavaScript Wrapper

Back in AL, in our /res/ControlAddin/Script/AngularWrapper.js file, we now need to add a function call to that new set of hooks on the window object.

function setHTML(storedHTML)
{

    window.angularHTMLEditor.zone.run(() => {window.angularHTMLEditor.setHTML(storedHTML);});  

}

This gets the Zone and triggers a run command to run our function.

Also, in our startup.js file, remove the InvokeExtensibilityMethod call.

Control Add-In

Still in AL, in our /res/ControlAddin/HTMLEditor.al file, we need to add the Function to the AL object so we can call it elsewhere. That is just a one line add towards the end of our object:

procedure setHTML(newHTML:Text);

Page AL

In this example, as you may recall, we’re not dealing with saved HTML, just in memory as a temporary action page. For a proper full solution, we’d do a lot more.

Given that, we only have to add a little code to our Pag73000.SendHTML.al file, in the trigger ControlReady:

CurrPage.HTMLEditor.setHTML(rawHTML);

We can also comment out or delete our earlier test message.

We theoretically send that rawHTML text variable to the HTML EditorControl, only, it’s not databound, so we need to mock-send data. For that, I’ll add a new Action to the page. Our new Page looks like so:

page 73000 SendHTML

{

    PageType = Card;

    ApplicationArea = All;

    UsageCategory = Administration;

    SourceTable = “Segment Header”;

   layout

    {

        area(Content)

        {

            group(Editor)

            {

                usercontrol(“HTMLEditor”; HTMLEditor)

                {

                    trigger ControlReady()

                    begin

                        //Message(‘Control Addin HTMLEditor Ready’);

                        CurrPage.HTMLEditor.setHTML(rawHTML);

                    end;

                }

            }

        }

    }

   actions

    {

        area(Processing)

        {

            action(SendEmail)

            {

                ApplicationArea = All;

                trigger OnAction()

                begin

                    Message(‘This button would send something.’);

                end;

            }

            action(TestHTML)

            {

                ApplicationArea = All;

                trigger OnAction()

                begin

                    rawHTML := ‘This is a<i>formatted</i> <strike>text</strike><b>test</b> message.’;

                    CurrPage.HTMLEditor.setHTML(rawHTML);

                end;

            }

        }

    }

   var

        rawHTML: Text;

}

Do a build on the Angular objects to get the latest in place, then we can run a test on our new Extension updates.

Now we have a new Action:

And when we run that, we should see:

Perfect.

Update the Data

In Angular, we have an easy way to hook into the Change monitoring system, and we’ll fire our event from that.

Note: This Change Event fires with pretty much each keystroke, so use this very carefully in a Validation chain. See here if you want to see how you can make it driven by a button press instead.

Fire the Event

In the-editor.component.html, we update our single line (with added line breaks for visual clarity to say:

    <app-ngx-editor [placeholder]=“‘Enter text here…'”

        [spellcheck]=“true”

        [(ngModel)]=“htmlContent”

        resizer=“basic”

        (ngModelChange)=“onHTMLChange()”>

    </app-ngx-editor>

In the-editor.component.ts, we add a new function:

  onHTMLChange() {

    //@ts-ignore

    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(“HTMLEditor”,[this.htmlContent]);

  }

Combining those two, whenever Angular detects a change to the content of the NgxEditor control, it will fire an event to NAV called HTMLEditor, passing the new value of that string variable, htmlContent.

Go ahead and run the build batch file again, as we need the .js files updated.

Make the Event Available

Making the Control Addin handle the event is as easy as that previous event definition for ControlReady. In HTMLEditor.al, add:

event HTMLEditor(newHTML:Text);

Handle the Event

In our Extension Pag73000.SendHTML.al, we need to add a bit of code: page / layout / area / group / UserControl(“HTMLEditor”):

 trigger HTMLEditor(newHTML: Text)

    begin

         rawHTML := newHTML;

     end;

Since we’re not actually doing anything with the data since this isn’t a real solution, so let’s also add a way to see the results via a new Action

action(ShowHTML)

{

    ApplicationArea = All;

   trigger OnAction()

      begin

          Message(rawHTML);

     end;

}

This will show us the raw HTML.

Now, let’s try it out!

I’ll add some formatted text to the end of our TestHTML string, then click ShowHTML:

Technically we’re done here! We have handled:

  • Creating a basic Angular application
  • Creating an AL Extension
  • Connecting AL to Angular
  • Passing data into Angular from NAV
  • Receiving a new event to data back from Angular

This is a really basic implantation, but when you remember that AL has built-in handling for JSON, so we can pass pretty huge chunks of data around.

We’ll address a really complex topic in the bonus blog post to follow: Resize awareness.

But for now, this concludes the main series. I hope this encourages everyone to start to wrap their heads around what power we can bring to the table when you can combine Extensions and Angular.  This brings a VERY powerful JavaScript toolset to the already amazing engine that is Microsoft Dynamics 365 Business Central.

If you do make anything with all this, please, let me know! I’ll happily sign NDAs just to see what fun things people make.