DEV Community

Adriano Fernandes
Adriano Fernandes

Posted on • Updated on

Listen the dynamic HTML select field changes with MutationObserver API

Hello devs,

I was working on a project that had an already custom dropdown component that has your own style and load the data from a hidden HTML select field options.

The component was working fine but I needed make a verification when the some option from custom dropdown was selected.

I created an example simulating the component that I mentioned above:

I thought, hmm, simple, let me create a event listener for select field and I can get the changes:

showCountrySelected(evt) {
  this.console.querySelector('span').textContent = evt.target.value;
}

this.dropdown.addEventListener('change', this.showCountrySelected.bind(this));
Enter fullscreen mode Exit fullscreen mode

Not so simple, some form elements needs a user interaction to fire the event:

The change event is fired for <input>, <select>, and <textarea> elements when an alteration to the element's value is committed by the user. Unlike the input event, the change event is not necessarily fired for each alteration to an element's value.

When the user commits the change explicitly (e.g., by selecting a value from a <select>'s dropdown with a mouse click, by selecting a date from a date picker for <input type="date">, by selecting a file in the file picker for <input type="file">, etc.);

Reference: MDN

A solution could be, the custom dropdown component fire a custom event when happens some change, for instance:

selectedCustomDropDown(evt) {
  const customDropDownClicked = evt.target;
  const changeEvent = new CustomEvent('dropdown:selected', {
    detail: customDropDownClicked.value
  });

  this.dropdown.value = customDropDownClicked.dataset.value;
  this.customDropdown.dispatchEvent(changeEvent);
}
Enter fullscreen mode Exit fullscreen mode

But, the custom dropdown component was already done and tested and been used in different parts of application. So I decided to make something different:

Create a function that observer the changes from select field and your child nodes, this moment I remembered that we have:

Mutation Observer API

MutationObserver allows you to provide a function that is called asynchronously when certain parts of the DOM change, such as adding a child to a node, changing an attribute on a node, or changing the text on a node. As the changes happen, the MutationObserver records them as MutationRecords and then calls a user provided callback at a later time with all the MutationRecords that are pending.

Solution

Instead of just updating the select field value as in the previous version, now I set the selected attribute to the selected option, this will generate a mutation in the select field and we can capture this change with the MutationObserver API.

My function that update the select field:

selectedCustomDropDown(evt) {
  const customDropDownClicked = evt.target;
  const dropDownOptionHasSelected = this.dropdown.querySelector(`option[selected]`);
  const dropDownOptionNewSelected = this.dropdown.querySelector(`option[value=${customDropDownClicked.dataset.value}]`);

  if(dropDownOptionHasSelected) {
    dropDownOptionHasSelected.removeAttribute('selected', '');
  }

  if(dropDownOptionNewSelected) {
    dropDownOptionNewSelected.setAttribute('selected', '');
  }
}
Enter fullscreen mode Exit fullscreen mode

The function that observer the mutations in select field using MutationObserver API:

listenDropDownChanges() {
  const observer = new MutationObserver(mutations => {
    // Filter the mutations to get the option with selected attribute
    const dropDownOptionSelected = mutations
      .filter(item => item.target[item.attributeName])
      .map(item => item.target.value);

    if(dropDownOptionSelected) {
      this.showCountrySelected(dropDownOptionSelected);
    }
  });

  // Set the select field to be monitored
  observer.observe(this.dropdown, {
    subtree: true,
    attributes: true,
    attributeFilter: ['selected']
  });
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

I set the options for observer:

  • subtree: Set to true to extend monitoring to the entire subtree of nodes rooted at target. All of the other MutationObserverInit properties are then extended to all of the nodes in the subtree instead of applying solely to the target node.
  • attributes: Set to true to watch for changes to the value of attributes on the node or nodes being monitored.
  • attributeFilter: An array of specific attribute names to be monitored. If this property isn't included, changes to all attributes cause mutation notifications.

And the filter to get the mutated option element with selected attribute (option[selected]):

const dropDownOptionSelected = mutations
  .filter(item => item.target[item.attributeName])
  .map(item => item.target.value);
Enter fullscreen mode Exit fullscreen mode

Here the result:

Browser Support

Alt Text

The browser support is really nice, almost 100% including IE11 and mobile browsers, so you definitely should use in your next project, be happy :)

Resources

This is just an example, but the best approach is choose to use the standards elements as possible you can.

Thanks for reading :)
See you next time!

Top comments (1)

Collapse
 
grossconstantin profile image
Constantin Groß

Note that this only works when using setAttribute() and removeAttribute() on the option elements, instead of option.selected = true/false, because the MutationObserver will only pick up changes in the serialized DOM. So if the script for the custom select uses the latter and doesn't trigger a change event, you're out of luck, unfortunately.