Traversering og manipulering av DOM
Vi er jo tross alt på nett, og et grensesnitt mot HTML-dokumentene våre er helt essensielt for å gjøre noe interessant med JavaScript. Velkommen til julekalenderens DOM-primer.
I dagens artikkel gir jeg en rask oversikt over hvilke metoder som er tilgjengelig for å jobbe med de underliggende HTML-dokumentene. Ofte bruker man kanskje et rammeverk, og i de tilfellene vil mye av funksjonaliteten jeg viser her være abstrahert bort i rammeverkets egne funksjoner. Hensikten her er å få en forståelse for de underliggende konseptene, og så kan man lære seg og bruke hvilket rammeverk man vil. Jeg kommer nesten ikke til å snakke om rammeverk idag.
Kort om DOM
DOM (Document object model) er en plattform- og språk-uavhengig API-spesifikasjon fra W3C. Den definerer (i 4 nivåer: DOM0, DOM1, DOM2 og DOM3) et programmeringsgrensenitt mot dokumenter.
I JavaScript er det sånn at nettleseren implementerer DOM-en og tilgjengeliggjør den for scriptmotoren. JavaScript selv har ingenting med DOM å gjøre - DOM er et API som nettleseren gjør tilgjengelig for språket, etter W3Cs spesifikasjoner. Som alltid er det litt hipp som happ hvordan nettleserprodusentene tolker spesifikasjonene, så alt er ikke så nyttig som det opprinnelig var ment.
DOM-teamet hos W3C er ikke lengre aktivt. Nå for tiden drives slike API-er videre av andre grupper hos W3C, som for eksempel den nye HTML-spesifikasjonen (som definerer både markup og API-er).
Traversere dokumentet
Å traversere dokumentet er en operasjon i to steg: først må du finne noen elementer, deretter kan du om ønskelig navigere videre fra disse.
Finne elementer
Det enkleste objektet å få tak i er dokumentet selv. Dette er alltid tilgjengelig gjennom document.
Skal du ha tak i noen andre elementer er det hovedsakelig to måter å finne dem på:
-
getElementById(id) -
getElementsByTagName(tagName)
(Merk hovedsakelig, det er også andre måter å finne elementer, som eksempelvis document.body for body-elementet).
Den første finner ett eller ingen elementer ved angitt id. Den andre returnerer en NodeList med elementer som matcher tagnavnet. Tagnavnet kan være '*' dersom du ønsker å finne alle elementer.
Disse to metodene kan vi bruke til å lete på dokumentet som helhet, eller på subtrær (angitt av en lokal rotnode).
- For å lete i hele dokumentet kalles disse på
document-objektet (document.getElementById("nav")). - Lokal filtrering/leting kan oppnås fra et vilkårlig element (
document.getElementById("nav").getElementsByTagName("li");).
Traversering fra en node
Noen vanlige metoder for å navigere fra et gitt element:
-
parentNode- peker til foreldrenoden -
childNodes- array med undernoder -
firstChild- peker til første undernode -
nextSibling- peker til nest søskennode
La oss anta følgende HTML:
<h1>Navigering i DOM</h1>
<div class="section" id="section1">
<h2>Finne elementer</h2>
<p>
Det er mange måter å finne elementer på [...]
</p>
</div>
<div class="section" id="section2">
<h2>Traversering</h2>
<p>
Det er mange måter å traversere [...]
</p>
</div>
Så har vi følgende eksempler på bruk av DOM (eksemplifisert via et YUI Test-testcase):
var assert = YAHOO.util.Assert;
// Referanse-elementer
var section1 = document.getElementById("section1");
var section2 = document.getElementById("section2");
var section1H3 = section1.getElementsByTagName("h3")[0];
var sample = document.getElementById("sample");
// Hente elementer
assert.areEqual(1, document.getElementsByTagName("h2").length,
"Should be one h2 element");
// Foreldrenode
assert.areEqual(sample, section1.parentNode,
"#section1 should be child of #sample");
// Barnenoder
assert.areEqual(section1H3, section1.firstChild,
"First child of section1 should be <h3>");
assert.isInstanceOf(NodeList, section2.childNodes,
"childNodes should be NodeList");
// Søskenelementer
assert.areEqual(section2, section1.nextSibling,
"#section2 should be next sibling of #section1");
Desverre får vi ikke grønt lys på alle testene her. YUI rapporterer om følgende:
testChildNodes: First child of section1 should be <h3>
Expected: [object HTMLHeadingElement] (object)
Actual:[object Text] (object)
(og en til i samme gata). Altså ventet vi et h3-element, men fikk et Text-objekt.
Node vs Element
Problemet er at metodene nevnt ovenfor traverserer nodetreet, og dette består ikke kun av elementer, men for eksempel også tekst-noder ("innholdet" i elementene, men også linjeskift osv), kommentarer og mer. For å jobbe rundt dette kan vi "bla" videre til vi møter på et element, som har nodeType 1. La oss anta en nextSiblingElement(node)-funksjon:
var assert = YAHOO.util.Assert;
// Referanse-elementer
var section1 = document.getElementById("section1");
var section2 = document.getElementById("section2");
// Søskenelementer
assert.areEqual(section2, nextSiblingElement(section1),
"#section2 should be next sibling of #section1");
Fortsatt ingen hell - la oss implementere metoden:
function nextSiblingElement(node) {
do {
node = node.nextSibling;
} while (node.nodeType !== 1);
return node;
}
Denne funksjonen begynner med å hente neste søskennode. Dersom den ikke har nodeType 1 fortsetter vi videre til neste node.
Og dermed får vi grønt lys på testen som går på barneelementer. firstChild-testen feiler fortsatt, men vi kunne godt ha løst den på tilsvarende måte:
function firstChildElement(node) {
node = node.firstChild;
if (node.nodeType !== 1) {
node = nextSiblingElement(node);
}
return node;
}
jQuery er et eksempel på rammeverk som tilbyr svært gode muligheter for å navigere rundt i dokumentet uten å kræsje i diverse tekstnoder osv.
Attributter
Det er også mulig å lese attributtene til elementene vi henter ut. DOM definerer getAttribute(attrName), men i JavaScript oppnår vi det vi ønsker ved å lese attributtene som om de var properties på objektene: document.getElementByTagName("img")[0].src.
Traverser med omhu
Husk at det ikke er gratis å kalle metoder som document.getElementsByTagName, så istedet for
for (var i = 0; i < document.getElementsByTagName("a").length; i++) {
// Gjør noe med linken i document.getElementsByTagName("a")[i];
}
bør du heller gå for
var elements = document.getElementsByTagName("a");
for (var i = 0; i < elements.length; i++) {
// Gjør noe med linken i elements[i];
}
elements = null;
...som er langt mer effektivt (hvor mye avhenger av antall aksesser og antall elementer - selv små eksempler gir 15% forskjell). Den siste linja i eksempel 2 unngår minnelekkasjer i IE. Sistnevnte skal vi se mer på senere i desember.
Manipulere dokumentet
"Skrive HTML" med JavaScript
Den eldste måten å oppdatere dokumentet på er document.write(markup);. Denne skriver markup direkte inn i dokumentet på plasseringen der uttrykket evalueres. I (skikkelig) XHTML er ikke denne tillatt fordi den glatt kan få et velformet dokument til å ikke lengre være velformet. Jeg vil ikke anbefale denne.
Litt ryddigere
Litt bedre er det å bruke innerHTML, en egenskap som er tilgjengelig på alle elementer. Den er i praksis det samme som ovenfornevnte idet den bare skriver vilkårlig markup til dokumentet (heller ikke tillatt i XHTML), men til å oppdatere feks teksten i et element er den akkurat passe:
var heading = document.getElementsByTagName("h1")[0];
heading.innerHTML = "Ny overskrift";
Sette inn elementer med DOM
DOM tilbyr også et API for å fjerne, flytte og lage nye noder:
var heading = document.getElementsByTagName("h1")[0];
// Fjern alle barnenoder
while (heading.childNodes.length > 0) {
heading.removeChild(heading.firstChild);
}
var span = document.createElement("span");
var text = document.createTextNode("Ny heading");
span.appendChild(text);
heading.appendChild(span);
// Resultat: <h1><span>Ny heading</span></h1>
Ganske "verbose", men det funker.
Endre attributter
Attributter kan endres via setAttribute(attrName, value), men som nevnt ovenfor kan du også bare skrive til attributtene direkte på elementobjektene.
Kraftigere spørremetoder
Mange rammeverk har også støtte for å spørre dokumentet etter elementer ved hjelp av CSS (1, 2 og 3)-selectorer. Dette er også standardisert for nettlesere, men er foreløpig ikke tilgjengelig i alle nettlesere. I støttende nettlesere kan vi benytte querySelector(selector) (når vi ønsker ett eneste element) og querySelectorAll(selector) (når vi vil ha alle som passer).
Disse metodene er landet i Safari og Opera, og kommer i Firefox 3.1 og IE8.
Det er mer til DOM enn hva vi har sett her, men forhåpentligvis har vi blitt kjent med de grunnleggende konseptene. For de nysgjerrige finnes det masse stoff å fordype seg i:
Vi kjører en tilsvarende gjennomgang av events i morgen, vel møtt!
Vel møtt til en prat om funksjoner og parametere i morgendagens artikkel!
Oppdatering: Events bytter plass med funksjoner og parametere. Med andre ord; events på tirsdag.
Kommentarer
Jørund Ruud
7. desember, 21:33
Christian
7. desember, 21:46