Relacje między elementami

Wiemy już, że elementy na stronie tworzą hierarchiczne drzewo.
Aby operować na takich obiektach, musimy dobrze opanować sztukę "spacerowania" po nich.
Możemy je wybierać a pomocą querySelector czy querySelectorAll, ale czasami to za mało.

Każdy element na stronie tworzy tak zwany node czyli pojedynczy węzeł drzewa. Takimi nodami są nie tylko elementy, ale także tekst w nich zawarty

Relacje między nodami

Rozpiszmy przykładowy akapit, który posiada w sobie pogrubiony tekst:


<div class="text-cnt">
    <p id="text">
        Mała
        <strong style="color:red">Ala</strong>
        miała
        <span style="color:blue">kota</span>
    </p>
</div>

Nasz html składa się ze składowych - nodów.
Część z nich jest elementami html (div, p, strong i span).
Wszystkie natomiast są nodami - div, p, tekst Mała, strong, tekst Ala, tekst mała, span, tekst kota.

Znając hierarchiczne położenie obiektów, możemy w łatwy sposób się po nich przemieszczać:


const p = document.querySelector('#text');

p.parentElement //wskazuje na nadrzędny nod będący elementem - div.text-cnt
p.parentNode //wskazuje na nadrzędny nod - div.text-cnt

p.firstChild //"Mała "
p.lastChild //"" - html jest sformatowany, wiec ostatnim nodem jest znak nowej linii

p.firstElementChild //pierwszy element html - <strong style="color:red">Ala</strong>
p.lastElementChild //ostatni element html - <span style="color:blue">kota</span>

p.children; //[strong, span] - kolekcja elementów html
p.children[0] //wskazuje na 1 element czyli <strong style="color:red">Ala</strong>

p.childNodes //[text, strong, text] - kolekcja wszystkich dzieci - nodow
p.childNodes[0] //"Mała"

p.firstElementChild.nextElementSibling //kolejny brat-element pierwszego elementu - <span style="color:blue">kota</span>
p.firstElementChild.nextSibling //kolejny brat-node pierwszego elementu - "miała"

p.firstElementChild.previousElementSibling //poprzedni brat-element pierwszego elementu - null, bo przed pierwszym stron nie ma elementow
p.firstElementChild.previousSibling //poprzedni brat-node pierwszego elementu - "Mała"

//powyższe możemy łączyć
p.children[0].firstChild //pierwszy element i w nim pierwszy nod : "Ala"
p.children[0].firstElementChild //null - w pierwszym strong nie mamy juz elementów

p.firstChild.firstElementChild //null - nie ma elementu w pierwszym tekście
p.firstElementChild.firstElementChild //null - nie ma elementy w strong
p.firstElementChild.firstChild //"Ala"

Mała Ala miała kota

Bardzo ważną rzeczą jest to, że powyższe właściwości, które zwracają kolekcje elementów, zwracają je żywe, czyli takie, które automatycznie reagujące na zmiany w strukturze html:


const divs = document.querySelector('div');
const children = divs.children; //pobrałem kolekcję wszystkich dzieci czyli w css div > *

console.log(children.length); //3
children[0].remove(); //usuwam 1 dziecko
console.log(children.length); //2

Temat ten podejmowaliśmy już tutaj.

closest()

Metoda el.closest(selektor) odnajduje najbliższy elementowi element który pasuje do selektora:

code class="language-html">
<div class="module">
    <div class="module-content">
        <div>
            <div class="module-text">
                Lorem ipsum dolor sit amet...
            </div>
            <button class="button">Kliknij</button>
        </div>
    </div>
</div>

document.querySelector('.button').addEventListener('click', function() {
    const module = this.closest('.module');
})

Metoda ta nie jest wspierana przez IE (w Edge już działa), dlatego jeżeli chcesz wspierać starsze wersje przeglądarek Microsoftu, musisz użyć tak zwanego polyfilla (czyli zrobić nakładkę poprawiającą działanie przeglądarki):


//polyfill dla przeglądarek nie obsługujących closest
if (!Element.prototype.matches) {
    Element.prototype.matches = Element.prototype.msMatchesSelector;
}
if (!Element.prototype.closest) {
    Element.prototype.closest = function(selector) {
        let el = this;
        while (el) {
            if (el.matches(selector)) {
                return el;
            }
            el = el.parentElement;
        }
    }
};


document.querySelector('.button').addEventListener('click', function() {
    const module = this.closest('.module');
})

Zastosowanie w praktyce

No dobrze - a do czego to może się przydać?
Zróbmy jakiś praktyczny przykład. Wyobraź sobie, że mamy listę modułów:


<div class="module-list">
    <div class="module" data-show="1">
        <div class="module-bar">
            Przykładowy tytuł modułu
            <span class="module-bar-text">(Widoczne)</span>
            <button class="btn module-btn" type="button">Toggle</button>
        </div>
        <div class="module-cnt">
            Lorem ipsum dolor sit amet, consectetur adipisicing elit.
            Debitis enim nam dicta vitae excepturi, dolore sapiente ipsa
            repudiandae voluptatem officia impedit unde ullam, animi.
            Iusto tenetur impedit provident ea sapiente?
        </div>
    </div>

    <div class="module">...</div>
    <div class="module">...</div>
    <div class="module">...</div>
</div>
Przykładowy tytuł modułu (Widoczne)
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Debitis enim nam dicta vitae excepturi, dolore sapiente ipsa repudiandae voluptatem officia impedit unde ullam, animi. Iusto tenetur impedit provident ea sapiente?
Przykładowy tytuł modułu (Widoczne)
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Debitis enim nam dicta vitae excepturi, dolore sapiente ipsa repudiandae voluptatem officia impedit unde ullam, animi. Iusto tenetur impedit provident ea sapiente?

Po kliknięciu przycisków .module-btn chcemy schować tekst w danym module, a dodatkowo zmienić tekst w .module-bar-text.

Gdybyśmy tutaj polegali tylko na querySelectorAll, nie bylibyśmy wyłapać zależności między elementami.
Dlatego skorzystajmy z tego co poznaliśmy powyżej - zamiast pobierać elementy globalnie, wyłapujmy rodziców i dzieci klikniętego elementu.
Dodatkowo dla sprawdzenia czy dany tekst jest widoczny czy nie, skorzystamy z customowego atrybutu data-show, który ustawiliśmy dla każdego .module.
To jedno z rozwiązań. Spokojnie można by po prostu sprawdzać, czy .module-content ma klasę .show za pomocą classList.contain('show'):


const list = document.querySelector('.module-list');
const btn = list.querySelectorAll(':scope .module-btn');

for (let i=0; i<btn.length; i++) {
    btn[i].addEventListener('click', function() {
        const button = this; //kliknięty guzik

        //rzeby zrozumiec ponizszy spacer, patrz na strukturę html
        const module = button.parentElement.parentElement;
        const moduleBar = button.parentElement;
        const moduleBarText = button.previousElementSibling;
        const moduleContent = moduleBar.nextElementSibling;

        if (parseInt(module.dataset.show, 10) === 1) {
            moduleBarText.innerText = '(Niewidoczne)';
            moduleContent.classList.remove('show');
            module.dataset.show = 0;
        } else {
            moduleBarText.innerText = '(Widoczne)';
            moduleContent.classList.add('show');
            module.dataset.show = 1;
        }
    });
}