Delaying JavaScript Execution Until HTML Elements are Present in Power Apps and Dynamics CRM
Niels Swimberghe - - Dynamics
Follow me on Twitter, buy me a coffee
This blog post was written for MetroStar Systems and originally published at MetroStar's blog here.
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.
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.