Listen the dynamic HTML select field changes with MutationObserver API
Tags: html, javascript, standards, browser
Published: 2020-02-21 17:40:11

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:

{% codepen https://codepen.io/djadriano/pen/XWbNbyP default-tab=js,result %}

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

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

this.dropdown.addEventListener('change', this.showCountrySelected.bind(this));
```

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](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event)

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

```javascript
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);
}
```

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:

```javascript
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', '');
  }
}
```

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

```javascript
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']
  });
}
```

## 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]`):

```javascript
const dropDownOptionSelected = mutations
  .filter(item => item.target[item.attributeName])
  .map(item => item.target.value);
```

Here the result:
{% codepen https://codepen.io/djadriano/pen/JjdbXKZ %}

## Browser Support

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/6yps75kryol9ir66j7hi.png)

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

- [Change Event](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event)
- [Getting To Know The MutationObserver API](https://www.smashingmagazine.com/2019/04/mutationobserver-api-guide/)
- [Mutation Observer at MDN](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)

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!