Engineered Code is proud to announce the availability of ecLearn - the Learning Management System built on top of Microsoft Dataverse

ENGINEERED CODE BLOG

Power Pages: Multiselect Change Event

I was very excited when multiselect (choices) column support was finally added to Power Pages in 2022. While I had published a workaround on this blog (and it was one of the more popular posts on the site), it was great not having to create extra fields and register plugins. Recently, however, I was asked about how to work with these fields on the form in JavaScript. I’ll dive into that in this post.

Choice Column Control

A choices column on a form is displayed using an advanced control that behaves like a multiselect dropdown, where selected choices are then displayed, and can be removed. While many of the fields on a Power Pages form consist of a basic HTML input or select element, that is certainly not the case here.

Looking into the client side source code, we can see that the field is a PCF Control. It seems to leverage an open source jQuery plugin called Searchable Option List

Diving deeper into this control, we can see that the dropdown is an unordered list, with the list items containing a checkbox input for each option. While the actual checkboxes are not visible to the user (a more advanced UI element is clicked on, which then toggles the checkbox), the selection of these checkboxes drives the value of this column.

As will all the common controls for the various field types on a form, there is an input where the ID matches the logical name of the column. In this case, it is a hidden input, and the value is a relatively complex JSON object that contains a list of the selection option. Here is an example:

[
    {
        "Value": 124900000,
        "Label": {
            "LocalizedLabels": [
                {
                    "Label": "Short Game Practice Area",
                    "LanguageCode": 1033,
                    "IsManaged": false,
                    "MetadataId": "4ab99d85-6d58-449e-90ab-84aa4e24861f",
                    "HasChanged": null
                }
            ],
            "UserLocalizedLabel": {
                "Label": "Short Game Practice Area",
                "LanguageCode": 1033,
                "IsManaged": false,
                "MetadataId": "4ab99d85-6d58-449e-90ab-84aa4e24861f",
                "HasChanged": null
            }
        },
        "Description": {
            "LocalizedLabels": [
                {
                    "Label": "",
                    "LanguageCode": 1033,
                    "IsManaged": false,
                    "MetadataId": "0da5fb19-2134-4f2b-8e81-11f03cee3de1",
                    "HasChanged": null
                }
            ],
            "UserLocalizedLabel": {
                "Label": "",
                "LanguageCode": 1033,
                "IsManaged": false,
                "MetadataId": "0da5fb19-2134-4f2b-8e81-11f03cee3de1",
                "HasChanged": null
            }
        },
        "Color": null,
        "IsManaged": false,
        "ExternalValue": "",
        "MetadataId": null,
        "HasChanged": null,
        "ParentValues": null
    },
    {
        "Value": 124900001,
        "Label": {
            "LocalizedLabels": [
                {
                    "Label": "Practice Putting Green",
                    "LanguageCode": 1033,
                    "IsManaged": false,
                    "MetadataId": "cf9c7715-51c9-4790-98a3-02622cf1b8fe",
                    "HasChanged": null
                }
            ],
            "UserLocalizedLabel": {
                "Label": "Practice Putting Green",
                "LanguageCode": 1033,
                "IsManaged": false,
                "MetadataId": "cf9c7715-51c9-4790-98a3-02622cf1b8fe",
                "HasChanged": null
            }
        },
        "Description": {
            "LocalizedLabels": [
                {
                    "Label": "",
                    "LanguageCode": 1033,
                    "IsManaged": false,
                    "MetadataId": "77f527a2-877c-4158-983e-80b088e5d42c",
                    "HasChanged": null
                }
            ],
            "UserLocalizedLabel": {
                "Label": "",
                "LanguageCode": 1033,
                "IsManaged": false,
                "MetadataId": "77f527a2-877c-4158-983e-80b088e5d42c",
                "HasChanged": null
            }
        },
        "Color": null,
        "IsManaged": false,
        "ExternalValue": "",
        "MetadataId": null,
        "HasChanged": null,
        "ParentValues": null
    }
] 

When working with this control in JavaScript, there are two main scenarios we’ll look at: handling a change event and modifying the values programmatically.

Change Event

Let’s start by looking at how we can add an event handler to when the value of this field changes.

My first attempt was to try to hook directly into the events of the Searchable Option List control. Unfortunately, this control isn’t initialized when the document is ready. So there is nothing to add an event to at that point. Perhaps I could use some polling, or maybe there is an event later in the lifecycle of the page, but I don’t know what that is. So I moved on from that.

Next, I thought I’d just handle the change event on the hidden input that contains the value. Unfortunately, change events aren’t fired for hidden inputs, so no luck with that approach either.

However, along similar lines, my next attempt was to use a MutationObserver. This is a JavaScript feature that allows you to monitor for changes to the DOM. In this case, I wanted to look for changes to the value of the hidden input. Borrowing code from here gave me something that looks like:

function registerChoicesChangeEvent(id, callback) {
    const hiddenInput = $('#' + id);
    let previousValue = hiddenInput.val();
    
    // 1: create `MutationObserver` instance
    const observer = new MutationObserver((mutations) => {

        // 2: iterate over `MutationRecord` array
        mutations.forEach(mutation => {
            // 3.1: check if the mutation type and the attribute name match
            // 3.2: check if value changed
            if (
                mutation.type === 'attributes'
                && mutation.attributeName === 'value'
                && hiddenInput.val() !== previousValue
            ) {
                previousValue = hiddenInput.val();
                callback.call(hiddenInput[0]);
            }
        });
    });

    observer.observe(hiddenInput[0], { attributes: true });
}

This code does the work to create a MutationObserver object which observes change to the value attribute of the hidden input. It keeps track of the value of the hidden input to ensure that it only fires the callback if the value has truly changed (otherwise you’ll get multiple callbacks for each individual change, since each update seems to set the hidden input to the same value multiple times).

Use the function like so to register your callback:

registerChoicesChangeEvent('new_choices', function () {        
    var val = $(this).val();
    if (val && val != '') console.log(JSON.parse(val)); else console.log(val);
});

Within the callback, this refers to the hidden input. Its value is a string of JSON, so use JSON.parse() to convert to a real object.

This seems to work fine. But I had another thought: we could handle change events on the checkboxes that correspond to each option.

The code to do that looks something like:

function registerChoicesChangeEventCheckboxes(id, callback) {
    const hiddenInput = $('#' + id);
    let previousValue = hiddenInput.val();
    $('#' + id).closest('.control').on('change', 'input.msos-checkbox', function () {
        if (hiddenInput.val() !== previousValue) {
            previousValue = hiddenInput.val();
            callback.call(hiddenInput[0]);
        }
    });
}

We target the control using the logical name of the column. This will get us the hidden input value. We go up to the root node of the control in the DOM using the control call, and then register a change event. We use the input.msos-checkbox selector in our event registration to only have the event fire when one of those specific checkboxes is changed. Remember that the control isn’t initialized when the document is ready, so the checkboxes don’t exist yet, but the wrapper with the control call does. This is why we have to register the event on the control class, and then filter on the descendants that will eventually be created. There again we apply logic to only fire our callback if the value of the input has actually changed.

Unfortunately, when I use this version, I run into an issue: the change event doesn’t fire when you unselect the last option. I didn’t dig into why, but my guess is that perhaps the change event isn’t fired properly when JavaScript is used to update the hidden checkbox. That isn’t an issue with our first version, so use that one.

Modify Selected Options Programmatically

The good news is that changing the selected options programmatically is as simple as checking (or unchecking) the checkbox, and then triggering the change event to ensure the control (and the hidden input that contains the value) is updated.

We’ll target the hidden input using the logical name of the column as the ID, go up the DOM to find a class of control, and then we’ll look for an input with the value that matches the option we want to select (or deselect). Then we can use the prop function to set the “checked” property, and then trigger the change event. Like so:

$('#new_choices').closest('.control').find('input[value="124900002"]').prop('checked', true).trigger('change');
 

5 responses to “Power Pages: Multiselect Change Event”

  1. Jimmy says:

    Hi Nick,
    Something seemed be pushed out over the past weekend, Feb 3rd 2024, so now our portal shows “one of the code components can’t be loaded” if there are more than one multiselect option set fields on a page. Seen on 4-5 sites so far in three tenants in Canadian region.
    Just wondering if you’ve come across this?
    Cheers,
    Jimmy

    • Nicholas Hayduk says:

      I haven’t run into it myself, but I’ve heard the same story from others. My recommendation is to put a support request in with Microsoft. The more users they get that are affected by the issue should push a fix up the priority list.

      Nick

  2. RN85Apps says:

    Have you observed that if a multiselect control is on a page (and the page requires you to scroll to see it), the page onload will automatically set focus and scroll to it? Very annoying. Do you know how to turn this off?

    • Nicholas Hayduk says:

      I hadn’t run into that myself before, but I was able to reproduce.

      I’d recommend filing a bug with MS. I don’t think that is intended behaviour.

      In the meantime, I took a quick look at the code. It seems like that is a control not specific to Power Pages, and that behaviour happens if the control detects that it is not within the Unified Interface for Model-Driven Apps.

      You can hack it into thinking that it is the Unified Interface using this JavaScript:

      window.Xrm = {};
      window.Xrm.Internal = {};
      window.Xrm.Internal.isUci = () => true;

      However, there may be other issues with doing that – I haven’t really tested this solution or used it in real life.

      Nick

  3. Evan Currier says:

    Great article! This helped me implement some custom functionality into our power pages environment that allows us to dynamically inject in multiselect controls into our portal forms and the instructions here helped me make it behavior appropriately.

    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

Contact

Engineered Code is a web application development firm and Microsoft Partner specializing in web portals backed by Dynamics 365 & Power Platform. Led by a professional engineer, our team of technology experts are based in Regina, Saskatchewan, Canada.