Skip to content

Commit 81123a7

Browse files
authored
Merge pull request #138 from odsantos/fix-2-ui-99-ui-misc
Update folder 2-ui/99-ui-misc
2 parents 8c18fe3 + f537b63 commit 81123a7

File tree

10 files changed

+1259
-3
lines changed

10 files changed

+1259
-3
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
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

Comments
 (0)