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));
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.);
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);
}
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', '');
}
}
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']
});
}
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);
Here the result:
Browser Support
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)
Note that this only works when using
setAttribute()
andremoveAttribute()
on the option elements, instead ofoption.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.