Detecting Changes in HTML Attributes with JavaScript Mutation Observer
Detecting mutations in the DOM tree can be quite challenging in vanilla JavaScript, but thankfully, there's a constructor that makes it easier.
In this article, we'll dive into the basics of Mutation Observer by driving you through the solution for a task I was assigned a couple of weeks ago at work. So, let's get started!
Context
The task was basically to build an ever-playing video carousel for a client's website. The idea was to have:
3 short muted music videos playing one at a time in the background. Each video would be related to a specific music genre.
A button that, when clicked, would pop up a modal containing a longer music video (with sound) related to the current visible short one. This means that every time the carousel went to a different slide, there would be a different pop-up video.
In short, there'd be a total of 3 short muted videos and 3 long popup videos.
As that project is built mainly with PHP and JQuery, I had no other choice than to figure out how to do all that magic without the flexibility that JavaScript libraries and frameworks offer.
So, after long research, I ran into a StackOverflow solution that recommended using the Mutation Observer constructor.
SPOILER: I'll be mixing JQuery (to reduce code when creating new DOM elements and not spam your screen) and vanilla JavaScript (to make the code less confusing for those who are not familiar with JQuery).
Definition
In a nutshell, this is a powerful JavaScript constructor that allows you to monitor changes to DOM elements in real-time, giving you the ability to respond to user interactions and make dynamic updates to your site.
Syntax
new MutationObserver(callback);
It lets you pass a callback as the first parameter. This callback's first parameter is an array of mutations.
There are 3 types of mutations you can watch for with this constructor: "attributes", "characterData", and "childList".
For more in-depth details, visit the MDN documentation.
NOTE: This constructor may not be necessary when working with libraries or frameworks because it's very likely that they have a more optimized functionality that's equivalent to this one. Maybe they do, maybe they don't. Anyways, it is very useful in vanilla JavaScript.
The Real Challenge
Regarding this task, the real challenge I had was to find a way to develop the dynamic video rendering feature (idea #2 from above).
For this, I noticed that the library I used (slick) to create the carousel was adding an aria-hidden
attribute to each background video
element. This attribute was being used to let the carousel know which video element was visible and which were hidden.
When
aria-hidden
=== "false", thatvideo
was visible.When
aria-hidden
=== "true", thatvideo
was hidden.
Ideas I came up with
There were a total of 5 ideas:
Create an array of strings representing music genres, which would serve as the ID for each video. These IDs would then be used in the .mp4 file names stored in AWS S3.
const musicGenres = ["Reggaeton", "Pop", "Country"];
The URL of every muted music video would be something like this: https://s3.amazonaws.com/bucket-name/Muted_Video_InsertGenreHere.mp4. And for the version with sound, it would be something like this: https://s3.amazonaws.com/bucket-name/Video_InsertGenreHere.mp4.
Loop through the music genres array to create the carousel videos with a
data-genre
attribute, whose value would be their respective genre.musicGenres.forEach(genre => { const videoHtml = ` <video class="carousel-video" preload muted playsinline data-genre=${genre}> <source src=".../Muted_Video_${genre}.mp4" type="video/mp4"> </video> `; // append each new video element to the carousel container $(".videoCarouselContainer").append(videoHtml); });
Create a new MutationObserver instance and listen to changes to the
aria-hidden
attribute of all video elements inside the carousel to know which is the currently visible video.const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { // save targeted element to avoid repeating `mutation.target` const targetedVideoElement = mutation.target; const isCurrentVideo = targetedVideoElement.getAttribute("aria-hidden") === "false"; } });
After saving the currently visible background video element in a variable, grab the value of its
data-genre
attribute.const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { // ... if (isCurrentVideo) { // if video is visible, play it targetedVideoElement.play(); const currentVideoGenre = targetedVideoElement.getAttribute("data-genre"); } else { // if video is not visible, pause and reset it targetedVideoElement.pause(); targetedVideoElement.currentTime = 0; } } });
Finally, create a new
video
element inside the popup modal that is of the same music genre as the current background video.const observer = new MutationObserver(mutationsList => { for (const mutation of mutationsList) { // ... if (isCurrentVideo) { // ... const popupVideoHtml = ` <video controls> <source src=".../Video_${currentVideoGenre}.mp4" type="video/mp4" /> Your browser does not support the video tag. </video> `; // set new video as the content of the modal container $(".ModalContainer").html(popupVideoHtml); } else { // ... } } });
This would work like this: every time there was a slide change event, a new video
element would be created as a child of the modal container.
However, all of this code won't do anything at this point. As the MDN documentation states:
DOM observation does not begin immediately; the
observe()
method must be called first to establish which portion of the DOM to watch and what kinds of changes to watch for.
For this, you just need to call that observe
method from the MutationObserver instance by passing 2 arguments:
the target DOM element
the configuration
const observerConfiguration = {
attributes: true
};
// get all muted video elements
const videoElements = document.querySelectorAll(".carousel-video");
// observe them
videoElements.forEach(videoElement => {
observer.observe(videoElement, observerConfiguration);
});
In this particular case, only attribute mutations (as per the 3rd idea) need to be watched for.
Optimizing the MutationObserver's performance
For better performance, you have to try to be as specific as possible in the configuration object. Otherwise, the performance of this code may be impacted negatively (probably not much but if you're too nitpicky about performance, here you go 😉).
To be more specific, there's the attributeFilter
property which lets you clarify that not all attribute mutations need to be observed and whose value is an array of the only attributes to watch for. With this, the configuration object would end up like this:
const observerConfiguration = {
attributes: true,
attributeFilter: ["aria-hidden"],
};
For more info about the observe
method and its configuration options, refer to the docs.
Final Code
You can find the complete code on this Github Gist.
Conclusion
If you're a web developer looking for a way to track changes to specific DOM elements in your code in real-time, you'll want to pay attention to Mutation Observer.