DOM manipulation

Now that we know how to get to the various elements that make up a web page, it's time to have a look at what we can actually do with them. There are a few basic operations available to us in which we can manipulate a page:

  • We can remove, insert and move elements around in the DOM to change their order.
  • We can change an element's contents
  • We can change an element's attributes

We're going to continue using the HTML example from the previous page explaining the DOM:

 1 <html>
 2   <head>
 3     <title>Book Club</title>
 4   </head>
 5   <body>
 6     <h1>My books</h1>
 7 
 8     <ul class="books">
 9       <li class="book unread">War and Peace</li>
10       <li class="book unread">The Origin of the Species</li>
11       <li class="book read">101 Really Funny Jokes</li>
12     </ul>
13   </body>
14 </html>

Removing and inserting elements

Let's say we wanted to insert a new book in the list. This is pretty straigh-forward: We need to get the list element containing all the books, which is the <ul class="books"> element from the HTML, and then we need to create a new list element with the title of the book and then insert the new element after the existing ones.

1 //Get the <ul> containing the books
2 var booksContainer = $$('ul.books')[0];
3 
4 //Insert a new element at the bottom of the "container" 
5 booksContainer.insert({bottom: '<li class="book unread">History of the World</li>'});

That was simple, wasn't it? We're using the Prototype library here, which really cuts down on the amount of work we have to do, but we can expand the example above a little bit so we can see more clearly what's happening:

1 var booksContainer = $$('ul.books')[0];
2 
3 var newBook = new Element('li', {'class': 'book unread'});
4 newBook.update('History of the World');
5 
6 booksContainer.insert({bottom: newBook});

Now we can see that we're actually creating a new element with a tag name of "li" and a class attribute of "book unread". After creating the element, we're using the update method to change the text inside the element to the title of the book. When all that's done, we finally insert the element at the bottom of the container element. This is basically the same as the previous example, it's just that Prototype created the element for us when we gave it a chunk of HTML representing the element.

Now let's say we want to remove a book from the list. In order to do that we just need to get the element and execute the remove method on it. Let's remove the second book in the list, "The Origin of the Species".

1 //First, get all the books
2 var books = $$('ul.books li.book');
3 
4 //Then, get the second book from the array (remember, we start counting at 0, so its position is 1)
5 var originOfTheSpecies = books[1];
6 
7 //Then, remove it
8 originOfTheSpecies.remove();

That was easy. Let's have a book burning, just for the fun of it:

 1 //Creating a function allows us to burn books whenever we want with little effort
 2 function burn(books){
 3   books.each(function(book){
 4     book.remove();
 5   });
 6 };
 7 
 8 var books = $$('ul.books li.book');
 9 books.pop(); //Let's remove the last book from the array, I doubt anyone's going to be learning anything from those jokes
10 
11 burn(books);

If you look inside the burn function, you'll notice something that might seem a little weird. It's a function inside a function! This is actually a pretty common sight in JavaScript code, as functions are at the core of the language and very practical and easy to deal with. In this case, we're passing a function as a parameter to the each method of the books array. What happens is that the function is being run once for each item in the array, and gets each book element in turn as its parameter. We're using the each method instead of a for loop.

Changing an element's contents

Let's imagine there's a new edition of the hilarious "101 Really Funny Jokes" called "102 Extremely Funny Jokes" and we want to change the title. That is, we want to change the content of the <li> element for the book. Using Prototype, that's pretty easy:

1 var book = $$('ul.books li.book')[2]; //Get the book directly from the array
2 book.update("102 Extremely Funny Jokes")

The update method can be used on any element to change its contents. Beware though that "changing an element's contents" means replacing everything inside it, including any child elements. What the update method actually does is to remove all elements inside the parent element, create new elements and insert them in their place. This means that you can use HTML, and Prototype will know how to create the right elements from it:

1 var books = $$('ul.books')[0];
2 
3 //Replace all the books with two books about Ruby and Rails
4 books.update('<li class="book read">Programming Ruby</li><li class="book read">Agile Web Development with Rails</li>');

Changing an element's attributes

So far, our elements have had only one attribute, the class attribute. For a JavaScript developer, this is the most useful attribute available to us, and we won't even bother looking at the other attributes most of the time. But let's imagine our book list had links (<a> elements) to each book:

1 <ul class="books">
2   <li class="book unread"><a href="/books/1">War and Peace</a></li>
3   <li class="book unread"><a href="/books/2">The Origin of the Species</a></li>
4   <li class="book read"><a href="/books/3">101 Really Funny Jokes</a></li>
5 </ul>

If we wanted to change the link to the "101 Really Funny Jokes" book to point to the books home page, http://www.101reallyfunnyjokes.com/, we could use Prototype's writeAttribute method:

1 var book = $$('ul.books li.book')[2];
2 book.writeAttribute('href', 'http://www.101reallyfunnyjokes.com/');

Easy! As you'd imagine, there's a complementary readAttribute method which will give you back the current contents of the attribute:

1 var book = $$('ul.books li.book')[2];
2 alert(book.readAttribute('href')); //Alerts "/books/3" (or the new link if this is done after updating it)

Like with any other attribute, we can change the class of an element using writeAttribute:

1 var book = $$('ul.books li.book')[2];
2 book.writeAttribute('class', 'book unread'); //Change the "read" class to be "unread"

The class attribute is very practical because it can contain the current state of an element. In our books example, we can determine from the class attribute that an element is in fact a book by having the "book" class, and if it's been read or not by having the "read" or "unread" classes. An element can have as many classes as we like; we just separate them with spaces. Because of this, it becomes a bit of a hassle keeping track of them all: If we had an element with 10 class names and we wanted to change just one of them, we'd have to read the entire class attribute, then change just that bit of the string containing the target class name and write the changed string back to the class attribute. Luckily, because working with class names is so common, Prototype has a few custom methods for this:

  • addClassName('foo') - Add the "foo" class name to the class attribute
  • removeClassName('foo') - Remove the "foo" class name from the class attribute
  • hasClassName('foo') - Returns true if the element has the "foo" class name, false if not
  • toggleClassName('foo') - If the element already has the "foo" class name, this will remove it. If not, it will add the class.

Using these methods, our example from above where we replace the "read" class with an "unread" class could be rewritten as follows:

1 var book = $$('ul.books li.book')[2];
2 book.removeClassName('class', 'read');
3 book.addClassName('class', 'unread');

Wait, that's three lines instead of two, how's that better? Yes, it's slightly longer, but the goal of programming isn't to reduce the amount of lines we write to accomplish a goal, it's to increase clarity. With this new version, it's very easy to see exactly what the code is doing: It's removing one class and inserting another. Another significant improvement to the code is that we've reduced coupling: The previous code was flawed because we can't know at any given time what class names an element will have, so if the element actually has a class attribute of "book unread good" and we replace that with "book read", the "good" attribute will just vanish into thin air. We've also quite possibly reduced the amount of future work needed to maintain the code because we no longer assume that we're using the "book" class to identify the elements in the list.

We could simplify our logic a bit more. There's a bit of redundancy in using both the "read" and "unread" class names: This means that whenever we want to add or remove one we have to make sure we remove or add the other - if we want to add the "read" class we have to also remove the "unread" class. Couldn't we just say that "if a book element has the 'read' class name, it's marked as read and if not it's unread"? Sure we can, and by doing that we've removed another little bit of coupling and our example becomes one line shorter again:

1 var book = $$('ul.books li.book')[2];
2 book.removeClassName('class', 'read');

Also available in: HTML TXT