|
| 1 | + |
| 2 | +# Mutation observer |
| 3 | + |
| 4 | +`MutationObserver` is a built-in object that observes a DOM element and fires a callback in case of changes. |
| 5 | + |
| 6 | +We'll first take a look at the syntax, and then explore a real-world use case, to see where such thing may be useful. |
| 7 | + |
| 8 | +## Syntax |
| 9 | + |
| 10 | +`MutationObserver` is easy to use. |
| 11 | + |
| 12 | +First, we create an observer with a callback-function: |
| 13 | + |
| 14 | +```js |
| 15 | +let observer = new MutationObserver(callback); |
| 16 | +``` |
| 17 | + |
| 18 | +And then attach it to a DOM node: |
| 19 | + |
| 20 | +```js |
| 21 | +observer.observe(node, config); |
| 22 | +``` |
| 23 | + |
| 24 | +`config` is an object with boolean options "what kind of changes to react on": |
| 25 | +- `childList` -- changes in the direct children of `node`, |
| 26 | +- `subtree` -- in all descendants of `node`, |
| 27 | +- `attributes` -- attributes of `node`, |
| 28 | +- `attributeFilter` -- an array of attribute names, to observe only selected ones. |
| 29 | +- `characterData` -- whether to observe `node.data` (text content), |
| 30 | + |
| 31 | +Few other options: |
| 32 | +- `attributeOldValue` -- if `true`, pass both the old and the new value of attribute to callback (see below), otherwise only the new one (needs `attributes` option), |
| 33 | +- `characterDataOldValue` -- if `true`, pass both the old and the new value of `node.data` to callback (see below), otherwise only the new one (needs `characterData` option). |
| 34 | + |
| 35 | +Then after any changes, the `callback` is executed: changes are passed in the first argument as a list of [MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects, and the observer itself as the second argument. |
| 36 | + |
| 37 | +[MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects have properties: |
| 38 | + |
| 39 | +- `type` -- mutation type, one of |
| 40 | + - `"attributes"`: attribute modified |
| 41 | + - `"characterData"`: data modified, used for text nodes, |
| 42 | + - `"childList"`: child elements added/removed, |
| 43 | +- `target` -- where the change occurred: an element for `"attributes"`, or text node for `"characterData"`, or an element for a `"childList"` mutation, |
| 44 | +- `addedNodes/removedNodes` -- nodes that were added/removed, |
| 45 | +- `previousSibling/nextSibling` -- the previous and next sibling to added/removed nodes, |
| 46 | +- `attributeName/attributeNamespace` -- the name/namespace (for XML) of the changed attribute, |
| 47 | +- `oldValue` -- the previous value, only for attribute or text changes, if the corresponding option is set `attributeOldValue`/`characterDataOldValue`. |
| 48 | + |
| 49 | +For example, here's a `<div>` with a `contentEditable` attribute. That attribute allows us to focus on it and edit. |
| 50 | + |
| 51 | +```html run |
| 52 | +<div contentEditable id="elem">Click and <b>edit</b>, please</div> |
| 53 | + |
| 54 | +<script> |
| 55 | +let observer = new MutationObserver(mutationRecords => { |
| 56 | + console.log(mutationRecords); // console.log(the changes) |
| 57 | +}); |
| 58 | +
|
| 59 | +// observe everything except attributes |
| 60 | +observer.observe(elem, { |
| 61 | + childList: true, // observe direct children |
| 62 | + subtree: true, // and lower descendants too |
| 63 | + characterDataOldValue: true // pass old data to callback |
| 64 | +}); |
| 65 | +</script> |
| 66 | +``` |
| 67 | + |
| 68 | +If we run this code in the browser, then focus on the given `<div>` and change the text inside `<b>edit</b>`, `console.log` will show one mutation: |
| 69 | + |
| 70 | +```js |
| 71 | +mutationRecords = [{ |
| 72 | + type: "characterData", |
| 73 | + oldValue: "edit", |
| 74 | + target: <text node>, |
| 75 | + // other properties empty |
| 76 | +}]; |
| 77 | +``` |
| 78 | + |
| 79 | +If we make more complex editing operations, e.g. remove the `<b>edit</b>`, the mutation event may contain multiple mutation records: |
| 80 | + |
| 81 | +```js |
| 82 | +mutationRecords = [{ |
| 83 | + type: "childList", |
| 84 | + target: <div#elem>, |
| 85 | + removedNodes: [<b>], |
| 86 | + nextSibling: <text node>, |
| 87 | + previousSibling: <text node> |
| 88 | + // other properties empty |
| 89 | +}, { |
| 90 | + type: "characterData" |
| 91 | + target: <text node> |
| 92 | + // ...mutation details depend on how the browser handles such removal |
| 93 | + // it may coalesce two adjacent text nodes "edit " and ", please" into one node |
| 94 | + // or it may leave them separate text nodes |
| 95 | +}]; |
| 96 | +``` |
| 97 | + |
| 98 | +So, `MutationObserver` allows to react on any changes within DOM subtree. |
| 99 | + |
| 100 | +## Usage for integration |
| 101 | + |
| 102 | +When such thing may be useful? |
| 103 | + |
| 104 | +Imagine the situation when you need to add a third-party script that contains useful functionality, but also does something unwanted, e.g. shows ads `<div class="ads">Unwanted ads</div>`. |
| 105 | + |
| 106 | +Naturally, the third-party script provides no mechanisms to remove it. |
| 107 | + |
| 108 | +Using `MutationObserver`, we can detect when the unwanted element appears in our DOM and remove it. |
| 109 | + |
| 110 | +There are other situations when a third-party script adds something into our document, and we'd like to detect, when it happens, to adapt our page, dynamically resize something etc. |
| 111 | + |
| 112 | +`MutationObserver` allows to implement this. |
| 113 | + |
| 114 | +## Usage for architecture |
| 115 | + |
| 116 | +There are also situations when `MutationObserver` is good from architectural standpoint. |
| 117 | + |
| 118 | +Let's say we're making a website about programming. Naturally, articles and other materials may contain source code snippets. |
| 119 | + |
| 120 | +Such snippet in an HTML markup looks like this: |
| 121 | + |
| 122 | +```html |
| 123 | +... |
| 124 | +<pre class="language-javascript"><code> |
| 125 | + // here's the code |
| 126 | + let hello = "world"; |
| 127 | +</code></pre> |
| 128 | +... |
| 129 | +``` |
| 130 | + |
| 131 | +Also we'll use a JavaScript highlighting library on our site, e.g. [Prism.js](https://prismjs.com/). A call to `Prism.highlightElem(pre)` examines the contents of such `pre` elements and adds into them special tags and styles for colored syntax highlighting, similar to what you see in examples here, at this page. |
| 132 | + |
| 133 | +When exactly to run that highlighting method? We can do it on `DOMContentLoaded` event, or at the bottom of the page. At that moment we have our DOM ready, can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them: |
| 134 | + |
| 135 | +```js |
| 136 | +// highlight all code snippets on the page |
| 137 | +document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem); |
| 138 | +``` |
| 139 | + |
| 140 | +Everything's simple so far, right? There are `<pre>` code snippets in HTML, we highlight them. |
| 141 | + |
| 142 | +Now let's go on. Let's say we're going to dynamically fetch materials from a server. We'll study methods for that [later in the tutorial](info:fetch). For now it only matters that we fetch an HTML article from a webserver and display it on demand: |
| 143 | + |
| 144 | +```js |
| 145 | +let article = /* fetch new content from server */ |
| 146 | +articleElem.innerHTML = article; |
| 147 | +``` |
| 148 | + |
| 149 | +The new `article` HTML may contain code snippets. We need to call `Prism.highlightElem` on them, otherwise they won't get highlighted. |
| 150 | + |
| 151 | +**Where and when to call `Prism.highlightElem` for a dynamically loaded article?** |
| 152 | + |
| 153 | +We could append that call to the code that loads an article, like this: |
| 154 | + |
| 155 | +```js |
| 156 | +let article = /* fetch new content from server */ |
| 157 | +articleElem.innerHTML = article; |
| 158 | + |
| 159 | +*!* |
| 160 | +let snippets = articleElem.querySelectorAll('pre[class*="language-"]'); |
| 161 | +snippets.forEach(Prism.highlightElem); |
| 162 | +*/!* |
| 163 | +``` |
| 164 | + |
| 165 | +...But imagine, we have many places in the code where we load contents: articles, quizzes, forum posts. Do we need to put the highlighting call everywhere? That's not very convenient, and also easy to forget. |
| 166 | + |
| 167 | +And what if the content is loaded by a third-party module? E.g. we have a forum written by someone else, that loads contents dynamically, and we'd like to add syntax highlighting to it. No one likes to patch third-party scripts. |
| 168 | + |
| 169 | +Luckily, there's another option. |
| 170 | + |
| 171 | +We can use `MutationObserver` to automatically detect when code snippets are inserted in the page and highlight them. |
| 172 | + |
| 173 | +So we'll handle the highlighting functionality in one place, relieving us from the need to integrate it. |
| 174 | + |
| 175 | +### Dynamic highlight demo |
| 176 | + |
| 177 | +Here's the working example. |
| 178 | + |
| 179 | +If you run this code, it starts observing the element below and highlighting any code snippets that appear there: |
| 180 | + |
| 181 | +```js run |
| 182 | +let observer = new MutationObserver(mutations => { |
| 183 | + |
| 184 | + for(let mutation of mutations) { |
| 185 | + // examine new nodes, is there anything to highlight? |
| 186 | + |
| 187 | + for(let node of mutation.addedNodes) { |
| 188 | + // we track only elements, skip other nodes (e.g. text nodes) |
| 189 | + if (!(node instanceof HTMLElement)) continue; |
| 190 | + |
| 191 | + // check the inserted element for being a code snippet |
| 192 | + if (node.matches('pre[class*="language-"]')) { |
| 193 | + Prism.highlightElement(node); |
| 194 | + } |
| 195 | + |
| 196 | + // or maybe there's a code snippet somewhere in its subtree? |
| 197 | + for(let elem of node.querySelectorAll('pre[class*="language-"]')) { |
| 198 | + Prism.highlightElement(elem); |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + |
| 203 | +}); |
| 204 | + |
| 205 | +let demoElem = document.getElementById('highlight-demo'); |
| 206 | + |
| 207 | +observer.observe(demoElem, {childList: true, subtree: true}); |
| 208 | +``` |
| 209 | + |
| 210 | +Here, below, there's an HTML-element and JavaScript that dynamically fills it using `innerHTML`. |
| 211 | + |
| 212 | +Please run the previous code (above, observes that element), and then the code below. You'll see how `MutationObserver` detects and highlights the snippet. |
| 213 | + |
| 214 | +<p id="highlight-demo" style="border: 1px solid #ddd">A demo-element with <code>id="highlight-demo"</code>, run the code above to observe it.</p> |
| 215 | + |
| 216 | +The following code populates its `innerHTML`, that causes the `MutationObserver` to react and highlight its contents: |
| 217 | + |
| 218 | +```js run |
| 219 | +let demoElem = document.getElementById('highlight-demo'); |
| 220 | + |
| 221 | +// dynamically insert content with code snippets |
| 222 | +demoElem.innerHTML = `A code snippet is below: |
| 223 | + <pre class="language-javascript"><code> let hello = "world!"; </code></pre> |
| 224 | + <div>Another one:</div> |
| 225 | + <div> |
| 226 | + <pre class="language-css"><code>.class { margin: 5px; } </code></pre> |
| 227 | + </div> |
| 228 | +`; |
| 229 | +``` |
| 230 | + |
| 231 | +Now we have `MutationObserver` that can track all highlighting in observed elements or the whole `document`. We can add/remove code snippets in HTML without thinking about it. |
| 232 | + |
| 233 | +## Additional methods |
| 234 | + |
| 235 | +There's a method to stop observing the node: |
| 236 | + |
| 237 | +- `observer.disconnect()` -- stops the observation. |
| 238 | + |
| 239 | +When we stop the observing, it might be possible that some changes were not processed by the observer yet. |
| 240 | + |
| 241 | +- `observer.takeRecords()` -- gets a list of unprocessed mutation records, those that happened, but the callback did not handle them. |
| 242 | + |
| 243 | +These methods can be used together, like this: |
| 244 | + |
| 245 | +```js |
| 246 | +// we'd like to stop tracking changes |
| 247 | +observer.disconnect(); |
| 248 | + |
| 249 | +// handle unprocessed some mutations |
| 250 | +let mutationRecords = observer.takeRecords(); |
| 251 | +... |
| 252 | +``` |
| 253 | + |
| 254 | +```smart header="Garbage collection interaction" |
| 255 | +Observers use weak references to nodes internally. That is: if a node is removed from DOM, and becomes unreachable, then it becomes garbage collected. |
| 256 | +
|
| 257 | +The mere fact that a DOM node is observed doesn't prevent the garbage collection. |
| 258 | +``` |
| 259 | + |
| 260 | +## Summary |
| 261 | + |
| 262 | +`MutationObserver` can react on changes in DOM: attributes, added/removed elements, text content. |
| 263 | + |
| 264 | +We can use it to track changes introduced by other parts of our code, as well as to integrate with third-party scripts. |
| 265 | + |
| 266 | +`MutationObserver` can track any changes. The config "what to observe" options are used for optimizations, not to spend resources on unneeded callback invocations. |
0 commit comments