Przejdź do treści

Temat 11 · 2 × 45 minINF.03

Model DOM: znajdowanie elementów i zmiana treści

Do tej pory twoje skrypty rozmawiały z tobą przez konsolę i okienka. Od tej lekcji zaczynają zmieniać stronę: podmieniać tekst akapitu, pogrubiać nagłówki, wpisywać wynik obliczenia pod przyciskiem. Pośrednikiem jest DOM (Document Object Model) – obiektowy model dokumentu, czyli HTML strony widziany przez JavaScript jako drzewo obiektów. Każde zadanie praktyczne INF.03 z JavaScriptem zaczyna się od znalezienia elementu w tym drzewie, a w testach pisemnych DOM to najczęstszy temat „kliencki”.

Drzewo DOM

Gdy przeglądarka wczytuje HTML, buduje z niego drzewo: każdy znacznik staje się węzłem elementu (element node), a tekst wewnątrz – węzłem tekstowym (text node). Korzeniem jest obiekt document, pod nim <html>, dalej <head> i <body> i tak w głąb. Element wewnątrz innego to jego dziecko (child), element nad nim – rodzic (parent), elementy obok – rodzeństwo (siblings).

Drzewo: document, pod nim html, pod nim head z title o treści Sklep oraz body z h1 Oferta, akapitem o id opis, listą ul o klasie lista z trzema li i przyciskiem o id kup.
Rys. 1. Drzewo DOM prostej strony. Znaczniki to węzły elementów, napisy w cudzysłowach to węzły tekstowe.

Odpowiada mu taki HTML:

sklep.html
<!DOCTYPE html>
<html lang="pl">
<head>
  <meta charset="UTF-8">
  <title>Sklep</title>
</head>
<body>
  <h1>Oferta</h1>
  <p id="opis">Dziś promocja</p>
  <ul class="lista">
    <li>Chleb</li>
    <li>Masło</li>
    <li>Mleko</li>
  </ul>
  <button id="kup">Kup</button>
</body>
</html>

JavaScript nie pracuje na pliku .html – pracuje na tym drzewie. Zmiana węzła natychmiast zmienia to, co widać na ekranie, a plik na dysku zostaje nietknięty.

Obiekt document ma gotowe skróty do najważniejszych węzłów:

Obiekt document
console.log(document.title);                 // tytuł z <title> (tu pusty – plac zabaw nie ma <head>)
console.log(document.body.children.length);  // liczba elementów bezpośrednio w <body>
console.log(document.documentElement.tagName); // HTML

document.title = 'Sklep – promocje';         // zmiana tytułu karty
document.getElementById('info').textContent =
  'Na stronie są ' + document.body.children.length + ' elementy.';
Podgląd

Kiedy skrypt widzi elementy

Przeglądarka czyta HTML od góry. Skrypt umieszczony w <head> uruchamia się, zanim powstanie <body> – getElementById zwraca wtedy null, a próba odczytu .value kończy się błędem TypeError: Cannot read properties of null z poprzedniej lekcji. Masz trzy bezpieczne opcje:

gdzie-skrypt.html
<!-- 1. skrypt na końcu <body> – elementy już istnieją (najprostsze, wystarczy na egzaminie) -->
<body>
  <p id="opis">…</p>
  <script src="skrypt.js"></script>
</body>

<!-- 2. atrybut defer – skrypt z <head> czeka na zbudowanie drzewa -->
<head>
  <script src="skrypt.js" defer></script>
</head>

<!-- 3. kod w funkcji uruchamianej dopiero kliknięciem – w chwili kliknięcia strona dawno jest gotowa -->
<button onclick="pokaz()">Pokaż</button>

W placu zabaw kod JS jest zawsze wstawiany na końcu <body>, więc elementy są gotowe.

getElementById – jeden element po identyfikatorze

Najważniejsza metoda egzaminu: document.getElementById('nazwa') zwraca element o atrybucie id="nazwa" albo null, gdy takiego nie ma. id musi być unikalne na stronie – dlatego metoda zwraca pojedynczy element, nie listę. Działa tylko na document (nie na innych elementach).

getElementById
const tytul = document.getElementById('tytul');   // element zapisany w zmiennej
const opis = document.getElementById('opis');

console.log(tytul.tagName);     // H2
console.log(opis.textContent);  // Jeszcze nic nie wiadomo.

document.getElementById('pokaz').addEventListener('click', () => {
  tytul.textContent = 'Promocja -20%';
  opis.textContent = 'Wszystkie zeszyty 20% taniej do piątku.';
});

console.log(document.getElementById('cennik'));  // null – nie ma takiego id
Podgląd

Element zapisuj w zmiennej o znaczącej nazwie (const opis = …) zamiast powtarzać document.getElementById('opis') w pięciu linijkach – kod jest krótszy, a egzaminator widzi kryterium „znaczące nazewnictwo”.

Zmiana treści: innerHTML, textContent, innerText

Sprawdź się

Co zobaczy użytkownik w akapicie po wykonaniu kodu?

document.getElementById('p1').innerHTML = 'Cena: <b>20 zł</b>';
document.getElementById('p2').textContent = 'Cena: <b>20 zł</b>';

Trzy właściwości czytają i ustawiają zawartość elementu:

WłaściwośćPrzy zapisiePrzy odczycie
innerHTMLnapis jest parsowany jako HTML – znaczniki działająHTML wnętrza ze znacznikami
textContentnapis wstawiany dosłownie – znaczniki widać jako tekstcały tekst, także ukryty CSS-em
innerTextjak textContenttylko tekst widoczny na ekranie, z uwzględnieniem CSS (wolniejsze)
innerHTML vs textContent
document.getElementById('zmien').addEventListener('click', () => {
  document.getElementById('p1').innerHTML = 'Cena: <b>20 zł</b>';
  document.getElementById('p2').textContent = 'Cena: <b>20 zł</b>';
});

const p3 = document.getElementById('p3');
console.log(p3.innerHTML);    // Pierwszy <b>pogrubiony</b> <span style="display:none">ukryty</span> akapit
console.log(p3.textContent);  // Pierwszy pogrubiony ukryty akapit
console.log(p3.innerText);    // Pierwszy pogrubiony akapit  (ukryty pominięty)
Podgląd

Którą wybrać?

  • Wynik obliczenia, komunikat, imię użytkownika → textContent. Jest szybsze i bezpieczne: jeśli ktoś wpisze w pole <img src=x onerror=…>, zobaczy tekst, a nie uruchomi kodu.
  • Potrzebujesz znaczników (łamanie linii <br>, pogrubienie, lista zbudowana w pętli) → innerHTML.

W arkuszach CKE wszędzie występuje innerHTML – jest w tabeli pomocniczej i w pytaniach testowych. Możesz go używać do wyniku; textContent nie da ci ani punktu więcej, ani mniej. Dopisywanie na końcu działa dla obu: wynik.innerHTML += '<br>kolejna linia'.

Kolekcje: getElementsByClassName i getElementsByTagName

id jest jedno, ale klasę ma wiele elementów. document.getElementsByClassName('nazwa') zwraca kolekcję (HTMLCollection) wszystkich elementów z tą klasą, a getElementsByTagName('p') – wszystkie elementy danego znacznika. Kolekcja działa jak tablica: ma .length, elementy wybierasz przez [i]. Nie jest jednak prawdziwą tablicą – nie ma metod push, forEach ani map.

Sprawdź się

Co zrobi ten kod?

var elementy = document.getElementsByClassName('wazne');
for (var i = 0; i < elementy.length; i++) {
elementy[i].style.fontWeight = 'bold';
}
Kolekcja elementów klasy
const wazne = document.getElementsByClassName('wazne');
console.log(wazne.length);           // 3 – dwa akapity i jeden li
console.log(wazne[0].textContent);   // Termin oddania: piątek.

document.getElementById('pogrub').addEventListener('click', () => {
  for (let i = 0; i < wazne.length; i++) {
    wazne[i].style.fontWeight = 'bold';     // pojedynczy element ma style
    wazne[i].style.color = 'darkred';
  }
});

const akapity = document.getElementsByTagName('p');
console.log('Akapitów: ' + akapity.length);  // 3
Podgląd

Kolekcja z getElementsBy… jest żywa (live): jeśli później dodasz na stronę nowy element z klasą wazne, kolekcja sama go „zobaczy” i length wzrośnie. To bywa zaskakujące w pętlach, które dodają elementy – wrócisz do tego w lekcji Tworzenie i usuwanie elementów.

querySelector i querySelectorAll – selektory CSS

Nowocześniejsza para metod przyjmuje dowolny selektor CSS, taki jak w arkuszu stylów:

  • document.querySelector(selektor) – pierwszy pasujący element albo null;
  • document.querySelectorAll(selektor) – wszystkie pasujące jako NodeList (statyczna lista z .length, [i] i metodą forEach).
querySelector i querySelectorAll
const opis = document.querySelector('#opis');            // po id – z krzyżykiem
const pierwszyLi = document.querySelector('.dodatki li'); // pierwszy li w .dodatki
const ostre = document.querySelector('.ostre');          // po klasie – z kropką
console.log(pierwszyLi.textContent, '|', ostre.textContent);  // Ser | Jalapeño

const wszystkieLi = document.querySelectorAll('.dodatki li');  // NodeList
console.log(wszystkieLi.length);                              // 3
wszystkieLi.forEach((li) => console.log('-', li.textContent)); // NodeList ma forEach

const zaznaczone = document.querySelector('input[name="rozmiar"]:checked');
document.getElementById('wynik').textContent =
  'Rozmiar: ' + zaznaczone.value + ', dodatków: ' + wszystkieLi.length;
Podgląd

Selektory, które przydają się najczęściej:

SelektorZnajduje
'#wynik'element o id="wynik"
'.wazne'elementy z klasą wazne
'p', 'li', 'img'elementy danego znacznika
'.lista li'li wewnątrz elementu z klasą lista
'input[name="rozmiar"]'pola o atrybucie name="rozmiar"
'input[name="rozmiar"]:checked'zaznaczone radio z tej grupy
'section > h2'h2 będące bezpośrednim dzieckiem section

Którą metodę wybrać? getElementById jest najkrótsze dla pojedynczego id i pojawia się w każdej tabeli pomocniczej CKE. querySelectorAll jest najwygodniejsze dla grup (ma forEach i przyjmuje złożone selektory). Obie rodziny możesz mieszać w jednym skrypcie.

Co właściwie zmieniasz: innerHTML, value, src, style

Pytania testowe sprawdzają, czy wiesz, która właściwość odpowiada za co. Każda zmienia inną rzecz w tym samym elemencie:

WłaściwośćCzego dotyczyPrzykładLekcja
innerHTML, textContenttreść między znacznikamip.innerHTML = 'Koszt: 20 zł'ta
valuewartość pola formularza (input, select, textarea)pole.value → '15'Kontrolki
srcadres obrazu / skryptuimg.src = 'obraz2.jpg'Style, klasy i atrybuty
style.nazwaWlasciwoscijedna właściwość CSSp.style.color = 'red'Style, klasy i atrybuty
className, classListklasy CSSp.classList.add('wazne')Style, klasy i atrybuty
id, tagName, href, alt, titlepojedyncze atrybutylink.hrefStyle, klasy i atrybuty
Sprawdź się

Kod ma po kliknięciu podmienić zdjęcie w elemencie img o id foto na plik kol2.jpg. Które polecenie to zrobi?

Zapis document.getElementById(id).style.<właściwość> = wartość to najczęstszy szablon w testach pisemnych; w pytaniu z czerwca 2022 (EE.09) należało wskazać właśnie .style. jako brakujący fragment, a dystraktory pomijały style albo wstawiały innerHTML.

Cztery właściwości, jeden skrypt
document.getElementById('zmien').addEventListener('click', () => {
  const numer = document.getElementById('numer').value;                 // value – pole
  document.getElementById('foto').src =
    'https://placehold.co/120x60/ddd/333?text=' + numer;                // src – obraz
  document.getElementById('opis').textContent = 'Obraz numer ' + numer; // treść
  document.getElementById('opis').style.color = 'steelblue';           // CSS
});
Podgląd

Poruszanie się po drzewie

Czasem masz element i potrzebujesz jego sąsiada albo rodzica – np. przycisk „Wykonane” ma przekreślić swój li. Służą do tego właściwości nawigacyjne:

Rodzic, dzieci, rodzeństwo
const srodek = document.getElementById('srodek');
const lista = document.getElementById('lista');

console.log(srodek.parentElement.id);                    // lista
console.log(srodek.previousElementSibling.textContent);  // Kupić chleb
console.log(srodek.nextElementSibling.textContent);      // Zadzwonić do babci
console.log(lista.children.length);                      // 3
console.log(lista.firstElementChild.textContent);        // Kupić chleb
console.log(lista.lastElementChild.textContent);         // Zadzwonić do babci

// querySelector działa też na elemencie – szuka tylko w jego wnętrzu
console.log(lista.querySelectorAll('li').length);        // 3
console.log(srodek.closest('ul').id);                    // lista – najbliższy przodek pasujący do selektora

document.getElementById('info').textContent = 'Lista ma ' + lista.children.length + ' zadania.';
Podgląd

Używaj wersji z Element w nazwie (parentElement, children, nextElementSibling). Starsze parentNode, childNodes, nextSibling zwracają też węzły tekstowe – białe znaki między znacznikami – i łatwo się na tym przejechać.

Tabela DOM z arkusza egzaminacyjnego

Każdy arkusz z JavaScriptem ma tabelę „Wybrane pola i metody modelu DOM języka JavaScript” – jedyną ściągę na stanowisku bez internetu. Nazwy są bez opisu, więc musisz je rozumieć wcześniej. Skład tabeli różni się między arkuszami – poniżej nazwy, które się w niej pojawiają, uzupełnione o kilka pokrewnych, i gdzie w kursie je znajdziesz:

W tabeli arkuszaCo toLekcja
getElementById, getElementsByTagName, getElementsByClassName, querySelectorszukanie elementówta
innerHTML, innerTexttreść elementuta
style.color, style.fontSize, style.backgroundColor, style.display…CSS z JS15
setAttribute, classList.addatrybuty i klasy15
createElement, appendChild, removeChild, replaceChild, scrollIntoViewtworzenie i usuwanie16
onclick, ondblclick, onmouseover, onmouseout, onkeyup, onkeydown, onloadzdarzenia12
document.writewypisanie podczas ładowania10

Sygnał dla ciebie: jeśli w tabeli pojawia się createElement, autor zadania spodziewa się tworzenia elementów; jeśli tylko getElementById i innerHTML – to klasyczny kalkulator.

Na egzaminie

Ściąga

SkładniaCo robiPrzykład
document.getElementById('x')jeden element o id="x" lub nullconst wynik = document.getElementById('wynik')
document.querySelector('sel')pierwszy element pasujący do selektora CSSdocument.querySelector('#wynik'), '.lista li'
document.querySelectorAll('sel')NodeList wszystkich pasujących (forEach, length, [i])document.querySelectorAll('.wazne')
document.getElementsByClassName('k')żywa kolekcja elementów klasy kpętla for + [i]
document.getElementsByTagName('p')żywa kolekcja elementów p.length = liczba akapitów
el.innerHTML = '…'treść jako HTML'Koszt: <b>20</b> zł'
el.textContent = '…'treść jako czysty tekst'Koszt: 20 zł'
el.innerTexttekst widoczny (CSS-aware)odczyt
el.innerHTML += '…'dopisanie na końcu+= '<br>linia'
el.valuewartość pola formularzapole.value
el.src, el.href, el.altatrybutyimg.src = '2.jpg'
el.style.wlasciwoscjedna właściwość CSSel.style.color = 'red'
el.parentElement, el.childrenrodzic, dziecili.parentElement
el.firstElementChild, el.nextElementSiblingpierwsze dziecko, następny brat
el.closest('sel')najbliższy przodek pasujący do selektorabtn.closest('li')
el.querySelector('sel')szukanie tylko wewnątrz ellista.querySelectorAll('li')
document.title, document.bodytytuł karty, element body
location.hostnamenazwa serwera z adresu strony'localhost'

Zadania

Zadanie 1
łatwe

Trzy akapity, jeden przycisk

Strona ma trzy akapity i przycisk. Napisz skrypt w dialekcie egzaminacyjnym (atrybut onclick, getElementById, innerHTML), który po kliknięciu „Zmień” wpisuje do drugiego akapitu tekst Zmieniono o: i aktualną godzinę (new Date().toLocaleTimeString()), a do trzeciego – liczbę wszystkich akapitów na stronie w formacie Akapitów: 3.

akapity.html (fragment)
<p id="a1">Pierwszy akapit</p>
<p id="a2">Drugi akapit</p>
<p id="a3">Trzeci akapit</p>
<button onclick="zmien()">Zmień</button>
Pokaż rozwiązanie
Trzy akapity
function zmien() {
  const godzina = new Date().toLocaleTimeString();
  document.getElementById('a2').innerHTML = 'Zmieniono o: ' + godzina;

  const liczbaAkapitow = document.getElementsByTagName('p').length;  // kolekcja → length
  document.getElementById('a3').innerHTML = 'Akapitów: ' + liczbaAkapitow;
}
Podgląd
Zadanie 2
średnie

Pogrub wszystkie ważne

Na stronie są akapity i punkty listy, część z klasą wazne. Dodaj dwa przyciski:

  • „Pogrub ważne” – ustawia style.fontWeight = 'bold' wszystkim elementom klasy wazne (pętla po getElementsByClassName);
  • „Policz” – wpisuje do akapitu #raport tekst Ważnych: X z Y elementów, gdzie X to liczba elementów klasy wazne, a Y – liczba wszystkich p i li razem (użyj querySelectorAll('p, li')).

Rozwiąż raz pętlą for z indeksem, a raz querySelectorAll + forEach.

Pokaż rozwiązanie
Pogrub ważne
// wersja 1: kolekcja + pętla z indeksem (dialekt egzaminacyjny)
document.getElementById('pogrub').addEventListener('click', () => {
  const wazne = document.getElementsByClassName('wazne');
  for (let i = 0; i < wazne.length; i++) {
    wazne[i].style.fontWeight = 'bold';
  }
});

// wersja 2: querySelectorAll + forEach
document.getElementById('policz').addEventListener('click', () => {
  const wazne = document.querySelectorAll('.wazne');
  const wszystkie = document.querySelectorAll('p, li');   // p i li – selektor z przecinkiem
  wazne.forEach((el) => (el.style.fontWeight = 'bold'));  // to samo, krócej
  document.getElementById('raport').textContent =
    'Ważnych: ' + wazne.length + ' z ' + wszystkie.length + ' elementów';
});
Podgląd

Uwaga: wszystkie.length liczy też akapit #raport, więc dla tego HTML wynik to Ważnych: 3 z 6 elementów (trzy p i trzy li).

Zadanie 3
średniejak na INF.03

Informacje o stronie

Zadanie wzorowane na pytaniu z E.14 (styczeń 2024), w którym skrypt wpisywał location.hostname do elementu. Strona info.html ma blok główny z nagłówkiem, dwoma obrazami, kilkoma akapitami i przyciskiem „Pokaż informacje”, a pod nim pusty akapit <p id="informacje"></p>.

Skrypt po kliknięciu wpisuje do tego akapitu, w trzech liniach (łamanie <br>):

wynik
Serwer: localhost
Tytuł strony: Moja strona
Obrazów na stronie: 2

Kryteria oceny:

  1. Skrypt w JavaScript, funkcja uruchamiana przyciskiem.
  2. Nazwa serwera pobrana z location.hostname.
  3. Tytuł pobrany z document.title.
  4. Liczba obrazów policzona z kolekcji elementów img (nie wpisana na stałe) – sprawdzana po dodaniu trzeciego obrazu do HTML.
  5. Trzy linie w jednym akapicie #informacje, etykiety i dwukropki jak we wzorze.
  6. Znaczące nazwy zmiennych i funkcji.

HTML do skopiowania:

info.html (fragment body)
<h1>Moja strona</h1>
<img src="1.jpg" alt="Zdjęcie 1">
<img src="2.jpg" alt="Zdjęcie 2">
<p>Akapit pierwszy.</p>
<p>Akapit drugi.</p>
<button onclick="pokazInformacje()">Pokaż informacje</button>
<p id="informacje"></p>
Pokaż rozwiązanie
Informacje o stronie
document.title = 'Moja strona'; // w pliku HTML ustawia to <title>; plac zabaw nie ma <head>

function pokazInformacje() {
  const serwer = location.hostname;                              // kryterium 2
  const tytul = document.title;                                  // kryterium 3
  const liczbaObrazow = document.getElementsByTagName('img').length; // kryterium 4

  document.getElementById('informacje').innerHTML =              // kryterium 5
    'Serwer: ' + serwer + '<br>' +
    'Tytuł strony: ' + tytul + '<br>' +
    'Obrazów na stronie: ' + liczbaObrazow;
}
Podgląd

W placu zabaw podgląd nie ma adresu z serwera, więc location.hostname jest pusty. Zapisz plik w folderze htdocs XAMPP-a, otwórz http://localhost/info.html – zobaczysz Serwer: localhost. Tak samo będzie na stanowisku egzaminacyjnym.

Zadanie 4
trudne

Automatyczny spis treści

Artykuł ma kilka nagłówków h2 wewnątrz <article>. Napisz skrypt, który po załadowaniu strony (bez klikania):

  1. zbiera wszystkie h2 z artykułu (querySelectorAll('article h2')),
  2. nadaje każdemu id w formacie sekcja-1, sekcja-2… (właściwość id),
  3. buduje w pętli napis z listą HTML: <li><a href="#sekcja-1">Tytuł</a></li>…,
  4. wstawia listę przez innerHTML do <ul id="spis">,
  5. do akapitu #licznik wpisuje Sekcji: N.

Dodatkowo: numer sekcji ma pojawić się na początku nagłówka (1. Wstęp). Nie używaj jeszcze createElement – tylko napisy i innerHTML.

Pokaż rozwiązanie
Spis treści
const naglowki = document.querySelectorAll('article h2');  // NodeList wszystkich h2
let listaHtml = '';

naglowki.forEach((naglowek, indeks) => {
  const numer = indeks + 1;                     // indeksy od 0, numeracja od 1
  naglowek.id = 'sekcja-' + numer;              // nowy atrybut id
  naglowek.textContent = numer + '. ' + naglowek.textContent;
  listaHtml += '<li><a href="#' + naglowek.id + '">' + naglowek.textContent + '</a></li>';
});

document.getElementById('spis').innerHTML = listaHtml;     // jeden zapis zamiast czterech
document.getElementById('licznik').textContent = 'Sekcji: ' + naglowki.length;
Podgląd

Budowanie napisu w pętli i jeden zapis innerHTML na końcu jest szybsze niż innerHTML += w każdej iteracji (przeglądarka parsowałaby HTML cztery razy). W lekcji o tworzeniu elementów zrobisz to samo przez createElement i appendChild.

Test powtórkowy

Test powtórkowy

Format jak w części pisemnej INF.03: cztery odpowiedzi, jedna poprawna.

0/0poprawnych
Sprawdź się

Po pobraniu elementu metodą getElementById jego zawartość (tekst między znacznikami) można zmienić, przypisując wartość do właściwości

Sprawdź się

Aby pobrać wszystkie znaczniki strony, którym przypisano klasę nomargin, należy użyć metody

Sprawdź się

Co zrobi poniższy kod?

var elementy = document.getElementsByClassName('styl1');
for (var i = 0; i < elementy.length; i++) {
elementy[i].style.fontWeight = 'bolder';
}
Sprawdź się

W wykropkowane miejsce należy wpisać

document.getElementById('naglowek').........color = 'red';
Sprawdź się

Co wypisze kod, jeśli na stronie nie ma elementu o identyfikatorze cennik?

var el = document.getElementById('cennik');
console.log(el);
Sprawdź się

Do elementu o id p1 przypisano: document.getElementById('p1').textContent = '<b>Uwaga</b>'; Co zobaczy użytkownik?

Sprawdź się

Które wywołanie zwróci pierwszy element listy li znajdujący się wewnątrz elementu o klasie menu?

Sprawdź się

Skrypt umieszczony w sekcji head strony zawiera wiersz: var p = document.getElementById('wynik').innerHTML; W konsoli pojawia się błąd TypeError. Dlaczego?

Sprawdź się

Co wyświetli strona otwarta pod adresem http://localhost/sklep/index.html po wykonaniu kodu?

var h = location.hostname;
document.getElementById('adres').innerHTML = h;
Sprawdź się

Które z poleceń jest poprawne składniowo i zmieni kolor tekstu wszystkich elementów klasy info?

var info = document.getElementsByClassName('info');