An observer is an object that monitors the state of a specific element and registers changes that occur in it. The element that is being monitored (I almost wrote "for which the surveillance is organized") is called the target. An observer can monitor the state of one or more elements, and in some cases also the descendants of the target element.
There are three main types of observers in JavaScript:
- ResizeObserver
- IntersectionObserver
- MutationObserver
In this article, I propose to focus on the practical implementation of each observer.
Resize Observer
Appointment
Watching the resizing of the target element.
Theory
MDN
My article on Habré
Support
Example
In the following example, we observe the width of the container with the ID "box". When the width of the container is less than 768px, we change the background color of the container and the color of the text (to the opposite with "filter: invert (100%)"), reduce the font size of the heading and body text, and hide additional information.
The markup looks like this:
<div id="box" class="box">
<h1 id="title" class="title">Some Awesome Title</h1>
<p id="text" class="text">Some Main Text</p>
<span id="info" class="info">Some Secondary Info</span>
</div>
Styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.box,
.title,
.text,
.info {
transition: 0.3s;
}
.box {
background: #ddd;
color: #222;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.title,
.info {
margin: 1rem;
}
.title {
font-size: 2rem;
}
.text {
font-size: 1.25rem;
}
Script:
//
const changeStyles = (elements, properties, values) =>
elements.forEach((element, index) => {
element.style[properties[index]] = values[index];
});
// ResizeObserver
const observer = new ResizeObserver((entries) => {
// ( )
for (const entry of entries) {
//
const width = entry.contentRect.width;
//
// 768px
//
if (width < 768) {
changeStyles(
[box, title, text, info],
["filter", "fontSize", "fontSize", "opacity"],
["invert(100%)", "1.5rem", "1rem", "0"]
);
} else {
// 768px
//
changeStyles(
[box, title, text, info],
["filter", "fontSize", "fontSize", "opacity"],
["invert(0%)", "2rem", "1.25rem", "1"]
);
}
}
});
// "box"
observer.observe(box);
Sandbox:
IntersectionObserver
Appointment
Observing the intersection of the target element with the parent element or the page's viewport.
Theory
MDN
My article on Habré
Support
Example
In the following example, we watch all sections on the page and write the current section number (its identifier) to local storage. This is done so that when the user returns to the page, scroll the viewport to the section where he left off. Please note that the example implements smooth scrolling: on pages with a lot of information, it is better to scroll instantly.
Markup:
<main id="main">
<section id="1" class="section">
<h3 class="title">First Section Title</h3>
<p class="text">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Ipsam nostrum ex delectus distinctio reprehenderit facere vitae beatae ab dolores, aliquam maiores officia mollitia unde et! Quaerat odit in minus dolor corrupti nemo nam beatae. Ex consequatur rem laborum necessitatibus omnis, soluta fuga maiores repellendus eveniet? Blanditiis quae officiis maiores vitae nobis in voluptate, dicta voluptas rerum. Et laudantium consequuntur vitae tenetur doloremque accusantium tempora quos magni repudiandae voluptatem perferendis velit reprehenderit laborum libero soluta quis id, quidem assumenda nihil obcaecati expedita, aliquam suscipit nesciunt facere. Voluptate rem perferendis ab iste? Maxime, earum quos! Modi, aut quis nihil quidem accusamus vero sunt debitis architecto soluta repellendus fugit suscipit aspernatur labore a est sit dolores in necessitatibus ea tenetur corporis. Exercitationem mollitia impedit qui nemo voluptate numquam perspiciatis repellendus repellat a odio fugit dolor ducimus labore ex veritatis pariatur aliquam enim distinctio libero doloremque saepe quaerat consectetur, ut sapiente. Laboriosam dignissimos iure praesentium modi ab perferendis at molestias maiores suscipit, expedita aut aperiam nam voluptates similique optio minus quam! Voluptas ullam sunt, a officiis accusamus adipisci sed saepe voluptatem minima maxime est assumenda cum quibusdam voluptates provident in quasi vitae. Corrupti voluptatibus laborum ipsum quia, cupiditate adipisci assumenda dolores sunt distinctio, recusandae nesciunt aliquid, explicabo ullam eligendi perspiciatis rerum architecto? Cumque numquam blanditiis, magnam delectus velit laudantium aliquid quibusdam excepturi vero nihil necessitatibus, sed officiis, molestias hic autem modi consequuntur iusto sapiente dolore. Voluptates tenetur provident eius distinctio iure rerum minima eum eaque. Ea autem, deleniti atque magnam eius modi dicta assumenda tempore ducimus molestias. Aperiam enim tenetur, hic blanditiis velit quod odio deserunt sequi quisquam dignissimos animi amet magnam excepturi dicta quidem error quis officia natus. Temporibus nobis dolores veritatis eius illo quas perspiciatis reiciendis dolorum optio, commodi, animi quos at! Amet praesentium totam ab error esse optio quo, quis iusto!
</p>
</section>
<section id="2" class="section">
<h3 class="title">Second Section Title</h3>
<p class="text">
...
</p>
</section>
<section id="3" class="section">
<h3 class="title">Third Section Title</h3>
<p class="text">
...
</p>
</section>
</main>
Styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #eee;
color: #222;
text-align: center;
}
main {
max-width: 768px;
margin: auto;
}
.section {
padding: 1rem;
}
.title {
font-size: 1.5rem;
margin: 1rem;
}
.text {
font-size: 1.25rem;
}
Script:
// ,
const findLastSection = () => {
//
// -
const number = localStorage.getItem("numberOfSection") || 1;
//
const section = document.getElementById(number);
// ( )
const position = Math.round(section.offsetTop);
//
scrollTo({
top: position,
//
behavior: "smooth",
});
};
findLastSection();
//
const createObserver = () => {
// IntersectionObserver
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
//
if (entry.isIntersecting) {
//
localStorage.setItem("numberOfSection", entry.target.id);
}
});
},
{
//
// 10%
threshold: 0.1,
}
);
//
const sections = main.querySelectorAll("section");
//
sections.forEach((section) => observer.observe(section));
};
createObserver();
Sandbox:
MutationObserver
Appointment
Watch for changes in attributes, text content of the target element and its descendants. Perhaps, in terms of functionality, this is the most interesting of the observers we are considering.
Theory
MDN
Modern JavaScript Tutorial
Support
Example
In the following example, we will implement a simple trick, in which the observer keeps track of the number of tasks on the list. In terms of functionality, our observer will be similar to “useEffect” in React.js or “watch” in Vue.js.
Markup:
<div id="box" class="box"></div>
Styles:
@import url("https://fonts.googleapis.com/css2?family=Stylish&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: stylish;
font-size: 1rem;
color: #222;
}
.box {
max-width: 512px;
margin: auto;
text-align: center;
}
.counter {
font-size: 2.25rem;
margin: 0.75rem;
}
.form {
display: flex;
margin-bottom: 0.25rem;
}
.input {
flex-grow: 1;
border: none;
border-radius: 4px;
box-shadow: 0 0 1px inset #222;
text-align: center;
font-size: 1.15rem;
margin: 0.5rem 0.25rem;
}
.input:focus {
outline-color: #5bc0de;
}
.btn {
border: none;
outline: none;
background: #337ab7;
padding: 0.5rem 1rem;
border-radius: 4px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.5);
color: #eee;
margin: 0.5rem 0.25rem;
cursor: pointer;
user-select: none;
width: 92px;
text-shadow: 0 0 1px rgba(0, 0, 0, 0.5);
}
.btn:active {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.5) inset;
}
.btn.danger {
background: #d9534f;
}
.list {
list-style: none;
}
.item {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
}
.item + .item {
border-top: 1px dashed rgba(0, 0, 0, 0.5);
}
.text {
flex: 1;
font-size: 1.15rem;
margin: 0.5rem;
padding: 0.5rem;
background: #eee;
border-radius: 4px;
}
Script:
//
const todos = [
{
id: "1",
text: "Learn HTML",
},
{
id: "2",
text: "Learn CSS",
},
{
id: "3",
text: "Learn JavaScript",
},
{
id: "4",
text: "Stay alive",
},
];
//
const Item = (todo) => `
<li
class="item"
id="${todo.id}"
>
<span class="text"}">
${todo.text}
</span>
<button
class="btn danger"
data-type="delete"
>
Delete
</button>
</li>
`;
//
const Template = `
<form id="form" class="form">
<input
type="text"
class="input"
id="input"
>
<button
class="btn"
data-type="add"
>
Add
</button>
</form>
<ul id="list" class="list">
${todos.reduce(
(html, todo) =>
(html += `
${Item(todo)}
`),
""
)}
</ul>
`;
// IIFE
(() => {
// "box"
box.innerHTML = `
<h1 id="counter" class="counter">
${todos.length} todo(s) left
</h1>
${Template}
`;
//
input.focus();
// MutationObserver
const observer = new MutationObserver(() => {
//
const count = todos.length;
// , , ,
counter.textContent = `
${count > 0 ? `${count} todo(s) left` : "There are no todos"}
`;
});
//
observer.observe(list, {
childList: true,
});
//
const addTodo = () => {
if (!input.value.trim()) return;
const todo = {
id: Date.now().toString().slice(-4),
text: input.value,
};
list.insertAdjacentHTML("beforeend", Item(todo));
todos.push(todo);
input.value = "";
input.focus();
};
//
const deleteTodo = (item) => {
const index = todos.findIndex((todo) => todo.id === item.id);
item.remove();
todos.splice(index, 1);
};
//
form.onsubmit = (e) => e.preventDefault();
//
box.addEventListener("click", (e) => {
if (e.target.tagName !== "BUTTON") return;
//
const { type } = e.target.dataset;
const item = e.target.parentElement;
//
switch (type) {
case "add":
addTodo();
break;
default:
deleteTodo(item);
break;
}
});
})();
Sandbox:
I hope you found something interesting for yourself. Thank you for attention.