Blog | MetroStar

Delaying JavaScript Execution Until HTML Elements are Present in Power Apps and Dynamics CRM

Written by Niels Swimberghe | 09/21/2021

You can customize forms using JavaScript in Model-Driven Power Apps forms and Dynamics CRM forms, but your form will run your JavaScript way before the form is ready to be customized.

Even when the form triggers the "loaded" event, that doesn't guarantee that the HTML elements you want to customize are available. If you're working with tabs, then the content on other tabs is only loaded as you switch to that tab. There's an event to respond when a tab switch occurs, but not when the content of the tab is "loaded." This makes it hard to know when to run JavaScript to customize the HTML generated by the form.

The reason this may be difficult is likely because the software wants to discourage you from interacting with the HTML directly. But sometimes the built-in APIs are not sufficient for your needs, and you will want to directly modify the DOM (Document Object Model). 

Let's look at an example:

Building a Password Field Toggle

Picture this: A stakeholder asks for a new feature where the Social Security Number (SSN) is automatically masked by asterisks, but they want to be able to click on a button to unmask the SSN when needed. 

Trying to Query the Input

You can add the SSN text field to a form and find out the HTML ID for the input by digging through the DOM in your browser's developer tools; however, when you try to get the element by ID using JavaScript, you will receive "null" instead (example below). 

document.getElementById('your_input_id') // => null

This JavaScript may be running inside of an iframe for security reasons, so you should also try the top frame like this:

window.top.document.getElementById('your_input_id') // => null

Unfortunately, it will still be "null" because the control hasn't been rendered by the time of execution. What if you run the code when the form is loaded (seen below)? 

Xrm.Page.data.addOnLoad(() => window.top.document.getElementById('your_input_id')) // => still null

You can already start to interact with the form through the Xrm APIs, but the element still hasn't been rendered "onLoad." As you can see, it is quite hard to develop this feature. Nevertheless, with some extra JavaScript, you can work around this roadblock.

Polling for Node Presence

Since there isn't an event you can rely on to ensure the availability of your input, you have to periodically check if it is available. Here's a script that takes an array of ID's that will trigger the callback when all nodes for the ID's can be found using "document.getElementById":

  function waitForElementsToExist(elementIds, callback, options) {

    options = Object.assign({

        checkFrequency: 500, // check for elements every 500 ms

        timeout: null, // after checking for X amount of ms, stop checking

    }, options);

 

    // poll every X amount of ms for all DOM nodes

    let intervalHandle = setInterval(() => {

        let doElementsExist = true;

        for (let elementId of elementIds) {

            let element = window.top.document.getElementById(elementId);

            if (!element) {

                // if element does not exist, set doElementsExist to false and stop the loop

                doElementsExist = false;

                break;

            }

        }

        

        // if all elements exist, stop polling and invoke the callback function 

        if (doElementsExist) {

            clearInterval(intervalHandle);

            if (callback) {

                callback();

            }

        }

    }, options.checkFrequency);

 

    if (options.timeout != null) {

        setTimeout(() => clearInterval(intervalHandle), options.timeout);

    }

}

Here's how you can use the function:

waitForElementsToExist(['your_input_id'], () => doStuffWithInput);

function doStuffWithInput(){}

By default, the function polls every 500ms, but you can change that by passing in an option. You can also set a timeout in case you don't want to keep polling forever. In most cases, it only makes sense to poll for a couple of seconds.

Here's how you can change the options to let the function poll every one second and timeout after five seconds:

waitForElementsToExist(['your_input_id'], () => doStuffWithInput, {checkFrequency: 1000,  timeout: 5000});

You can pass in multiple ID's in which case all nodes need to be found before the callback is invoked. You could simplify this by only accepting a single ID instead of an array but that depends on your needs.

You may now see that your SSN field is on a separate tab. In this case, you'd want to wrap this into a tabStateChange callback like this:

let yourTab = Xrm.Page.ui.tabs.getByName('your_tab_name');

yourTab.addTabStateChange(function(){

    if(yourTab.getDisplayState() === 'expanded')

    {

        waitForElementsToExist(['your_input_id'], () => doStuffWithInput, {checkFrequency: 500,  timeout: 3000});

    }

});

This snippet does the following:

  • Gets a tab with the name "your_tab_name"
  • Registers a tab state change event handler which will be triggered when the tab is shown or hidden
  • Inside the handler, it will run the polling code only when the tab's display state is "expanded"

Inside the callback, you can be certain that your input is rendered and you can start building your password toggle feature.

Read how to create Dataverse Activities using Power Automate

Summary

When your JavaScript runs inside of Dynamics CRM forms, or Model-Driven Power Apps forms, certain HTML elements will not have been rendered yet. Even when querying for these elements "onLoad," you may not find them. By checking if these elements exist on an interval, you can quickly detect when certain DOM nodes have rendered.

About the Author 

Niels is a Sr. .NET Developer at MetroStar and technical content creator. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ blog on .NET, Azure, and web development.