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:
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.
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:
-
element.hasAttribute(naam)
: heeft dit element het gevraagde attribuut ‘naam’? -
element.getAttribute(naam)
: geef de waarde van het attribuut ‘naam’ van dit element. -
element.setAttribute(naam, waarde)
: ken de waarde toe aan het attribuut. -
element.removeAttribute(naam)
: verwijder attribuut ‘naam’.
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 = "© 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:
-
plaats: waar wordt het nieuwe element toegevoegd gezien t.o.v. huidigEl?
Er zijn vier mogelijkheden:
-
'beforebegin'
: voeg element vlak voor huidigEl toe; -
'afterbegin'
: voeg element in huidigEl in, helemaal in het begin; -
'beforeend'
: voeg element in huidigEl in, helemaal op het einde; 'afterend'
: voeg element vlak na huidigEl in.
-
- element: het nieuwe toe te voegen element (dat we al klaargemaakt hebben in de vorig stap).
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:
-
add() / remove()
: voegt de klasse toe of verwijdert ze; -
toggle()
: bijzonder nuttig, voegt de klasse toe als ze nog niet bestaat, anders verwijdert ze; contains()
: bevat dit de klasse, geeft true / false.