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 (i podobnych), 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. Nas głównie będą interesować nody, które są elementami - np. buttony, divy itp.

Relacje między nodami

Rozpiszmy przykładowy html, który będziemy analizować:


<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 text = document.querySelector('#text');

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

text.firstChild //pierwszy node - w naszym przypadku to tekst "Mała "
text.lastChild //ostatni node - "" - html jest sformatowany, wiec ostatnim nodem jest znak nowej linii

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

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

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

text.nextSibling //następny węzeł
text.previousSibling //poprzedni węzeł
text.nextElementSibling //następny brat-element
text.previousElementSibling //poprzedni brat-element

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

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

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

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

Mała Ala miała kota

W większości przypadków będzie nas interesować odwoływanie się do elementów html, dlatego głównymi właściwościami, które nas interesują są:

element.parentElement rodzic elementu lub null
element.nextElementSibling następny element (brat) lub null
element.previousElementSibling poprzedni element (brat) lub null
element.children dzieci elementu lub pusta tablica
element.firstElementChild lub element.children[0] pierwsze dziecko elementu lub null
element.lastElementChild lub element.children[element.children.length-1] ostatnie dziecko elementu lub null

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); //ilość dzieci 3
children[0].remove(); //usuwam 1 dziecko
console.log(children.length); //ilość dzieci 2

Temat ten podejmowaliśmy już tutaj.

closest()

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


<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

W ramach ćwiczeń zróbmy jakiś przykład.

Tutaj możesz ściągnąć kod (prawy i zapisz jako), na którym zaraz będziemy pracować. W poniższych listingach nie będę się skupiał na CSS. Odpowiednie stylowanie możesz zobaczyć w powyższym pliku html.

Wyobraź sobie, że mamy listę modułów. Każdy taki moduł to belka tytułowa z tekstem (Widoczne/Niewidoczne), tekst z jakąś treścią i przycisk pokazujący tą treść.


<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" data-show="0">...</div>
    <div class="module" data-show="0">...</div>
    <div class="module" data-show="0">...</div>
</div>

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 w stanie wyłapać zależności między elementami.
Klikamy na przycisk .module-btn i który tekst mielibyśmy pokazać? querySelectorAll zwróci nam wszystkie .module-cnt, querySelector tylko pierwszy. To za mało.
W takich przypadkach właśnie musimy skorzystać z tego co poznaliśmy powyżej - zamiast pobierać elementy globalnie, po kliknięciu w przycisk pobierajmy rodziców i dzieci klikniętego elementu.


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

        //żeby zrozumieć poniższy spacer, patrz na strukturę html
        const module = button.parentElement.parentElement;
        const moduleBar = button.parentElement;
        const moduleBarText = button.previousElementSibling;
        const moduleContent = moduleBar.nextElementSibling;
    });
}

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.contains('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

        //żeby zrozumieć poniższy 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;
        }
    });
}
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?