A JavaScript DOM-manipulációs lehetőségei

Ebben a jegyzetben áttekintjük a HTML DOM-mal kapcsolatos fontosabb tudnivalókat, majd egy gyakorlati példán keresztül megismerkedünk a JavaScript által biztosított fontosabb DOM-manipulációs lehetőségekkel.

1. HTML DOM

A webfejlesztésben a HTML nyelvet használjuk weboldalak létrehozására. Ennek a nyelvnek a segítségével mondhatjuk meg, hogy mi az, amit egy weboldalon látni szeretnénk (pl. szövegek, képek, táblázatok, űrlapok, multimédia stb.). Emellett a HTML lehetőséget biztosít a weboldalon megjelenő tartalom strukturálására is, különféle szakaszok, tartalmi egységek kialakításával.

A HTML dokumentumok úgy épülnek fel, hogy HTML objektumokat (úgynevezett tageket) ágyazunk egymásba. Ezek az objektumok egy hierarchikus fastruktúrát alkotnak a dokumentumban.

Amikor egy weboldal betöltődik, akkor a böngésző a weboldalon található HTML objektumokból elkészíti az úgynevezett dokumentum-objektum modellt, avagy röviden a DOM-ot. A DOM-fa (DOM tree) segítségével könnyen szemléltethetjük a weboldalon található HTML elemek hierarchikus viszonyait.

Példa: Egy egyszerű HTML kód és az ahhoz tartozó DOM-fa.

<!DOCTYPE html>
<html lang="hu">
  <head>
    <title>DOM példa</title>
    <meta charset="UTF-8"/>
  </head>
  <body>
    <h1>Az oldal címe</h1>
    <img src="my-image.png" alt="Valami klassz kép"/>
    <p>
      <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Valami klassz link</a>
    </p>
  </body>
</html>

HTML DOM-fa

Megjegyzés: A DOCTYPE nem egy HTML tag, ezért a DOM-fában sem szerepel.

1.1. HTML elemek DOM-beli viszonyai

Ha egy HTML dokumentumban az A objektum (nem feltétlen közvetlenül) tartalmazza a B objektumot, akkor azt mondjuk, hogy az A objektum a B objektum őse, a B objektum pedig A-nak leszármazottja. Amennyiben ez a tartalmazás közvetlen, akkor A-t a B szülőjének, B-t pedig az A gyerekének nevezzük.

Néhány példa a fenti kódból és az ahhoz tartozó DOM-fából:

Ha az A és B objektumok szülője megegyezik, akkor A és B egymás testvérei. Például a fenti kódban és az ahhoz tartozó DOM-fában a <h1>, <img> és <p> elemek egymás testvérei, hiszen mindhárom elem szülője a <body>.

A DOM-fa tetején lévő, szülővel nem rendelkező elemet a fa gyökérelemének nevezzük. A teljes HTML DOM-ban a gyökérelem mindig a <html> objektum lesz (ugyanis ebbe ágyazunk be minden további HTML elemet).

1.2. A DOM gyakorlati jelentősége

Amikor a weboldalunk tartalmát CSS-ben formázzuk, akkor használhatunk olyan szelektorokat (kijelölőket) is, amelyek a DOM-beli viszonyaik alapján jelölnek ki HTML objektumokat. Néhány példa DOM-alapú CSS szelektorokra, a teljesség igénye nélkül:

A webes világban gyakran előfordul, hogy dinamikusan szeretnénk manipulálni a DOM-fát, miután már a weboldal betöltődött (pl. szeretnénk egy objektumot módosítani vagy törölni, esetleg egy új objektumot akarunk a fába beszúrni). Erre biztosítanak lehetőséget a JavaScript DOM-manipulációs műveletei.

A DOM tulajdonképpen nem más, mint egy objektumorientált reprezentációja a weboldalnak. A weboldalon szereplő elemek Node-ok (csomópontok) a DOM-fában, amelyek számos property-vel (adattaggal) és metódussal rendelkeznek. Ezeket JavaScriptből egyszerűen el tudjuk érni.

A következő fejezetben megismerkedünk a JavaScript néhány fontosabb DOM-manipulációs lehetőségével. Fogjuk párszor használni a document objektumot, ami lényegében a böngésző által megnyitott HTML dokumentumot reprezentálja és hozzáférést biztosít a DOM-fához.

Oké, de mi értelme van ennek? Miért nem lehet csak simán a HTML-t átírogatni?

A hangsúly itt azon van, hogy azután szeretnénk a weboldal tartalmát dinamikusan módosítani, miután az oldal már betöltődött. Nézzünk néhány gyakorlati példát, amikor DOM-műveleteket használunk:

Könnyen belátható, hogy a HTML önmagában nem elég robusztus ahhoz, hogy "utólag" manipuláljuk a weboldalaink szerkezetét. Ezért van szükségünk a DOM-ra és a JavaScript DOM-műveleteire.

2. JavaScript DOM-műveletek, egy példán keresztül

A jegyzet hátralévő részében egy végletekig leegyszerűsített feladatlista alkalmazást fogunk elkészíteni. A weboldalon megjelennek a napi feladataink, amelyeket lehetőségünk van törölni, ha teljesítettük őket. Emellett új feladatot is bármikor létrehozhatunk.

A példaprojekt elkészítéséhez szükséges kiinduló fájlok letölthetők egy ZIP-ben, ide kattintva.

Megjegyzés: Mivel csak a DOM-műveletek bemutatása a cél, ezért az alkalmazás eléggé kezdetleges lesz: a feladatokat nem mentjük el sehova, így az oldal frissítésekor a dinamikusan hozzáadott adatok elvesznek. Emellett lesznek beégetett adataink is. Ha valaki ennél egy fokkal realisztikusabb weboldalt szeretne készíteni, akkor a 2.6. fejezetben talál tippeket a példaprojekt "felokosítására".

2.1. Objektumok megkeresése a DOM-fában

Egy egyszerű feladattal fogunk indítani: keressük meg JavaScriptben az alábbi <h1>-es címsort a DOM-fában, és írassuk ki azt a konzolra!

<h1 id="page-title" class="text-center">Feladataim</h1>

HTML objektumok DOM-fában történő megkeresésére többféle lehetőségünk is van:

Példa: A felsoroltak közül bármelyik szelektorral megkereshetjük a fenti címsort. Az alábbi utasítások mindegyikének hatására a kérdéses címsor fog kiíródni a konzolra.

console.log(document.getElementById("page-title"));
console.log(document.getElementsByTagName("h1")[0]);                 // kollekciót ad vissza, indexeljük!
console.log(document.getElementsByClassName("text-center")[0]);      // kollekciót ad vissza, indexeljük!
console.log(document.querySelector("header h1.text-center"));
console.log(document.querySelectorAll("header h1.text-center")[0]);  // kollekciót ad vissza, indexeljük!

2.2. Eseménykezelés

Miközben a felhasználó böngészi a weboldalunkat, történhetnek különféle események - pl. a felhasználó rákattint egy oldalelemre, egy elem fölé viszi a kurzort, egy HTML elem betöltődik vagy megváltozik. Ezekhez az eseményekhez társíthatunk eseménykezelő függvényeket, amelyek akkor hívódnak meg, ha az adott esemény bekövetkezik.

Az eseménykezelés egyik módja, hogy az elemeknek adott, eseménykezeléssel kapcsolatos attribútumokkal szabályozzuk az események működését. A kérdéses HTML elemet ellátjuk az alábbi attribútumok valamelyikével, és az attribútum értékeként megadjuk az eseménykezelést végző függvényt.

Megjegyzés: Ezeken kívül vannak még további eseménykezeléssel kapcsolatos attribútumok is, amiket használhatunk. A teljes listát megtalálhatjuk ezen a linken.

Példa: A példaprojektben található "Hozzáadás" gombra kattintva az addTask() eseménykezelő függvény hívódik meg.

<button type="button" class="add-btn" onclick="addTask()">Hozzáadás</button>

A példaprojektben kizárólag az attribútumokkal történő eseménykezelésre találunk példát. Egy másik módszer az eseménykezelésre, ha egy DOM-beli elem addEventListener() metódusát használjuk, ezzel rendeljük hozzá az elemhez az eseménykezelő függvényt (ekkor az elemnek nem kell semmilyen attribútumot adni). Egy HTML elemhez több eseménykezelő is hozzárendelhető (akár ugyanarra az eseménytípusra is).

Az addEventListener() metódus paraméterei sorban:

Példa: Eseménykezelő hozzárendelése egy gombhoz az addEventListener() metódussal.

<body>
  <button type="button" id="my-btn">Kattints rám!</button>
  <script>
    const button = document.getElementById("my-btn"); // gomb megkeresése

    button.addEventListener("click", function() {  // kattintás esemény kezelése
      alert("Hurrá, működik az eseménykezelő!");
    });
  </script>
</body>

Capturing és bubbling

Tegyük fel, hogy van egy HTML elemünk, ami egy másik HTML elembe van beágyazva! Mind a beágyazó elemhez, mind a beágyazott elemhez hozzárendelünk egy-egy eseménykezelőt ugyanarra az eseménytípusra - mondjuk a kattintásra. Ha a belső elemre kattintunk, akkor mindkét elem eseménykezelője meghívódik. Az addEventListener() metódus harmadik paraméterével szabályozhatjuk, hogy milyen sorrendben legyenek ezek meghívva.

Ha true-ra állítjuk a harmadik paramétert, akkor capturing történik. Ekkor az eseményt először a legkülső elem kezeli le, majd ezután mindig az eggyel "beljebb" található elem eseménykezelő függvénye hívódik meg.

Ha false-ra állítjuk a harmadik paramétert, akkor bubbling történik. Ekkor az eseményt először a legbelső elem kezeli le, majd ezután mindig az eggyel "kijjebb" található elem eseménykezelő függvénye hívódik meg. Ha nem adjuk meg expliciten a 3. paraméter értékét, akkor alapértelmezett módon mindig bubbling történik.

Nézzünk egy példát! Figyeljük meg a konzolon, hogy az "eseménykezelők" feliratra kattintva, capturing esetén "kívülről befelé", míg bubbling esetén "belülről kifelé" sorrendben hívódnak meg az elemek eseménykezelői!

<body>
  <p id="my-paragraph">
    Ez egy példa <strong id="my-strong">eseménykezelők</strong> használatára.
  </p>
  <script>
    // Két függvény, amely kiírja annak a HTML elemnek a nevét, amelyen az esemény bekövetkezett
    // A capture()-t majd capturing, a bubble()-t pedig majd bubbling módban fogjuk használni
    function capture() { console.log("Capturing: " + this.tagName); }   
    function bubble() { console.log("Bubbling: " + this.tagName); }

    // A <p> és <strong> elemek megkeresése a DOM-fában
    const p = document.getElementById("my-paragraph");      // külső (beágyazó) elem
    const strong = document.getElementById("my-strong");    // belső (beágyazott) elem
 
    // Kattintás eseményre vonatkozó eseménykezelők hozzárendelése a két elemhez
    p.addEventListener("click", capture, true);             // capturing
    strong.addEventListener("click", capture, true);        // capturing
    p.addEventListener("click", bubble, false);             // bubbling
    strong.addEventListener("click", bubble, false);        // bubbling
  </script>
</body>

A kimenet a konzolon: Capturing: P, Capturing: STRONG, Bubbling: STRONG, Bubbling: P.

2.3. Elemek beszúrása és módosítása

Ha egy új elemet szeretnénk a DOM-fába beszúrni, akkor a következő lépéseket kell követnünk:

  1. A document.createElement(tagname) metódussal létrehozzuk a beszúrandó HTML elemet.
  2. Beállítjuk az újonnan létrehozott elem tartalmát, attribútumait és stílusát (opcionális lépés).
  3. Beszúrjuk az elemet a DOM-fába a szülőobjektum append() vagy appendChild() metódusával.

Az append() és appendChild() metódusok mindketten arra szolgálnak, hogy egy DOM-beli objektumhoz gyerekobjektumot fűzzünk hozzá. Két fontos különbség a két metódus között:

Megjegyzés: Az append() és appendChild() metódusok mindig a szülőelem legutolsó gyerekeként szúrják be az új objektumot a DOM-fába. Ha a szülő egy tetszőleges indexű gyerekeként szeretnénk beszúrni az elemet a fába, akkor használjuk a szülő insertBefore() metódusát.

A metódus két paramétert vár: rendre a beszúrandó objektumot, és a szülőelem azon gyerekét, ami elé be fogjuk szúrni az új elemet. A szülőelemnek felhasználhatjuk a children property-jét, ami visszaadja az elem összes gyerekét egy indexelhető kollekcióban.

Nézzünk egy példát! Szúrjunk be az alábbi weboldalra egy "Második bekezdés" feliratú <p> objektumot, a "Harmadik bekezdés" feliratú <p> elem elé!

<div id="parent">
  <p>Első bekezdés</p>
  <p>Harmadik bekezdés</p>
</div>
<script>
  const parent = document.getElementById("parent");       // a szülőobjektum
  const newParagraph = document.createElement("p");       // az új elem létrehozása
  newParagraph.innerText = "Második bekezdés";
  // Az új elem beszúrása a szülőobjektum második (1. indexű) gyereke elé
  parent.insertBefore(newParagraph, parent.children[1]);
</script>

Feladat: Tegyük működőképessé az "új feladat hozzáadása" funkciót a példaprojektben!

A megoldás menete:

A feladat hozzáadását végző űrlap HTML kódja a következő:

<form>
  <input type="text" id="task-text" class="form-input"/>
  <button type="button" class="add-btn" onclick="addTask()">Hozzáadás</button>
</form>

Látható, hogy a "Hozzáadás" gombhoz hozzárendeltük az onclick attribútummal az addTask() eseménykezelő függvényt. Ez a függvény fog meghívódni, amikor a felhasználó a gombra kattint, így ennek a törzsét kell megírnunk. Azt szeretnénk, hogy a függvény kérdezze le az űrlapmezőbe írt szöveget és szúrja be azt és egy "Törlés" gombot a weboldalon található táblázat egy új sorába. Valahogy így:

<table>
  <!-- A táblázat fejléce... -->
  <tbody>
    <!-- Néhány korábbi feladat... -->
    <tr>
      <td>Az újonnan beszúrt feladat szövege...</td>
      <td><button type="button" class="delete-btn" onclick="deleteTask(this)">X</button></td>
    </tr>
  </tbody>
</table>
A feladat megoldásának lépései
  1. Keressük meg a <tbody> objektumot, hiszen ennek a gyerekeként fogjuk az új feladatot beszúrni!
  2. Hozzunk létre a beszúrni kívánt feladatnak egy új sort a táblázatban (<tr>)! A sorban helyezzünk el két táblázatcellát (<td>)!
  3. Az első táblázatcellába írjuk bele a feladat szövegét, vagyis az űrlapon található beviteli mezőbe írt értéket!
  4. A második táblázatcellában helyezzünk el egy "Törlés" gombot, a fenti kódban található mintának megfelelően!
  5. Szúrjuk be a két <td> objektumot a <tr> gyerekeiként a DOM-fába! A <tr> objektum pedig legyen a <tbody> gyereke a weboldalon!

A megoldás elkészítése:

Keressük meg a <tbody> objektumot!

const tbody = document.getElementsByTagName("tbody")[0];

Hozzunk létre egy táblázatsort és két táblázatcellát! A cellákat a sor gyerekeiként, a sort pedig a <tbody> gyerekeként szúrjuk be a DOM-fába!

const tbody = document.getElementsByTagName("tbody")[0];

const row = document.createElement("tr");
const column1 = document.createElement("td");
// Itt majd kialakítjuk az első cella tartalmát...
const column2 = document.createElement("td");
// Itt majd kialakítjuk a második cella tartalmát...

row.append(column1, column2);
tbody.append(row);

Írjuk bele az első táblázatcellába az id="task-text" attribútummal rendelkező beviteli mezőbe írt szöveget!

// ...
const column1 = document.createElement("td");
column1.innerText = document.getElementById("task-text").value;
// ...

Már csak a második táblázatcella tartalmát kell kialakítanunk. Ebben egy törlés gombot fogunk elhelyezni, tehát a document.createElement() metódussal létrehozunk egy új <button> objektumot, amit a második táblázatcella gyerekeként szúrunk be a DOM-fába. A gomb szöveges tartalma legyen X!

Ahhoz, hogy a létrehozott gomb megfelelően működjön, hozzáadunk néhány attribútum-érték párt. Egy HTML objektum attribútumainak beállítása a setAttribute() metódussal történik. Ennek első paramétereként megadjuk a beállítani kívánt attribútum nevét, második paraméterként pedig az attribútum értékét.

Rendeljük hozzá a gombhoz a class="delete-btn" attribútumot, hogy ugyanúgy legyen formázva, mint a többi törlés gomb! Ehhez használjuk az elem classList property-jét, amivel lekérhetjük az összes olyan class nevét, amivel az elem rendelkezik. A classList-nek az add() metódusával hozzáadjuk a gombhoz a delete-btn class-értéket.

// ...
const column2 = document.createElement("td");
const deleteBtn = document.createElement("button");   // gomb létrehozása
deleteBtn.innerText = "X";                            // gomb szöveges tartalmának beállítása
deleteBtn.setAttribute("type", "button");             // gomb attribútumainak beállítása
deleteBtn.setAttribute("onclick", "deleteTask(this)");
deleteBtn.classList.add("delete-btn");  // class="delete-btn" attribútum hozzáadása a gombhoz
column2.append(deleteBtn);              // gomb beszúrása a DOM-fába a 2. táblázatcella gyerekeként
// ...

Tehát az addTask() függvény végleges verziója a következőképpen néz ki:

function addTask() {
  const tbody = document.getElementsByTagName("tbody")[0];
  const row = document.createElement("tr");

  const column1 = document.createElement("td");
  column1.innerText = document.getElementById("task-text").value;

  const column2 = document.createElement("td");
  const deleteBtn = document.createElement("button");   
  deleteBtn.innerText = "X";                           
  deleteBtn.setAttribute("type", "button");           
  deleteBtn.setAttribute("onclick", "deleteTask(this)");
  deleteBtn.classList.add("delete-btn");
  column2.append(deleteBtn);

  row.append(column1, column2);
  tbody.append(row);
  document.getElementById("task-text").value = "";  // űrlapmező tartalmának kiürítése
}

1. megjegyzés: A classList egyéb metódusai a class-hozzáadásra szolgáló add()-on kívül:

2. megjegyzés: A példaprojektben erre nem találunk példát, de az objektum stílusának módosítása az elem style property-jével lehetséges. Ez lényegében a style attribútum értékének módosításával állítja be a HTML elem stílusát (ezt inline CSS-nek is nevezzük).

<body>
  <p id="my-paragraph">I'm blue da ba dee da ba die...</p>
  <script>
    // A bekezdés kék betűszínűvé tétele JavaScript segítségével
    document.getElementById("my-paragraph").style.color = "blue";
  </script>
</body>

A style property-ről (ami tulajdonképpen egy objektum) szóló részletes referencia elérhető itt.

2.4. Objektumok törlése

2.4.1. Egy adott gyerekobjektum törlése

Ahhoz, hogy egy objektumot kitöröljünk a DOM-fából, szükségünk lesz a törlendő objektumra és annak szülőjére. A törléshez a szülő removeChild() metódusának paramétereként adjuk meg a törlendő objektumot.

Feladat: Tegyük működőképessé a táblázat soraiban megjelenő "Feladat törlése" gombokat!

A megoldás menete:

Minden ilyen, feladat törlésére szolgáló gomb forráskódja a következő:

<button type="button" class="delete-btn" onclick="deleteTask(this)">X</button>

A gombra kattintva tehát a deleteTask() eseménykezelő függvény hívódik meg, így ennek a törzsét kell megírnunk. Azt szeretnénk, hogy a gombra kattintva töröljük ki a táblázatból azt a sort, amihez a gomb tartozik. A függvény paraméterben megkapja az aktuális objektumot (this), azaz a gombot, amire kattintottunk.

A törlendő sorhoz tartozó gomb tehát a függvény paramétereként adott. Ahhoz, hogy ebből megkapjuk a törlendő sort, végig kell gondolnunk a gomb és az őt tartalmazó táblázatsor viszonyát. A törlés gombok szülője egy <td> (cella), amelynek szülője lesz a törlendő <tr> (sor). Tehát két szülővel kell "feljebb lépnünk" a DOM-ban a megnyomott gombhoz képest. Kelleni fog még a törlendő sor szülője is, ami a <tbody> objektum lesz.

Mindez egy ábrán szemléltetve:

Egy táblázatsor törlése

A feladat megoldásának lépései
  1. Keressük meg a <tbody> objektumot, hiszen ennek az egyik gyerekét (egy táblázatsort) fogjuk kitörölni!
  2. Keressük meg a megnyomott gomb "nagyszülőjét" (azaz a szülőjének a szülőjét), hiszen ez lesz a törlendő sor!
  3. A <tbody> objektum removeChild() metódusával töröljük a megtalált sort a <tbody> gyerekei közül!

A megoldás elkészítése:

A DOM-beli elemek parentNode property-jével lekérdezhetjük az adott elem szülőjét. Ahhoz, hogy megkapjuk a megnyomott gomb "nagyszülőjét" (a törlendő sort), kétszer egymás után alkalmazzuk a property-t.

A deleteTask() függvény végleges verziója a következőképpen néz ki:

function deleteTask(btn) {
  const tbody = document.getElementsByTagName("tbody")[0];  // <tbody> megkeresése
  const row = btn.parentNode.parentNode;  // a törlendő sor a megnyomott gomb "nagyszülője" lesz
  tbody.removeChild(row);                 // a sor kitörlése a <tbody> gyerekei közül
}

2.4.2. Összes gyerekobjektum törlése

Feladat: Tegyük működőképessé a weboldalon található "Összes feladat törlése" gombot!

A megoldás menete:

A fent említett gomb HTML forráskódja a következő:

<button type="button" id="delete-all-tasks-btn" class="delete-btn" onclick="deleteAllTasks()">
  Összes feladat törlése
</button>

A gombra kattintva tehát a deleteAllTasks() eseménykezelő függvény hívódik meg, így ennek a törzsét kell megírnunk. Azt szeretnénk, hogy a függvény törölje ki az összes feladatot a DOM-fából. A feladatok továbbra is a <tbody> gyerekeiként, táblázatsorok formájában vannak jelen a DOM-ban.

JavaScriptben nincs olyan explicit DOM-metódusunk, amely egy objektum összes gyerekét kitörölné, ezért egy kicsit másképp oldjuk meg a dolgokat:

A feladat megoldásának lépései
  1. Keressük meg a <tbody> objektumot, hiszen ennek az összes gyerekét (a feladatokat tartalmazó táblázatsorokat) fogjuk kitörölni!
  2. Amíg van a <tbody>-nak gyereke, addig mindig töröljünk ki egy tetszőleges gyereket!

A megoldás elkészítése:

Teljesen mindegy, hogy mikor melyik gyereket töröljük ki, hiszen végül minden gyereket ki fogunk törölni. A példánkban én mindig a <tbody> legelső gyerekét törlöm, amit a szülő firstChild property-jével érhetünk el.

Tehát a deleteAllTasks() függvény végleges verziója a következőképpen néz ki:

function deleteAllTasks() {
  const tbody = document.getElementsByTagName("tbody")[0];  // <tbody> megkeresése

  while (tbody.hasChildNodes()) {         // <tbody> összes gyerekének törlése
    tbody.removeChild(tbody.firstChild);
  }
}

2.5. A kész forráskód letöltése

A teljes, kikommentezett megoldás letölthető ide kattintva.

2.6. A példaprojekt "okosabbá tétele"

Megjegyzés: A "felokosított" példaprojekt letölthető ide kattintva.

3. Egy rövid megjegyzés

Összetettebb webes projektekben általában nem a jegyzetben tárgyalt, hagyományos DOM-műveleteket szoktuk használni. Vannak ugyanis mindenféle library-k és keretrendszerek (framework-ök), amelyek használatával jelentősen leegyszerűsödik a DOM-manipuláció megvalósítása a fejlesztők számára. Viszont mivel ezek a library-k és framework-ök önmagukban megérnének egy-egy külön jegyzetet, ezért ezekbe most nem megyünk bele.