Poznaj querySelector i przestań używać getElementsBy…

btw – kod może Cię zaskoczyć!

Spis treści

  1. Wprowadzenie
  2. QuerySelector/querySelectorAll
    1. Kilka słów o querySelector
    2. Słówko o querySelectorAll
    3. Dwa słowa o pewnej nieścisłości
    4. Kod przykładu na Github
    5. Klikalny przykład na Github Pages
  3. Dlaczego querySelector jest lepszy od innych metod?
  4. Podsumowanie

Wprowadzenie

Lat temu bardzo dużo główną przewagą jQuery nad czystym JS było to, że ujednolicała i upraszczała zachowanie pomiędzy przeglądarkami. JS jednak zaczął nadrabiać stracone lata, modernizując swoje API, większy nacisk również został położony na kompatybilność między przeglądarkami.

Jedną z rzeczy które najbardziej lubię w jQuery jest możliwość używania selektorów do pobierania elementu, np $('.form .element:not(.inactive)').

I choć JavaScript od wielu lat daje możliwość używania selektorów przy pobieraniu elementów DOM za pomocą querySelector/querySelectorAll, bardzo często widzę użycia innych metod pobierania elementów, jak getElementById czy getElementsByTagName.

W tym wpisie będę chciał przekonać Cię, że querySelector/querySelectorAll to zdecydowanie lepszy wybór 🙂

QuerySelector/querySelectorAll

Kilka słów o querySelector

Metoda ta zwraca pierwszy element pasujący do selektora.

Zwrócony element jest typu Element, interfejs ten udostępnia m.in metody:

  • closest, która zwróci pierwszego rodzica pasującego do selektora
  • animate, pozwalającą utworzyć oraz uruchomić animację na wybranym elemencie
  • requestFullscreen, która pokaże wybrany element na pełnym ekranie

Istotnym elementem jest to, że metody querySelector i querySelectorAll można uruchamiać również na obiekcie typu Element – co spowoduje wyszukanie po danym selektorze wewnątrz wybranego elementu. Wiąże się z tym pewna nieścisłość – ale o tym dalej.

Słówko o querySelectorAll

Ponieważ querySelector zwraca tylko pierwszy pasujący element, potrzebujemy metody która zwróci nam wszystkie pasujące elementy. Tą metodą jest querySelectorAll.

Zwraca ona obiekt NodeList – możesz iterować po tym obiekcie jak po tablicy metodą forEach bądź składnią for..of, aczkolwiek jeśli chcesz mieć pełną paletę operacji na tablicach możesz przetransformować obiekt do prawdziwej tablicy:

const array = [...document.querySelector('.items')];

Dwa słowa o pewnej nieścisłości

querySelector pozwala stosować selektory a także przeszukiwać DOM relatywnie w stosunku do elementu, jest jednak jedno “ale”.

Jeżeli przeszukujemy DOM relatywnie spodziewalibyśmy się, że znalezione elementy będą musiały spełniać warunek selektora wewnątrz elementu. Brzmi to może dziwnie 🙂 Ale spójrzmy na kod:

<div class="parent">
  <div id="element">
    <div class="child"></div>
  </div>
</div>

Mając taki kod JS:

const element = document.querySelector('#element');
const child = element.querySelector('.parent .child');

Spodziewałbym się zwrócenia wartości null, ponieważ w divie o id element nie ma elementu z klasą child wewnątrz elementu z klasą parent.

Co zaskakujące jednak, zostanie zwrócony div z klasą child – ponieważ domyślnie cała hierarchia DOM jest brana pod uwagę.

Może to być uznane za nienaturalne – jak więc uzyskać naturalne zachowanie? Poprzez dodanie do selektora pseudoklasy :scope.

Spójrzmy na taki kod HTML:

<div class="parent">
  Parent
  <div class="child child-1">
    Child outer
    <div class="child child-2">
      Child inner
      <div class="child child-3">
        Child
        <div class="parent child-1">
          Child but also a parent
          <div class="child child-2">
            Child
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Powtarza się tam selektor .parent .child, który będziemy chcieli sprawdzić.

Stworzymy selektor który będzie miał formę albo

':scope .parent .child'

I ten selektor będzie sprawdzany tylko w hierarchii wewnątrz elementu, oraz selektor z zachowaniem domyślnym:

'.parent .child'

Elementem w stosunku do którego będziemy przeszukiwać dom będzie element z tekstem Child outer. Kliknięcie na przycisk będzie wywoływać funkcję, która do pasujących elementów dodawać będzie czarne tło:

const child1 = document.querySelector('.child-1');
    
document.querySelector('#select').addEventListener('click', () => {
  const scoped = document.querySelector('#scoped').checked;
  const selector = `${scoped ? ':scope ' : ''}.parent .child`;
  const elements = [...child1.querySelectorAll(selector)];
  elements.forEach(element => element.classList.add('black'));
});

Jaki będzie efekt dla wywołania funkcji bez zaznaczonego checkboxa scoped? Zaznaczonych zostanie wiele elementów, ponieważ pod uwagę wzięta będzie klasa parent będąca poza elementem w stosunku do którego szukamy:

Natomiast gdy zaznaczymy checkbox zaznaczy się tylko jeden element:

Ponieważ dodana pseudoklasa wymusza branie pod uwagę tylko hierarchię wewnątrz elementu.

Kod przykładu na Github

Pełny kod przykładu można znaleźć pod adresem:

https://github.com/radek-anuszewski/queryselector-scope-demo

Klikalny przykład na Github Pages

Możesz przeklikać przykład pod adresem:

https://radek-anuszewski.github.io/queryselector-scope-demo/index.html

Dlaczego querySelector jest lepszy od innych metod?

Moim zdaniem oczywiście 🙂 A więc czemu tak uważam?

Po pierwsze, mamy bardziej czytelne i jednorodne API. Istnieje jedna metoda zamiast kilku:

  1. getElementById
  2. getElementsByClassName
  3. getElementsByTagName

Pierwsza z powyższych zwraca pojedynczy element – co jest oczywiście logiczne biorąc pod uwagę, że id powinno być unikalne, pozostałe 2 zwracają listę elementów.
Uważam że zachowanie querySelector i querySelectorAll jest czytelniejsze – chcesz pierwszy pasujący do selektora element? Użyj tego pierwszego. Chcesz listę elementów? Użyj drugiego.

Używanie selektorów pozwala nam reużywać wiedzy którą nabyliśmy przy nauce / pracy w CSS. I odwrotnie – jeśli poznajemy selektory pod kątem metody querySelector wiedza ta będzie mieć również zastosowanie przy stylowaniu aplikacji.

No i najważniejsze na koniec, coś co jest oczywiste – selektory 🙂 Dzięki temu, bez konieczności użycia jQuery bądź innej biblioteki czy konieczności odfiltrowywania elementów w kodzie, możemy bardzo szczegółowo przeszukiwać DOM.

Podsumowanie

Oczywiście dużą rolę w wyborze sposobu odpytywania DOM będą odgrywać osobiste preferencje. Niektórzy uznają dedykowane metody za lepsze – jako bardziej ekspresyjne niż querySelector, którego nazwa na pierwszy rzut oka niewiele mówi.
Osobiście uważam, że querySelector ma znacznie większe możliwości niż inne metody a także jest bardziej minimalistyczna. Zamiast 3 metod mamy 2, które dodatkowo używają zasad znanych z CSS – dlatego myślę, że powinny być preferowanym sposobem dostępu po DOM.