What is a Mutation Observer and when should you use it?
The MutationObserver
is a powerful but lesser-known concept that is supported by all major browsers. It allows you to watch for changes in the DOM.
With it, you can observe:
- When an element is inserted or removed
- When an element is modified (both its attributes and content)
- When an element’s child/children are modified
Cool! But when is it useful?
Using a MutationObserver
in the Real World
At Tailwind, we recently had a case where a MutationObserver
was the perfect tool for the job.
For context, our members use Tailwind to schedule their Instagram posts. They often include captions with their posts, like so:
Our members can edit their post captions directly, as well as select hashtags from the “Suggested Hashtags” section below the post.
Let’s imagine that we want to add a character count to the post caption:
If the post caption was a simple text box where users type in the caption by hand, then we could use an onchange
handler to listen for changes and update the character count, like so:
/**
* When the user edits the caption, update the
* character count element with the new caption length.
*/
function onCaptionChange (event) {
const captionLength = event.target.value.length;
document.querySelector('.post-caption').innerHTML=captionLength;
}
Unfortunately, this solution doesn’t work for us.
The reason? The post caption can be edited directly, but it also changes when a user selects a suggested hashtag!
If a hashtag is inserted into the post caption like so:
…then the onCaptionChange
handler defined above won't be triggered, because the user isn't editing the caption directly. In general, onchange
handlers are not triggered when an element is programmatically changed, but rather when the user interacts with the element. When a Tailwind member selects a hashtag, we programmatically insert it into the post caption, so the onCaptionChange
has no opportunity to fire.
What we need is something that will allow us to listen for any changes to the post caption, regardless of whether those changes come from the user typing in the caption or selecting a hashtag.
Enter the MutationObserver
To see how a MutationObserver
can let us watch for changes to a post's caption, we'll be using a jsfiddle example which you can find here.
Try playing with the link above. Create a caption, insert some hashtags using the buttons on the page, and watch the Character count change like so:
Diving Into the Code
Here’s how the the code works at a high-level:
- Our post caption is a
contenteditable
div where the user creates a caption by typing and/or selecting hashtags to insert. - Any time the post caption changes, the character count below the caption is updated.
Now, let’s break the code down piece by piece to see how we’re using a MutationObserver
to update the character count.
The editable caption
We use a contenteditable
div to allow users to edit the caption. Here's how that looks in the DOM:
<div class="post-caption" contenteditable="true" placeholder="Enter your caption here..."></div>
Counting Characters
We’ll use a simple span to show the character count for the caption:
<span>Character count: </span><span id="character-count">0</span>
(it starts at 0
because the caption is empty before the user starting adding content)
The suggested hashtags
A user can add suggested hashtags by clicking the buttons provided. In production, Tailwind crunches a bunch of data to tell users about the best hashtags to add to a post’s caption, but for this example we’re just using simple static buttons that look like this:
<input type="button" value="#football" onclick="addTextToDiv('#football')" />
<input type="button" value="#luck" onclick="addTextToDiv('#luck')" />
<input type="button" value="#nfl" onclick="addTextToDiv('#nfl')" />
Notice the onclick="addTextToDiv(...)"
handlers? They are responsible for adding a hashtag to the post's caption when the user clicks on a hashtag button. It looks like this:
// Add text to the contenteditable div
const postCaptionElement = document.querySelector('.post-caption');...function addTextToDiv(hashtag) {
postCaptionElement.innerText += ` ${hashtag}`;
}
It’s a straightforward function that takes the post caption element and append the hashtag to the end of the caption (after whatever text the user has already entered).
Now, the fun part!
Let’s look at the MutationObserver
and see how it listens for changes.
The MutationObserver
is defined like so:
// Create a mutation observer instance
const observer = new MutationObserver(function(mutations) {
// For each mutation, update the character count <span>:
mutations.forEach(updateCharacterCount);
});
This instance of the MutationObserver
is defined to listen for changes, and for each change, call the updateCharacterCount
function, which looks this:
const charCountElement = document.querySelector('#character-count');
function updateCharacterCount() {
charCountElement.innerHTML = postCaptionElement.innerText.length;
}
Remember the <span id="character-count">
defined above? The function above directly changes that element by looking at the post caption, getting the length of its content (I.E. how many characters are in the caption), and setting the #character-count
element with the new number of characters.
Up to now, we’ve defined the MutationObserver
and what it needs to do when the post caption changes. The last remaining step is to attach the observer
variable created above to an actual element in the DOM:
// Define the configuration for the observer
const config = {
characterData: true,
childList: true,
subtree: true
};const postCaptionElement = document.querySelector('.post-caption');// Tell the observer what node to observe and what options to use
observer.observe(postCaptionElement, config);
The observer
that we created earlier has a method called observe
, which accepts two arguments:
- The element to observe (in this case, the
<div class="post-caption" contenteditable="true"...>
element) - A
config
object, which tells theMutationObserver
what kinds of changes to watch for. There are bunch of options, which you can learn more about here.
Here are the config
options we use and what they mean:
const config = {
// Watch for changes to the node's text content
characterData: true,
// Watch for changes to the node's direct child/children
childList: true,
// Watch for changes to all of the node's descendants
subtree: true
};
As mentioned, there are other changes you might want to watch for, depending on your use-case. For the purposes of this example, we only care about the content of the post caption (I.E. its text content and any children/descendants).
Tailwind is Hiring!
One of my favorites part about growing as a developer is learning new technical concepts and how to apply them; something I do at Tailwind every day. If this sounds interesting to you and you’re looking for a job, we’d love to talk!