MutationObserver
is a lesser known JavaScript feature which allows you to detect when elements
in a web page are inserted, changed or removed. It is still relatively new,
but it is supported by every modern
browser.
The web is full of demos and tutorials of MutationObserver, but it’s pretty hard to find examples of it actually being used in practice. Even a search of Github is almost all libraries and test cases. We’ve had a couple occasions to use it at Eager however, which I now have the opportunity to share.
Client-side Image Optimization
Believe it or not, it’s actually possible to swap the src’s of img tags before the browser begins to load them. We can use that to optimize our images without changing the HTML source of our page. This code uses a FireSize service to handle the actual optimization.
We start by setting up a MutationObserver
which will call our checkNode
function with any new nodes which are added to the DOM:
var observer = new MutationObserver(function(mutations){
for (var i=0; i < mutations.length; i++){
for (var j=0; j < mutations[i].addedNodes.length; j++){
checkNode(mutations[i].addedNodes[j]);
}
}
});
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
If we run this code early in the head of the page, it will call our checkNode
function with each DOM node as the browser parses the page’s HTML. This gives
us the ability to check or mutate these nodes before they’ve ever been
rendered.
We can define our checkNode
function to decide if this is an image for us to
optimize.
checkNode = function(addedNode) {
if (addedNode.nodeType === 1 && addedNode.tagName === 'IMG'){
addedNode.src = optimizeSrc(addedNode.src)
}
}
Finally, we can define optimizeSrc
to switch out our image’s src for an
optimized one:
optimizeSrc = function(src) {
return "//firesize.com/" + src;
}
For a complete implementation, take a look at our FireSize app source code.
Initializing When An Element Becomes Available on the Page
It’s a common pattern to wait for jQuery.ready
or DOMContentLoaded
to
initialize code which depends on elements on the page. Those events don’t fire
until the entire DOM has loaded however, meaning the page will start to be
rendered before you have a chance to change or add to its content.
Our pattern from the image optimization solution also works for detecting when
any element becomes available, allowing you to initialize code which depends on
that element at the exact first moment it’s possible. We can redefine
checkNode
to instead check if our element matches an arbitrary selector:
checkNode = function(addedNode) {
if (addedNode.nodeType === 1){
if (addedNode.matches('.should-underline')){
SmartUnderline.init(addedNode);
}
}
}
Managing ContentEditable Regions
As you may know, the contenteditable
attribute can be used to make any HTML
element editable by the user. For example:
<div contenteditable>
<h1>My awesome content!</h1>
<p>You can edit this</p>
</div>
Will produce this editable element:
My awesome content!
You can edit this
If you are truely building an editor, it’s common to want some control over what the user can enter. You can use MutationObservers to prevent certain modifications, or take action when they occur. For example, lets say we want some (very basic) support for ‘markdown-style’ links in our editable area.
We begin with the standard MutationObserver binding, this time paying attention
to changes in the characterData
of our fields, and calling a function which
we will later define called replaceLinks
:
var observer = new MutationObserver(function(mutations){
for (var i=0; i < mutations.length; i++){
replaceLinks(mutations[i].target);
}
})
observer.observe(document.querySelector('[contenteditable]'), {
characterData: true,
subtree: true
})
The replaceLinks
function itself uses a regular expression to find
markdown-style links and swap them out for HTML links:
function replaceLinks(target){
// Replace markdown encoded links
// a la [Text](URL)
// with <a href='URL'>Text</a>
var content = target.textContent.replace(
/(.*)\[([^\]]+)\]\(([^\]]+)\)(.*)/g,
"$1<a href='$3'>$2</a>$4"
)
if (content !== target.textContent){
var newNode = document.createElement('template')
newNode.innerHTML = content
target.parentElement.replaceChild(newNode.content, target)
}
}
This is, by necessity, a simplified example. I know of at least one markdown editor which does try to do this, with pretty user-hostile consequences. Please don’t actually do this type of dynamic replacement of text without careful consideration. You could, however, insert a new element to show a menu, or something similar.
Here’s a full demo of the working code. Try adding a [markdown-style](link)
:
My awesome content!
You can edit this
Hopefully that gave you some ideas for how you might use MutationObserver
in
your future projects. Our next post is going to be on the history of CSS
and what (very interesting) alernatives were considered instead. Subscribe
below to be notified when it’s released.