Observer API
Resize observer
Obiekt typu Resize Observer pozwala nam reagować na zmiany rozmiaru obserwowanego elementu (lub elementów) na stronie. Dzięki temu nie musimy za każdym razem podłączać się pod zdarzenie resize
dla window, co z pewnością wyjdzie na korzyść jeżeli chodzi o wydajność naszej strony.
Aby zacząć reagować musimy stworzyć nowy obiekt typu ResizeObserver
, a następnie użyć go do obserwowania danych elementów:
//tworzymy observer
const observer = new ResizeObserver((elements, observer) => {
//elements - tablica obserwowanych przez ten observer elementów
//observer - dany observer
for (let el of elements) {
console.log(el);
console.log(el.target); //element który właśnie się zmienił
}
});
//zaczynamy obserwować jakieś elementy
const el = document.querySelector(".resize-element");
observer.observe(el);
Rozszerzając lewego diva zmień rozmiar prawego i sprawdź w konsoli:
Gdy spojrzysz na wynik, zobaczysz, że funkcja zwrotna daje nam dostęp do kilku przydatnych informacji:
const observer = new ResizeObserver((elements, observer) => {
for (let el of elements) {
console.log(el.contentBoxSize) //rozmiar treści elementu bez padding i border
console.log(el.borderBoxSize) //rozmiar wraz z padding i borderem
console.log(el.contentRect) //rozmiar treści
console.log(el.target) //dany element który się zmienił
}
});
Obiekt typu ResizeObserver
daje nam dostęp do kilku funkcji:
observer.disconnect() | Przestaje obserwować wszystkie obserwowane elementy |
---|---|
observer.observe(el) | zaczyna obserwować dany element |
observer.unobserve(el) | przestaje obserwować dany element |
Przez lata budowaliśmy (budujemy i będziemy budować) strony w oparciu o @media queries
, dla których piszemy warunki w odniesieniu do całej szerokości strony. Od jakiegoś czasu możemy już eksperymentować z @container queries, które pozwalają nam pisać odpowiednie warunki w odniesieniu do rozmiaru elementu-rodzica.
Na pełne wsparcie tego zapisu musimy jeszcze chwilkę poczekać, natomiast nic nie stoi na przeszkodzie, by podobną rzecz zrobić za pomocą ResizeObserver
:
.is-small {
background: crimson;
}
.is-medium {
background: dodgerblue;
}
.is-big {
background: gold;
}
.is-large {
background: var(--color-main);
}
const div = document.querySelector("div");
const breakpoints = {
"small" : 0,
"medium" : 200,
"big" : 300,
"large" : 500
}
const observer = new ResizeObserver(elements => {
for (let el of elements) {
//usuwam wszystkie klasy
const classToDelete = Object.keys(breakpoints).map(key => `is-${key}`);
el.target.classList.remove(...classToDelete);
//dodaję odpowiednią klasę tylko jeżeli szerokość elementu jest większa niż danego breakpointa
let className = "";
for (let [key, val] of Object.entries(breakpoints)) {
if (el.borderBoxSize[0].inlineSize > val) className = `is-${key}`;
}
if (className !== "") el.target.classList.add(className);
}
});
btnAdd.addEventListener("click", e => {
const newDiv = document.createElement("div")
newDiv.textContent = ++count;
div.append(newDiv);
observer.observe(newDiv);
});
Oczywiście to tylko prosty przykład, który warto przeanalizować i w razie potrzeby zmodyfikować.
Mutation observer
Kolejny w kolejce to Mutation Observer, który służy do obserwowania zmian struktury html danych elementów.
Użycie tego obserwatora jest bardzo podobne do powyższego. Główną różnicą jest to, że tworząc nowy obiekt tego typu musimy poza elementem do obserwowania przekazać dodatkowe opcje, które wskażą co i jak obserwować.
let observer = new MutationObserver(mutations => {
...
});
const observerOptions = {
childList: true, //czy obserwować dzieci danego elementu
attributes: true, //czy obserwować atrybuty
characterData: false, //czy obserwować zmiany tekstu
subtree: false, //czy obserwować zmiany drzewa we wnętrzu obserwowanego elementu
attributeFilter: ["title", "date-text"], //zmiany w jakich atrybutach mają być obserwowane
attributeOldValue: false, //czy rejestrować wartości atrybutów przed zmianą
characterDataOldValue: false, //czy rejestrować wartość tekstu sprzed zmiany
subtree: true //false - jeżeli chcemy obserwować zmiany tylko danego elementu bez jego dzieci
}
const div = document.querySelector(".element");
observer.observe(div, observerOptions);
Podając opcje musimy ustawić przynajmniej jedną z pierwszej trójki na true
. Cała reszta parametrów jest opcjonalna.
Jeżeli chodzi o metody to mamy dostęp do:
observer.observe(el) | zaczyna obserwować dany element |
---|---|
observer.disconnect() | wyłącza obserwowanie |
observer.takeRecords() | zwraca listę wszystkich zmian na DOM które zostały wykryte, ale jeszcze nie trafiły do funkcji zwrotnej, którą podłączyliśmy |
Przykład użycia:
<div class="test-container">
<div id="div" class="test-element">
<div>Jakaś <strong>przykładowa treść</strong></div>
</div>
<button class="button" id="buttonChangeAttr">Zmień atrybut</button>
<button class="button" id="buttonAdd">Dodaj element</button>
</div>
const div = document.querySelector("div");
const observer = new MutationObserver(mutationList => {
for (let mutation of mutationList) {
console.log(mutation);
}
});
observer.observe(div, {
childList: true,
attributes: true,
subtree: true
});
Sprawdź poniższy div w debuggerze
Dodatkowy artykuł na temat Mutation Observer znajdziesz pod adresem https://www.smashingmagazine.com/2019/04/mutationobserver-api-guide/
Intersection observer
Kolejnym obserwatorem jest Intersection observer, który służy do obserwowania czy dany element znajdzie się w widocznym obszarze ekranu lub przewijanego elementu.
Omawiane działanie możesz spotkać na wielu "fikuśnych" stronach, na których podczas przewijania nagle zaczynają się animować różne elementy.
Takie rozpoczęcie animacji możemy wykrywać na dwa sposoby. Stara metoda to podpięcie się pod zdarzenie scroll
dla danego elementu (najczęściej window) i sprawdzanie w nim, czy dany element pojawił się na ekranie:
const el = document.querySelector(".element-to-check");
function checkAndToggle() {
const pos = el.getBoundingClientRect(); //pobieram pozycję i rozmiar elementu
const offset = pos.height / 2;
const bottomEdge = pos.bottom - offset;
const topEdge = post.bottom + offset;
if (bottomEdge > window.scrollY || topEdge < window.scrollY + window.innerHeight) {
//jest
} else {
//nie ma
}
}
window.addEventListener("scroll", checkAndToggle);
Nie jest to jednak idealne rozwiązanie, ponieważ działanie bezpośrednio na zdarzeniu scroll
jest mocno obciążające dla przeglądarki (szczególnie gdy w powyższy sposób obsługujemy wiele elementów na stronie). Zamiast tego do takich testów o wiele lepiej skorzystać z omawianego obserwatora.
Jego użycie jest bliźniacze do powyższych, różnica jest tak naprawdę w przekazanych opcjach i ewentualnie danych, do których mamy dostęp w funkcji zwrotnej.
let observerOptions = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 0.5
}
let observer = new IntersectionObserver(elements => {
for (let el of elements) {
if (el.isIntersecting) {
el.target.style.background = "dodgerblue";
} else {
el.target.style.background = "";
}
}
}, observerOptions);
const div = document.querySelector("div");
observer.observe(div);
Tworząc nowy obiekt tego typu powinniśmy przekazać obiekt z dodatkowymi opcjami:
root | Element który staje się obszarem widoczności. Możemy podać null - wtedy obszarem staje się viewport |
---|---|
rootMargin | Margines na około roota, który pozwala zmniejszyć lub powiększyć jego obszar na którym będzie testowany element. Wartość podobna do marginesu z css to znaczy "10px 20px 30px 40px". Możemy ją podać w dowolnej jednostce. |
threshold | Określa moment w którym ma być reakcja na pojawienie się elementu w obszarze widoczności. Wartość 0 określa że reakcja będzie jak tylko element zacznie się wyłaniać spoza ekranu. Wartość 0.5 oznacza, że reakcja będzie gdy element pojawi się w połowie, 1 oznacza, że reakcja będzie gdy cały pokaże się w widocznym obszarze. Możemy też podać tablicę wartości które będą oznaczać kolejne poziomy widoczności np. [0, 0.25, 0.5] |
I tak samo jak w poprzednich obserwatorach mamy dostęp do dodatkowych metody:
observer.observe(el) | zaczyna obserwować dany element |
---|---|
observer.disconnect() | wyłącza obserwowanie |
observer.unobserve(el) | wyłącza obserwowanie danego elementu |
observer.takeRecords() | zwraca listę wszystkich zmian na DOM które zostały wykryte, ale jeszcze nie trafiły do funkcji zwrotnej, którą podłączyliśmy |
Przykład użycia:
Trening czyni mistrza
Jeżeli chcesz sobie potrenować zdobytą wiedzę, zadania znajdują się w repozytorium pod adresem: https://github.com/kartofelek007/zadania