JavaScript is the duct tape of the internet.
—Charlie Campbell

Het Document Object Model

Opnieuw een langere pagina, gebaseerd op javascript.info en op lessen van de voorbije jaren. We wisselen tekst en code af met filmpjes, omdat het soms belangrijk is dat je ziet hoe je in de console werkt. Mijn doel hier is je een basis meegeven van hoe JS in de browser werkt en tonen wat het kan. Het is maar een heel kleine overzicht, waarbij ik me zeer sterk beperk tot enkele voorbeelden die de principes illustreren. Bij de uitwerking van je eigen functies kan je bijna niet anders dan dingen opzoeken.

Browser omgeving

JS is ontwikkeld voor gebruik binnen een browser. Het is dan ook logisch dat er heel wat dingen in de taal nauw verbonden zijn met de browser. Het centrale globale object in de browser is window. Probeer bvb. volgende code in de console om de hoogte van het browservenster op te vragen. Maak daarna het venster iets kleiner en voer de code terug uit in de console.

console.log(window.innerHeight);

Het Document Object Model (DOM) is een voorstelling van alle inhoud van een pagina als objecten waarvan je heel wat gegevens kan opvragen of veranderen. Het object document is het startpunt om met het DOM te werken. De rest van deze pagina gaat helemaal over het DOM.

DOM boom

Ik wil dit verhaal zo concreet mogelijk proberen te brengen via een voorbeeld. We beperken ons tot het illustreren van enkele principes. Als goede developer zal je zelf andere dingen moeten opzoeken, bvb. in enkele van de geciteerde bronnen. Het vertrekpunt is volgend HTML-bestand:

<!DOCTYPE html>
<html lang="nl">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DOM</title>
</head>
<body>
  <header>
    <h1>Het Document Object Model</h1>
    <nav>
      <ul>
        <li class="belangrijk online"><a href="https://javascript.info/">Javascript.info</a></li>
        <li><a href="https://exploringjs.com/impatient-js/">JavaScript for impatient programmers</a></li>
        <li><a href="https://eloquentjavascript.net">Eloquent JavaScript</a></li>
      </ul>
    </nav>
  </header>
  <main>
    <h2>Javascript en de browser</h2>
    <p>JS is oorspronkelijk gemaakt om in de browser uitgevoerd te worden.</p>
    <p><img src="patroon.jpg" alt=""></p>

    <h3>DOM</h3>
    <p class="belangrijk">Het <abbr title="Document Oject Model">DOM</abbr> is een voorstelling van een pagina als een boomstructuur met:</p>
    <ul>
      <li>ouders</li>
      <li>kinderen</li>
      <li>broers / zussen</li>
    </ul>

    <h3>Events</h3>
    <p>Via JS kunnen we reageren op een gebeurtenis, zoals het klikken op een knop.</p>
  </main>
  <footer>

  </footer>
</body>
</html>

Dit bestand kan er in een browser (met enkel de standaard stijl van de browser zelf) als volgt uitzien:

basisdocument met enkel styling van browser

Dit HTML-document wordt geladen door de browser, die er een boomvoorstelling (tree) van maakt. De wortel (root) is het element html. Dat element heeft twee kinderen (takken): head en body. Deze familierelaties benadrukten we reeds in beide delen HTML en CSS.

In feite moeten we de uitleg in wat volgt wat vereenvoudigen. De boomstructuur bestaat namelijk uit knopen (nodes). De HTML-elementen waar we hierboven over spraken zijn zogenaamde ‘element nodes’. Zo is er in de code een elementknoop h1. Deze knoop bevat een tekstknoop (‘text node’) met als inhoud de string "Het Document Object Model". To zover is alles eenvoudig …

Wat de zaken wel een beetje bemoeilijkt, is dat de ‘enters’ (nieuwe lijn symbolen) op het einde van een regel in de HTML-code ook nodes zijn. Die tekstknopen zijn meestal niet erg interessant om iets mee te doen. In het vervolg van deze tekst zullen we altijd naar de elementknopen en hun inhoud verwijzen. Trouwens, alles in de HTML-code is een deel van het DOM. Een commentaar in HTML (tussen <!-- en -->) is in het DOM een comment node en als zodanig ook door JS te bereiken.

De developer tools van de browser tonen je deze DOM grafisch. De vervelende tekstknooppunten die overeenkomen met het nieuwe lijn symbool worden meestal niet getoond in dit overzicht. Je kan de driehoekjes voor de elementen open of dicht klikken.

Visuele weergave van het DOM in de developer tools

Wandelen door de DOM boom

Met ‘wandelen door een boom’ wordt bedoeld: ga naar specifieke elementen, zoek hun kinderen, kleinkinderen, ouders, broer / zus, grootouders enz. Een belangrijk onderscheid (ook in CSS trouwens) is het verschil tussen kind (child) en nakomeling (descendant)

In CSS bestaat de kindselector (child selector) en de nakomelingselector (descendant selector). Weet je nog welke dit zijn? Zoek eventueel even op.

De meestgebruikte in CSS is de nakomelingselector: de spatie. Zo betekent de selector main ul “alle elementen ul die ergens in main zitten, mogelijk verschillende niveau's diep”. Als je de spatie vervangt door een > wordt de betekenis strenger. Zo betekent main > ul “alle elementen ul die een direct kind zijn van het element main”.

De stamvader van de familiestamboom is het document object in JS. Dit element heeft heel veel eigenschappen en methodes, bvb. document.body om het element body te selecteren, of document.head voor het head element.

We kunnen nu op zoek gaan child nodes, parent nodes enz. Maar zoals hierboven uitgelegd vinden we dan ook de niet zo interessante nieuwe lijn symbolen als tekstknopen. Ook commentaarknopen vinden we zo, maar ook die zijn meestal niet zo interessant. We beperken ons dus liever tot het zoeken naar elementen.

Kinderen

De eigenschap children geeft van een elementknoop alle kinderen weer. Het resultaat is een zogenaamde collection. Dat is iets wat lijkt op een array (maar het niet is) en waarvan je alle elementen kan aflopen, de lengte kan bepalen enz.

In het volgende voorbeeld vragen we van document de eigenschap body op, en van dit element vragen we de eigenschap children. Je kan dus met de punt-notatie heel wat eigenschappen aan elkaar knopen. Een beetje verrassend geeft deze eigenschap aan dat er vier kinderen van body zijn, terwijl er duidelijk maar drie in de code staan. Het vierde kind (met nummer 3, want een collection begint bij het element 0) is echter een div die door een VS Code extensie automatisch toegevoegd werd aan mijn code.

document.body.children;
// geeft: HTMLCollection { 
//	0: header, 
//	1: main, 
//	2: footer, 
//	3: div#ConnectiveDocSignExtentionInstalled, 
//	length: 4, … }

Wat is het resultaat van volgende regel JS?

document.body.children[1].children.length;

Het resultaat is het getal 8. Het kind met nummer 1 is het element main. Dit element heeft 8 kinderen.

Je kan via firstElementChild (merk op dat men in JS altijd camel case gebruikt) en lastElementChild het eerste en laatste kind element van een ander element opvragen.

Welk element wordt geselecteerd door volgende JS code:

document.body.children[0].lastElementChild;

Deze regel selecteert het element nav. Het kind van body met nummer 0 is header. Het element header heeft twee kinderen. Het laatste kind van dit element is nav.

Broer / zus

Het Nederlands heeft geen vertaling voor het Engelse ‘sibling’. We moeten dit woord omschrijven door ‘broer / zus’. Elementen die dezelfde ouder hebben zijn siblings. Met previousElementSibling selecteer je het element dat vlak boven het huidige element staat en dat hetzelfde ouderelement heeft. De eigenschap nextElementSibling doet dan hetzelfde voor de broer of zus vlak onder het huidige element.

Welk element is het resultaat van:

document.head.children[1].nextElementSibling;

title: het kind van head met nummer 1 is het tweede kind (meta name="viewport" …). De sibling die daarop volgt is het element title.

Zoeken naar elementen in de DOM

We kunnen nu in het DOM stamboomgewijs wandelen van ouder naar kind en omgekeerd. Echt handig is dat natuurlijk niet. Er moet een snellere manier bestaan om een bepaald element uit het DOM te selecteren. Die manieren (meervoud) bestaan natuurlijk.

getElementById()

Luie developers of zij die niet de moeite nemen om CSS grondig te leren kennen geven elk HTML element een apart id. Met de methode document.getElementById("…") kan je dan elk element dat een id attribuut heeft, selecteren. Die (ver)oude(rde) methode werkt nog steeds, maar ik vind het veel interessanter om je te leren werken met querySelector()

querySelector()

Je weet dat we in CSS heel specifieke selectoren kunnen gebruiken om een element van stijl te voorzien. Dat is de basis voor de krachtige methode document.querySelector("…") die tussen haakjes elke CSS selector als argument (in de vorm van een string) kan nemen. Enkele voorbeelden, toegepast op het document dat hierboven beschreven is:

document.querySelector("nav"); // selecteert het enige nav element
document.querySelector("main ul"); // selecteert alleen de ul in main
document.querySelector('img[src="patroon.jpg"]') // selecteer de img met het gewenste attribuut
document.querySelector("#kalender") // element met id kalender (is hier niet, dus geeft null)

Aan het laatste voorbeeld kan je zien dat de methode getElementById() niet meer nodig is, want je kan gebruik maken van de methode querySelector("#…") die veel meer mogelijkheden heeft.

querySelectorAll()

Wat als er meerdere elementen voldoen aan de selector in querySelector()? Dan geeft de methode als resultaat het eerste element terug.

Vaak wil je echter effectief alle elementen die voldoen aan de selector, omdat je met al deze elementen iets wilt doen via een JS script. In dit geval gebruik je querySelectorAll("…"). Deze methode geeft een collection terug. Je kan dan een specifiek element eruit pikken met [getal]. Of je schrijft een lus om alle elementen uit de collection één voor één te selecteren.

document.querySelectorAll("main > h2");
// selecteert alle h2 elementen die kind zijn van main

Eén van de scripts die ik gebruik in deze site staat hieronder. Ontleed de code en zoek uit wat ze juist doet.

let alleTitels = document.querySelectorAll('h2, h3');
let h2Teller = 1;
let h3Teller = 1;
for (let i = 1; i <= alleTitels.length; i++){
  if (alleTitels[i-1].localName === 'h2') {
    h3Teller = 1;
    alleTitels[i-1].innerText = h2Teller + " " + alleTitels[i-1].innerText;
    h2Teller++;
  } else {
    alleTitels[i-1].innerText = h2Teller-1 + "." + h3Teller + " " + alleTitels[i-1].innerText;
    h3Teller++;
  }
}

Ik had geen zin om in deze tekst alle titels (alleen die van niveau h2 en h3) manueel te nummeren. Als je dan een titel tussenvoegt, moet je alle nummers aanpassen. Dat kan gemakkelijk geautomatiseerd worden! De code selecteert alle h2 en h3 elementen in een collection met de naam alleTitels. Een lus gaat dan alle elementen in deze collection af en berekent het nummer (zowel h2- als h3-nummer) dat aan de titel moet voorafgaan. Met de eigenschap innerText wordt dan dit nummer vooraan toegevoegd aan de huidige titel.

Andere methoden

Er zijn nog heel wat andere manieren om elementen te selecteren, maar eigenlijk zijn ze niet nodig omdat ze allemaal vervangen zijn door querySelector() en querySelectorAll(). Je ziet ze af en toe nog wel in (oudere) scripts: getElementsByTagName(), getElementsByClassName() en getElementsByName().

Niet gebruiken!

Attributen en waarden

HTML-elementen kunnen meerdere attributen hebben. Een a element heeft een href attribuut. Een img heeft altijd een attribuut src. Voor styling kunnen class attributen handig zijn enz.

Enkele mogelijke methoden om attributen op te vragen, aan te passen, … van een element:

Een voorbeeld op het gekende HTML-document:

document.head.firstElementChild.hasAttribute("charset"); // geeft true
document.querySelector("html").getAttribute("lang") // geeft "nl"

Tekst in een element veranderen

Je merkte misschien al dat ons voorbeeld HTML-document een leeg element footer bevat. Het staat wel in de code, maar zonder inhoud. Inhoud toevoegen kan op verschillende manieren. We tonen er twee.

De eerste methode (innerText) is geschikt als je enkel tekst wilt toevoegen. Onderstaande code zorgt voor de tekst ‘© 2022’ in de footer:

document.querySelector("footer").innerText = "&copy; 2022"
Als de inhoud die je wilt toevoegen ook HTML-elementen bevat, moet je de innerHTML eigenschap gebruiken:
document.querySelector("footer").innerHTML = "tekst die <strong>belangrijk</strong> is"

Een element toevoegen / verwijderen

De nav in het voorbeeld HTML-document bevat drie links naar JS boeken of sites. Stel dat we een vierde bron willen toevoegen. Ik wil bvb. graag wat reclame maken voor mijn cursus algoritmisch denken (voor Toegepaste informatica) m.b.v. P5JS, te vinden op https://u0012047.webontwerp.ucll.be/algo/. Dit toevoegen aan de navigatie gaat in een aantal stappen.

Een nieuw element aanmaken

Het doel is dus om volgend HTML-element aan te maken met een bepaalde klasse en inhoud:

<li class="promo">
  <a href="https://u0012047.webontwerp.ucll.be/algo/">Algoritmisch denken m.b.v. P5JS</a>
</li>

Dat kan als volgt:

let algoLink = document.createElement('li');
// maak een nieuw li element en stop dat in variabele algoLink

algoLink.className = 'promo';
// geef dit element class="promo"

algoLink.innerHTML = '<a href="https://u0012047.webontwerp.ucll.be/algo/">Cursus Algo Ti</a>'
// vul dit li element met inhoud

Een element toevoegen aan het DOM

Het nieuwe element is aangemaakt, de klasse is toegekend en het element kreeg inhoud. Nu moet het op de juiste plaats aan het DOM toegevoegd worden. We maken hiervoor gebruik van de methode insertAdjacentElement (of de varianten insertAdjacentHTML en insertAdjacentText).

De methode huidigEl.insertAdjacentElement(plaats, element) verwacht twee parameters:

Hier willen we deze nieuwe vierde navigatielink toevoegen aan de ul die in nav staat, en wel als laatste item. De keuze voor 'beforeend' ligt dus voor de hand:

let navLijst = document.querySelector('nav ul');
// pak het referentie element vast en geeft het een naam

navLijst.insertAdjacentElement('beforeend', algoLink);
// voeg het nieuwe li element op het einde van de ul toe

Een knoop verwijderen

Je verwijdert een knoop (node) met de methode remove(). We geven een klein voorbeeldje: verwijder de tweede li in het nav element.

document.querySelector('nav li:nth-child(2)').remove()
// de li die een tweede kind is van een ander element en
// in het element nav staat (als nakomeling) wordt verwijderd

Style en class

Ik wil de titel h2 in een andere kleur met een gewijzigde achtergrond weergeven. De voorkeursmethode zou dan zijn om dat in het extern CSS-bestand aan te passen. Het kan echter ook in het style attribuut van dit element h2. Hopelijk weet je ondertussen dat je dan altijd kiest voor een wijziging in het extern stijlbestand! JS heeft eveneens twee manieren om stijl te veranderen die met het bovenstaande te vergelijken zijn: style en class toevoegen. Je kiest indien mogelijk altijd voor de tweede mogelijkheid.

De style eigenschap in JS: zelden gebruiken!

Je weet ondertussen hoe het gaat? Pak het juiste element vast en wijzig dan een eigenschap ervan. Ondanks het feit dat dit niet de voorkeursmethode is , laat ik toch even zien hoe het kan:

let titel2 = document.querySelector(h2);
// er is maar één h2, dus eenvoudige selectie

titel2.style.color = '#fff';
// tekstkleur wordt wit

titel2.style.backgroundColor = '#333';
// achtergrondkleur donkergrijs, let op schrijfwijze backgroundColor

titel2.style.padding = '10px';
// tekst kwam wat te dicht tegen de rand, dus voorzie wat padding

Merk op dat de CSS-eigenschap background-color heet. Het JS-equivalent laat altijd het streepje weg en gebruikt ‘camel case’: backgroundColor.

Klasses toevoegen: voorkeursmethode

De h2 aanpassen kan veel beter in het CSS-bestand gebeuren. We voegen eerst volgende code toe aan het CSS-bestand. Denk ook even na over een zinvolle klassenaam!

h2.donker {
  color: #fff;
  background-color: #333;
  padding: 10px;
}

De stijlaanpassing zit dus in het document waar ze thuis hoort: het CSS-bestand. Nu moeten we enkel nog de juiste klasse toevoegen aan het element:

let titel2 = document.querySelector('h2');
titel2.classList.add('donker');
titel2.classList.contains('donker') // geeft true als resultaat

De eigenschap classList heeft volgende methoden: