
Creating and destroying content
The main strength of the DOM is the option not only to read, but also to alter the content and the structure of the document at hand. For this we have several methods at our disposal.
Creating new content
createElement(element)
- Creates a new element
createTextNode(string)
- Creates a new text node with the value
string
.
Newly created elements are not added to the document immediately, they
remain in limbo until we append them somewhere in the node tree. These functions
have to be applied to the document object
rather than to a node
.
Javascript: mynewparagraph=document.createElement('p'); mynewtext=document.createTextNode('this is a new paragraph');
Altering the existing content
setAttribute(attribute,value)
- Adds a new
attribute
with thevalue
to the object appendChild(child)
- Adds
child
as achildNode
to theobject
.child
needs to be anobject
, you cannot use astring
. cloneNode()
- Copies the whole
node
with allchildNodes
. hasChildNodes()
- Checks if an object has
childNodes
, and returnstrue
if that is the case. insertBefore(newchild,oldchild)
- Adds
newchild
beforeoldchild
to the document tree. removeChild(oldchild)
- Removes the childnode
oldchild
. replaceChild(newchild,oldchild)
- Replaces
oldchild
withnewchild
. removeAttribute(attribute)
- Removes the
attribute
from the object.
The picture example
Let's say we have links to images, and they should open in a new window in browsers without Javascript, or below the links when Javascript is available.
HTML: <ul id="imglist"> <li><a href="home.gif" target="_blank">Home (new window)</a></li> <li><a href="home_on.gif" target="_blank">Home active (new window)</a></li> <li><a href="jscsshtml.gif" target="_blank">HTML-CSS-Javascript (new window)</a></li> </ul>
Now, when Javascript and DOM is available, we want to:
- Get rid of the "(new window)" wording in the links.
- Add an event handler to call a function
popw()
This function should
- Show the linked image below the link when it is not there already.
- Remove the image, if it is already there (to avoid the link to add the image over and over again).
- Make the image disappear when users click on it.
The first problem is not hard to solve:
Javascript: function imgpop() { var il,imga,imgatxt; // get all LIs in imagelist, loop over them il=document.getElementById('imglist').getElementsByTagName('li'); for(i=0;i<il.length;i++) { // grab first link in the LI imga=il[i].getElementsByTagName('a')[0]; // delete the wording (new window) in the link text // (which is the nodeValue of the first node) imgatxt=imga.firstChild; imgatxt.nodeValue=imgatxt.nodeValue.replace(/ \(new window\)/,''); // add the event handlers to call popw(); imga.onclick=function(){return popw(this);} //imga.onkeypress=function(){return popw(this);} } }
Now, for the function popw()
we need to use some of the
methods shown above:
Javascript: function popw(o) { var newimg; // if there is already an image in the parentNode (li) if(o.parentNode.getElementsByTagName('img').length>0) { // delete it o.parentNode.removeChild(o.parentNode.getElementsByTagName('img')[0]); } else { // else, create a new image and add a handler that removes it when you // click it newimg=document.createElement('img'); newimg.style.display='block'; newimg.onclick=function(){this.parentNode.removeChild(this);}; newimg.src=o.href; o.parentNode.appendChild(newimg) } return false; } See this imagepop example in action
The datepicker link example
Let's say for example we have a form that has date fields and we want to offer users with Javascript enabled a date picker, others should simply enter the date by hand. Let's not discuss the date picker function now, but focus on how to call it.
We start with the necessary HTML.
To see which element should get a datepicker link, we add classes with the name date
to them.
HTML: <h1>Flight booking</h1> <form action="nosend.php" method="post" onsubmit="return check(this);"> <p>Step 1 of 4</p> <h2>Please select your dates</h2> <p> <label for="startdate">Start Date</label> <input type="text" class="date" id="startdate" name="startdate" /> </p> <p> <label for="enddate34;>End Date</label> <input type="text" class="date" id="enddate" name="enddate" /> </p> <p> <input type="submit" value="send" /> </p> </form>
We loop through all input
s in the document
, and check which one has a
className
that contains date
(remember, elements can have more than one class
in the class attribute!).
If that is the case, we create a new link, and a link text. We append the link text as a child of the link and add event handlers to call our picker script.
Once the link is created, we append it after the input field.
Javascript: function addPickerLink() { var inputs,pickLink,pickText; // loop through all inputs inputs=document.getElementsByTagName('input'); for(i=0;i<inputs.length;i++) { // if the class contains 'date' if(/date/.test(inputs[i].className)) { // create a new link and a text pickLink=document.createElement('a'); pickText=document.createTextNode('pick a date'); // add the text as a child of the link pickLink.appendChild(pickText); // set the href to # and call picker when clicked or tabbed to pickLink.setAttribute('href','#'); pickLink.onclick=function(){picker(this);return false;}; //pickLink.onkeypress=function(){picker(this);return false;}; // add the new link to the parent of the input field (the P) inputs[i].parentNode.appendChild(pickLink) } } } See the picker demo in action.
Now all fields with dates have a link after them that links to picker()
.
All we need now is to tell the picker function where to apply the return value to.
As we send the link itself as the object to picker()
, we need to access its
previous sibling, the INPUT
.
Javascript: function picker(o) { alert('This is a simulation only.') // no real function today o.previousSibling.value='26/04/1975'; }
Close, but not quite. As we appended the new link as the last child to the
parent node of the input, it could very well be that the previousSibling
of
our new link is indeed not the INPUT
but whitespace! Therefore, we need to
loop through the previous siblings until we hit an element.
Javascript: function picker(o) { alert('This is a simulation only.') // no real function today while(o.previousSibling.nodeType!=1) { o=o.previousSibling; } o.previousSibling.value='26/04/1975'; }
Looping is always hacky and could be rather slow. To avoid the loop we have to change our function.
Changing the addPickerLink()
function
It is always easy to use appendChild()
, but it makes us dependent
on the markup. What happens for example if we need to add a SPAN
with an asterisk next to the input
to indicate a mandatory field later on?
The trick is use insertBefore()
on the nextSibling
of our input field.
Javascript: function addPickerLink() { var inputs,pickLink,pickText; // loop through all inputs inputs=document.getElementsByTagName('input'); for(i=0;i<inputs.length;i++) { // if the class contains 'date' if(/date/.test(inputs[i].className)) { // create a new link and a text pickLink=document.createElement('a'); pickText=document.createTextNode('pick a date'); // add the text as a child of the link pickLink.appendChild(pickText); // set the href to # and call picker when clicked or tabbed to pickLink.setAttribute('href','#'); pickLink.onclick=function(){picker(this)}; //pickLink.onkeypress=function(){picker(this)}; // add the new link directly after the inputinputs[i].parentNode.appendChild(pickLink)inputs[i].parentNode.insertBefore(pickLink,inputs[i].nextSibling); } } } See the cleaner picker demo in action.
Things to remember
That's about it, with these tools we are able to access and alter any element of a document, and can enhance the user's experience without becoming dependent on Javascript.
It might be a bit confusing at first, but once you got your head around the DOM a bit, it gets easier every time you use it.
Some common obstacles are:
- Ensure to check for elements before you try to access them. Some browsers
are happy to check for
object.nextSibling.nodeName
and returnfalse
when there is no next sibling or if it is a text node, others however throw an error stating that you try to access an attribute of a non-existant element. - Make sure not to rely on the markup too much, as linebreaks might be read as a new node, or the markup might change, and you don't want to change your script every time the markup changes.
- Reading the content of an element is done by reading the values of its
childNodes
, not the value of the element itself!document.getElementsByTagName('h2')[0].nodeValue
is empty,document.getElementsByTagName('h2')[0].firstChild.nodeValue
isn't. - When checking for
nodeNames
andattributes
, make
sure to stay case insensitive, as some browsers render elements as uppercase, others as lowercase. - DOM generated HTML is in most of the cases not well-formed, if you want to reuse HTML from a browser-generated page, you'd have to tidy it up.
- Avoid too much looping, if you have the chance to create the markup you need to work with, go for IDs instead.
- Know your syntax. Many a time a
getElementsById
can cause a lot of rescripting. - Know your Javascript objects and HTML attributes, it is no use checking for an attribute that is not meant to be there to start with.
- Don't assume your own ways of marking up to be commonly used. An example
is to check if the
className
contains your string rather than is your string, as some developers like using multiple classes.
What about innerHTML?
When Internet Explorer 4 came to be, innerHTML
was born, a fast
way to generate and change content. It is a way to read the content of an element a
lot easier than with the original W3C
recommendations. This is especially the case, if an element contains
childNodes
which are elements themselves and we want to read the
whole content. To do this with DOM
only, you need to go through
excrutiating exercises[1]
of checking nodeTypes
and reading the values of each
child node. innerHTML
is a lot easier to use, but has some
drawbacks. For example you do not get any references back to the elements you
create with it as the whole value is a string rather than objects. Furthermore,
innerHTML
is only HTML
related, not XML, and
DOM was meant to traverse any
markup. For a comparison and browser support check the
DOM section of
Quirksmode.org[2]
or the big discussion at
Developer-x[3].
Links
- [1] innerHTML for mozilla http://www.webfx.nu/dhtml/mozInnerHTML/mozInnerHtml.html
- [2] Excellent DOM support charts and comparison with innerHTML http://www.quirksmode.org
- [3] DOM vs. innerHTML http://www.developer-x.com/content/innerhtml/