JavaScript Mapping Library
While driving my kids to school this morning, I had an interesting thought. Is it possible for a web component to recognize, and respond, when its inner DOM contents have changed? Turns out of course it is, and the answer isn’t really depenedant on web components, but is a baked-in part of the web platform, the MutationObserver. Here’s what I built as a way to test it out.
I began with a simple web component that had the following simple feature – count the number of images inside it and report. So we can start with this HTML:
<img-counter> <p> <img src="https://placehold.co/60x40"> </p> <div> <img src="https://placehold.co/40x40"> </div> <img src="https://placehold.co/90x90"></img-counter>
And build a simple component:
class ImgCounter extends HTMLElement { constructor() { super(); } connectedCallback() { let imgs = this.querySelectorAll('img'); this.innerHTML += `<p>There are <strong>$ {imgs.length}</strong> images in me.</p>`; } }if(!customElements.get('img-counter')) customElements.define('img-counter', ImgCounter);
It just uses querySelectorAll to count the img node inside it. For my initial HTML, this reports 3 of course.
querySelectorAll
img
I then added a simple button to my HTML:
<button id="testAdd">Add Img</button>
And a bit of code:
document.querySelector('#testAdd').addEventListener('click', () => { document.querySelector('img-counter').innerHTML = '<p>New: <img src="https://placehold.co/100x100"></p>' + document.querySelector('img-counter').innerHTML;});
When run, it will add a new image, but obviously, the counter won’t update. Here’s a CodePen of this initial version:
See the Pen Img Counter WC 1 by Raymond Camden (@cfjedimaster) on CodePen.
The MDN docs on MutationObserver are pretty good, as always. I won’t repeat what’s written there but the basics are:
So my thinking was…
My first attempt was rather naive, but here it is in code form, not CodePen, for reasons that will be clear soon:
class ImgCounter extends HTMLElement { constructor() { super(); } connectedCallback() { this.renderCount(); const mutationObserver = (mutationList, observer) => { this.renderCount(); }; const observer = new MutationObserver(mutationObserver); observer.observe(this, { childList: true, subtree: true }); } renderCount() { let imgs = this.querySelectorAll('img'); this.innerHTML += `<div><p>There are <strong>$ {imgs.length}</strong> images in me.</p></div>`; }}
So the MutationObserver callback is sent information about what changed, and in my simple little mind, I figured, I don’t care. If something changes, just rerun the count to count images.
Look at that code and see if you can figure out the issue. If you can, leave me a comment below.
So yes, this "worked", but this is what happened:
renderCount
I had to tweak things a bit, but here’s the final version, and I’ll explain what I did:
class ImgCounter extends HTMLElement { #myObserver; constructor() { super(); } connectedCallback() { // create the div we'll use to monitor images: this.innerHTML += '<div id="imgcountertext"></div>'; this.renderCount(); const mutationObserver = (mutationList, observer) => { for(const m of mutationList) { if(m.target === this) { this.renderCount(); } } }; this.myObserver = new MutationObserver(mutationObserver); this.myObserver.observe(this, { childList: true, subtree: true }); } disconnectedCallback() { this.myObserver.disconnect(); } renderCount() { let imgs = this.querySelectorAll('img'); this.querySelector('#imgcountertext').innerHTML = `There are <strong>$ {imgs.length}</strong> images in me.`; }}if(!customElements.get('img-counter')) customElements.define('img-counter', ImgCounter);document.querySelector('#testAdd').addEventListener('click', () => { document.querySelector('img-counter').innerHTML = '<p>New: <img src="https://placehold.co/100x100"></p>' + document.querySelector('img-counter').innerHTML;});
I initially had said I didn’t care about what was in the list of items changed in the mutation observer, but I noticed that the target value was different when I specifically added my image count report. To help with this, I’m now using a div tag with an ID and renderCount modifies that.
target
div
When a new image (or anything) is added directly inside the component, my target value is img-counter, or this, which means I can run renderCount on it. When renderCount runs, the target of the mutation is its own div.
img-counter
this
Also, I noticed that the MutationObserver talks specifically called out the disconnect method as a way of ending the DOM observation. That feels pretty important, and web components make it easy with the disconnectedCallback method.
disconnect
disconnectedCallback
All in all, it works well now (as far as I know ;), and you can test it yourself below:
See the Pen Img Counter WC 2 by Raymond Camden (@cfjedimaster) on CodePen.
Remember, MutationObserver can absolutely be used outside of web components. Also note that if you only want to respond to an attribute change in a web component, that’s really easy as it’s baked into the spec. As always, let me know what you think, and I’ve got a strong feeling that someone going to show me a better way of doing this, and I’d be happy to see it!
Raymond Camden
You must be logged in to post a comment.
This site uses Akismet to reduce spam. Learn how your comment data is processed.