Obiekt DOM

Przedostatnim elementem naszej podróży jest DOM (Document Object Model), czyli obiektowy model dokumentu HTML. Gdy przeglądarka ładuje stronę internetową, tworzy z reprezentacji HTML drzewo obiektów zwanych węzłami (nodes). Każdy element HTML staje się węzłem typu element, teksty stają się węzłami tekstowymi, itd. Obiekt document w JavaScript daje nam dostęp do tego drzewa DOM, dzięki czemu możemy dynamicznie odczytywać i modyfikować zawartość strony, strukturę elementów oraz reagować na zdarzenia (np. kliknięcia, ruch myszką, wpisywanie tekstu).

Innymi słowy, DOM jest interfejsem między JavaScriptem a HTML. Pozwala on traktować zawartość strony jako strukturę danych, którą możemy zmieniać „w locie” po załadowaniu strony, tworząc interaktywne aplikacje. Ta sekcja wprowadzi metody manipulacji DOM: jak wybierać elementy, zmieniać ich treść i styl, dodawać nowe elementy lub usuwać istniejące, a także jak obsługiwać zdarzenia użytkownika.

Wybieranie elementów DOM

Aby cokolwiek zrobić z DOM, najpierw musimy znaleźć (wybrać) elementy HTML, które nas interesują. JavaScript oferuje kilka metod do wyszukiwania elementów w dokumencie:

  • document.getElementById("id") – zwraca element o danym id. Identyfikator powinien być unikalny w ramach strony. Jeśli element o takim id nie istnieje, zwróci null.
    Przykład: const header = document.getElementById("main-header"); pobierze element o identyfikatorze "main-header".
  • document.querySelector("selector") – zwraca pierwszy element pasujący do danego selektora CSS. Selektorem może być nazwa tagu (np. "p"), klasa (".classname"), id ("#id") lub bardziej złożony selektor jak w CSS. Jeśli żaden element nie pasuje, zwraca null.
    Przykład: const pierwszyLink = document.querySelector("a"); zwróci pierwszy napotkany element <a> na stronie. const specjalny = document.querySelector(".active") zwróci pierwszy element z klasą „active”.
  • document.querySelectorAll("selector") – zwraca wszystkie elementy pasujące do selektora, w postaci kolekcji (NodeList). NodeList można iterować (np. pętlą for…of) lub przekształcić na prawdziwą tablicę. Jeśli żaden element nie pasuje, zwraca pustą kolekcję.
    Przykład: const wszystkieLinki = document.querySelectorAll("a"); pobierze listę wszystkich linków <a> w dokumencie.
  • Inne metody: Starsze, wyspecjalizowane metody to m.in. getElementsByClassName("nazwaKlasy"), getElementsByTagName("nazwaTagu"), które zwracają kolekcje elementów danej klasy lub tagu. Działają poprawnie, ale obecnie querySelectorAll jest często preferowane ze względu na elastyczność (można nim zastąpić tamte metody jednym uniwersalnym wywołaniem z odpowiednim selektorem CSS, np. document.querySelectorAll("div.note") zamiast getElementsByClassName("note")).

Przykład wykorzystujący różne metody selekcji:

 // HTML przykładowo zawiera <ul id="lista"><li class="item">A</li><li class="item">B</li></ul>
 const lista = document.getElementById("lista");          // <ul> z id "lista"
 const pierwszyItem = document.querySelector("#lista .item");  // pierwszy element z klasą "item" wewnątrz #lista
 const wszystkieItemy = document.querySelectorAll(".item");    // wszystkie elementy z klasą "item"
 console.log(lista.tagName);         // "UL"
 console.log(wszystkieItemy.length); // np. 2 (jeśli są dwa <li class="item">)

Kiedy mamy już referencję do elementu/elementów DOM, możemy przejść do manipulowania nimi.

Manipulacja treścią i stylami

Najprostszą formą manipulacji jest zmiana tekstowej treści lub HTML wewnątrz elementu, a także modyfikacja stylów CSS elementu.

  • Zmiana zawartości tekstowej: Właściwość textContent pozwala odczytać lub ustawić tekst wewnątrz elementu (bez interpretacji HTML). Podobnie działa innerText (który dodatkowo uwzględnia pewne aspekty widoczności CSS), ale zazwyczaj textContent jest preferowane do czystego tekstu.
    Jeśli chcemy wstawić kawałek HTML (np. tag <strong>), można użyć innerHTML, który traktuje przypisany string jako kod HTML do wyrenderowania wewnątrz elementu.
  • Zmiana stylów (CSS): Każdy element posiada właściwość style, która jest obiektem reprezentującym style inline (wbudowane w elemencie). Możemy ustawiać poszczególne właściwości CSS jako atrybuty tego obiektu. Np. element.style.color = "red"; ustawi kolor tekstu na czerwony, a element.style.backgroundColor = "yellow"; tło na żółte. Właściwości w stylu camelCase odpowiadają tym w CSS (np. background-color w CSS to backgroundColor w JavaScript).
    Innym podejściem jest manipulacja klasami CSS elementu, co jest często bardziej praktyczne – zamiast bezpośrednio zmieniać wiele stylów w skrypcie, dodajemy lub usuwamy klasy, które mają zdefiniowane style w arkuszu CSS. Do tego służy właściwość classList, która udostępnia metody takie jak add, remove, toggle do zarządzania listą klas elementu.

Przykłady:

 const naglowek = document.getElementById("main-header");
 naglowek.textContent = "Witaj w JavaScript!";  // zmiana tekstu nagłówka
 naglowek.style.color = "blue";                // zmiana koloru tekstu naglowek.style.fontSize = "24px";             // zmiana rozmiaru czcionki (np. na 24px)

 naglowek.classList.add("highlight");          // dodanie klasy CSS do elementu
 // (zakładamy, że w CSS mamy np. .highlight { background-color: yellow; } )

Powyższy kod zmienia tekst nagłówka (np. <h1 id="main-header">) na „Witaj w JavaScript!”, ustawia jego tekst na niebieski, rozmiar czcionki na 24px, a następnie dodaje mu klasę "highlight", która może np. podświetlać tło. Manipulacja textContent usuwa ewentualną poprzednią zawartość i wstawia nowy tekst. Gdybyśmy zamiast tego użyli innerHTML = "<span>Witaj</span>", to nagłówek zawierałby nowy element <span> wewnątrz. Trzeba być ostrożnym z innerHTML – jeśli wstawiamy w ten sposób dane pochodzące od użytkownika, może to prowadzić do problemów z bezpieczeństwem (wstrzyknięcie niechcianego kodu HTML/JS).

  • Zmiana atrybutów: Poza tekstem i stylem, elementy HTML mają różne atrybuty (np. src dla <img>, href dla <a>, value dla <input> itd.). Możemy je zmieniać poprzez bezpośrednie właściwości (np. image.src = "nowy.png") lub metody setAttribute/getAttribute. Np. link.setAttribute("target", "_blank") doda atrybut target do linku.

Tworzenie i usuwanie elementów w DOM

JavaScript może nie tylko zmieniać istniejące elementy, ale też tworzyć nowe elementy od zera i dodawać je do DOM, lub usuwać elementy.

  • Tworzenie elementu: Służy do tego metoda document.createElement("nazwaTagu"). Tworzy ona nowy węzeł elementu w oderwaniu od dokumentu (na razie nie jest widoczny na stronie, dopiero po dołączeniu). Po utworzeniu możemy ustawić jego zawartość lub atrybuty:
 const newParagraph = document.createElement("p"); // utworzenie elementu <p>    newParagraph.textContent = "To jest nowy akapit."; 
 newParagraph.style.fontWeight = "bold";

Powyżej utworzyliśmy nowy paragraf z tekstem i ustawiliśmy pogrubienie tekstu stylem. Nadal nie jest on dodany do strony.

  • Dodawanie do DOM: Aby element pojawił się na stronie, musimy go wstawić do drzewa DOM, np. jako dziecko jakiegoś innego elementu. Najczęściej używamy parentElement.appendChild(nowyElement) lub nowszej metody parentElement.append(nowyElement):
 const sekcja = document.getElementById("section1"); sekcja.appendChild(newParagraph);

Załóżmy, że w HTML mamy element o id section1 (np. <div id="section1"></div>). Powyższy kod dołącza nasz nowy paragraf jako dziecko tego elementu (na koniec zawartości sekcji). Metoda appendChild umieszcza element zawsze jako ostatni element wewnątrz rodzica. Jeśli chcemy wstawić w konkretne miejsce, można użyć parentElement.insertBefore(newElem, referencyjnyElem) lub metod typu after, before (np. element.before(newElem) wstawia nowyElem przed danym elementem w DOM).

  • Usuwanie elementu: Możemy usunąć element z DOM na kilka sposobów. Mając referencję do elementu, możemy wywołać element.remove(), co spowoduje jego usunięcie z drzewa DOM. Ta metoda jest prostsza, wspierana w większości nowoczesnych przeglądarek. Alternatywnie, możemy wywołać parent.removeChild(element), co zadziała nawet w starszych środowiskach: javascriptKopiujEdytujnewParagraph.remove(); // usuwa newParagraph z DOM (jego rodzicem był sekcja) Po tej operacji newParagraph istnieje dalej jako obiekt w pamięci, ale nie jest już częścią dokumentu widocznego dla użytkownika. Jeśli nie będziemy już go używać, po pewnym czasie zostanie oczyszczony z pamięci przez GC.

Podsumowując, dzięki createElement, appendChild/append i removeChild/remove możemy dowolnie manipulować strukturą strony: dynamicznie dodawać nowe sekcje, listy, elementy interfejsu lub usuwać te, które są niepotrzebne.

Obsługa zdarzeń

Strona internetowa staje się interaktywna głównie dzięki zdarzeniom (events) i ich obsłudze w JavaScript. Zdarzenie to np. kliknięcie w przycisk, najechanie myszą na element, naciśnięcie klawisza, załadowanie strony itp. W JavaScript możemy podpiąć funkcje (zwane obsługami zdarzeń lub handlerami) pod konkretne zdarzenia na konkretnych elementach. Gdy zdarzenie wystąpi, przeglądarka wywoła naszą funkcję, która zareaguje w określony sposób.

Najlepszym sposobem obsługi zdarzeń jest użycie metody addEventListener. Składnia: element.addEventListener(typZdarzenia, funkcjaDoWywołania).

Przykład: obsługa kliknięcia (event "click") na przycisku:

 const button = document.querySelector("button#clickMe");
 button.addEventListener("click", () => {
      alert("Kliknięto przycisk!");
 });

W powyższym kodzie zakładamy, że w HTML istnieje <button id="clickMe">Kliknij mnie</button>. Skrypt pobiera ten przycisk i rejestruje nasłuch na zdarzenie click. Gdy użytkownik kliknie ten przycisk, wykona się funkcja strzałkowa, która wywoła alert z komunikatem. Zamiast funkcji strzałkowej moglibyśmy przekazać nazwę zdefiniowanej gdzieś indziej funkcji, np. button.addEventListener("click", pokazKomunikat), jeśli funkcja pokazKomunikat była zdefiniowana.

Obsługa zdarzeń może również modyfikować DOM dynamicznie. Na przykład, możemy sprawić, że kliknięcie przycisku doda nowy element do strony lub zmieni styl istniejącego elementu:

 const dodajBtn = document.getElementById("dodajAkapit");  // np. <button id="dodajAkapit">Dodaj akapit</button>
 dodajBtn.addEventListener("click", () => {
      const p = document.createElement("p");
      p.textContent = "Nowy akapit dodany przez JavaScript.";
      document.body.appendChild(p);
 });

W tym przykładzie po kliknięciu przycisku o id "dodajAkapit" tworzymy nowy paragraf i dodajemy go na sam dół strony (document.body). Za każdym kliknięciem będzie dodany kolejny nowy akapit. Widać tu połączenie kilku poznanych wcześniej rzeczy: selekcja elementu, obsługa zdarzenia, tworzenie elementu i modyfikacja DOM.

Kilka wskazówek dot. obsługi zdarzeń:

  • Istnieje wiele typów zdarzeń: click, dblclick, mouseover, mouseout, mousedown, mouseup, keydown, keyup, submit, change, input i wiele innych. Każde zdarzenie przekazuje do funkcji obsługującej obiekt zdarzenia (event object) zawierający szczegółowe informacje (np. który klawisz został naciśnięty, jaka pozycja kursora, itp.). W naszym arrow function nie użyliśmy parametru, ale moglibyśmy go dodać, np. (event) => { ... } i korzystać z event.
  • Można podpiąć wiele niezależnych „nasłuchiwaczy” do tego samego zdarzenia na tym samym elemencie za pomocą addEventListener (każdy wykona się w kolejności dodania). To przewaga nad starszą metodą ustawiania element.onclick = ..., gdzie przypisanie nowej funkcji nadpisuje poprzednią.
  • Aby usunąć obsługę zdarzenia, istnieje odpowiednia para removeEventListener, ale wymaga to referencji do tej samej funkcji, którą dodano (przydatne np. przy czyszczeniu nasłuchów, gdy element jest usuwany).

Dzięki zdarzeniom i manipulacji DOM możemy tworzyć bogate interakcje: walidować formularze na bieżąco, tworzyć galerie obrazów, reagować na ruch myszki, budować gry przeglądarkowe, i wiele więcej.