Dynamic DOM
We know from our earlier work with jQuery that one way to manipulate the DOM is to dynamically add elements, though we haven't done so in an assignment yet. Now is the time.
Dynamically added elements pose a problem for event handling. We might want to handle events that happen to dynamically added elements. There is a feature called event bubbling that not only makes this easy, it makes event-handling more efficient.
This reading is about
- dynamically constructing and adding DOM elements
- delegated event handlers
- Constructing DOM Elements
- The AppendTo Method
- Attributes
- Pitfalls When Creating HTML
- Nested Structures
- Dynamic Grocery List
- Construction
- Adding Elements
- Event Delegation Motivation
- Event Delegation Pattern
- Event Delegation and jQuery
- Event.target
- Searching up the tree with .closest
- Searching down the tree with .find
- Removing from a List
- Removing from the DOM
- Cloning
- Templates
- Hiding the DIV Template
- The Cloning Technique
- Creating an Item By Cloning
- No IDs
- Summary
Constructing DOM Elements¶
If we think of the document as a living tree, what we are doing is grafting a new branch, complete with twigs and leaves, onto the tree.
Let's start simple though, by adding a single item to a list of
items. To create an LI
element, we can use jQuery, supplying the tag
in angle brackets:
var elt = $("<li>"); // a new LI element
The line of code above creates an LI element, but the element is not on the page anyplace. I think of this as being off-stage, like props in a play.
(Aside: note the difference between the above code, which creates a new LI element, from the following code, which matches all LI elements that are currently on the page. The angle brackets are a small bit of syntax with a big impact.)
var li_elements = $("li"); // all current LI elements
Once we have a new LI element, we can modify it using jQuery, using the same techniques and methods we've used before, but now working with this unattached DOM element in a variable. For example, we can set the text inside the element:
var elt = $("<li>"); // a new LI element
elt.text("milk");
Finally, we can append that element to the page. Supposing that the
element #groceries
exists on the page, here's one way to add our new
item to the list:
var elt = $("<li>"); // a new LI element
elt.text("milk");
$("#groceries").append(elt);
Here's an example in action:
The AppendTo Method¶
An alternative to using append
is to use appendTo
, which reverses the roles. So instead of:
$("#groceries").append(elt);
we can say
elt.appendTo("#groceries");
So what? Well, the alternative can be nice if you are a fan of chaining and concise coding. So we could do:
var elt = $("<li>"); // a new LI element
elt.text("milk").appendTo("#groceries");
Or even:
$("<li>").text("milk").appendTo("#groceries");
Which should you use? You can use either technique. You should use whichever you feel comfortable with. But it's nice to know about these alternatives.
Attributes¶
One common thing we do is to add attributes to the new element. With
li
we might add a class, but otherwise they don't normally have a
lot of attributes. (I recommend avoiding IDs for dynamically created
elements.) But radio buttons have several attributes.
You can, of course, use the .attr
method in the normal way. Like this:
var box1 = $('<input>').attr('type','radio').attr('value','wzly');
or, equivalently,
var box1 = $('<input>')
.attr('type','radio')
.attr('value','wzly');
That works fine.
An alternative is to add a second argument to the jQuery function that is a JS object literal (a dictionary) of attribute/value pairs. Like this:
var box1 = $('<input>', {'type':'radio',
'value':'wzly'});
That's just a little more succinct in many cases. But if you'd rather
use .attr
in your own code, that's fine too.
Pitfalls When Creating HTML¶
An alternative to the structured creation of DOM elements described above is to put a pile of HTML into a string and hand it to jQuery to parse and build DOM elements out of. I don't recommend it, but let's see it first.
Here are two examples:
var box1 = $("<input type='radio' value='wzly'>");
var li2 = $("<li class='optional'">candy</li>");
That seems pretty nice and easy, but I discourage it for these reasons:
- it has embedded quotation marks, which can get ugly when strings get longer and more complicated
- it's much harder to do when you have variables that you want to use in the string (say the "value" attribute is in a variable)
- it can't use the two-argument version of jQuery, where we specify attribute/value pairs
- it can't be easily combined with the other jQuery methods like
.attr
and.text
While this can be made to work, I think it's better to use a more structured approach.
Nested Structures¶
So far, we've looked at building a simple li
and adding it to the
page. What about more complex, nested structures? For example, the
radio button input is just a little circular widget like this: . It needs to have some associated text, and both the
text and the checkbox need to be children of a label
. Like this:
<label>
<input type="radio" name="station" value="wzly">
WZLY 91.5 FM
</label>
How could we build that? All we have to do is build three things:
the label
, the radio button and a string, and append them together
the right way. We can then append the result to where we want it. Like
this:
var label = $("<label>");
var box1 = $('<input>', {'type':'radio',
'name':'station',
'value':'wzly'});
var text = "WZLY 91.5 FM";
label.append(box1)
.append(text)
.appendTo('#my_form');
Here it is in action:
Dynamic Grocery List¶
Let's start with a demonstration and then we'll dig into the concepts that it exemplifies:
Add a few items to the list. Try clicking on the "done" buttons. We would use the "done" button to delete something from the list when we have bought it. (Maybe the button should say "bought".)
This example uses the techniques above to dynamically build list items when we add items to the list. The HTML for the list might look like this:
<ol id="groceries">
<li><p><button data-role="done">done</button>
<span class="amount">4</span> of
<span class="item">apples</span></p></li>
<li><p><button data-role="done">done</button>
<span class="amount">1</span> of
<span class="item">bananas</span></p></li>
<li><p><button data-role="done">done</button>
<span class="amount">lots</span> of
<span class="item">chocolate</span></p></li>
</ol>
There is also an underlying data structure, a list of dictionaries, like this:
// initialized when the page loads
var groceries = [{item: 'apples', amount: 4},
{item: 'bananas', amount: 1},
{item: 'chocolate', amount: "lots"}];
We can imagine that this initial list comes from a server-based database, rather than being a constant.
Construction¶
Let's first look at the construction of the elements. When the HTML page loads, the grocery list looks like this:
<ol id="groceries"></ol>
The HTML is constructed from the data structure like this:
function addDomElement(item, amt) {
let li = $("<li>");
let p = $("<p>");
let spanItem = $("<span>", {class: "item"}).text(item);
let spanAmt = $("<span>", {class: "amount"}).text(amt);
let btn = $("<button>", {"data-role": "done"}).text("done");
p.append(btn, spanAmt, " of ", spanItem).appendTo(li);
li.appendTo("#groceries");
}
function initDOM() {
groceries.forEach( (elt) => addDomElement(elt.item, elt.amount));
}
initDOM();
The addDomElement
function constructs one li
element (and
descendant structure) from a single item
, amount
pair. It works by
building the various elements, saving them in local variables, and
then appending them all together in the correct way. Finally, it
appends the result to the #groceries
order list element. (Notice
the use of the .append
method with several arguments, which is
helpful brevity.)
Adding Elements¶
The form to add an item to the grocery list is pretty straightforward:
<form id="add-form">
<p><label>item <input type="text" name="item"></label>
<p><label>amount <input type="text" name="amount"></label>
<button type="button" id="add">add item</button></p>
</form>
The JS is fairly straightforward as well:
$("#add").one().click(function () {
let item = $("[name=item]").val();
let amt = $("[name=amount]").val();
console.log('add ', item, amt);
addItem(item, amt);
addDomElement(item, amt);
$("#add-form")[0].reset();
});
This function is attached to the button
in the form. The button is
not a submit button, so there's no need to prevent the default form
submission.
The function extracts the two pieces of information, uses the
addItem
function to add the item to the data structure and uses the
same addDomElement
function we saw earlier to add the item to the
page.
The addItem
function is straightforward. It just creates a
dictionary and pushes it onto the end of the list:
function addItem(item, amt) {
groceries.push({item: item, amount: amt});
}
Now, let's turn to the behavior of being able to delete items from the list. Do to that, we will use event delegation.
Event Delegation Motivation¶
What is event delegation? Why do it? In Ottergram, there were 5 or 10 pictures of otters, to which we attached an handler to each one, so 10 nearly identical event handlers. The event handlers were added when the page loads. This leads to two problems:
- it's wasteful of memory to create many nearly identical event handlers, and
- any elements that are dynamically added after the page loads don't get event handlers
The latter is particularly important with dynamic parts of a page, such as the grocery list. When the page loads, there are no grocery items, yet we are able to add a single event handler that works with them when they are added later. There could be 100 items on our list, and we still only have 1 event handler.
Event Delegation Pattern¶
Here's a screenshot of our DOM after the page has loaded:
With event delegation, we put the event handler on the ol#groceries
,
rather than on each of the li
elements. The screenshot shows that
the event handler is on the ol
, not the three li
elements. Because
of the way browsers work, clicking on an li
counts as clicking its
parent (the ol
), its grandparent, and each of its ancestors, in
turn, all the way to the body
element.
This is called event bubbling.
(One way to think about bubbling is to think about how, if we were to click on a map showing Wellesley college, clicking on the college also clicks on the town of Wellesley, the state of Massachusetts, the USA and North America, because each larger entity contains the smaller ones.)
Event bubbling solves both our problems: there's only one event handler, regardless of how long our grocery list is, and it works even for dynamically added grocery items.
Event Delegation and jQuery¶
jQuery allows us to do event delegation in an easy way. We just use
the .on
method that we used before, but we add an extra argument,
between the event argument and the function argument. So instead of:
$('sel').on('click',
function () { ... });
We do:
$('sel').one().on('click',
'descendant selector',
function () { ... });
(I'm going to use .one()
in these examples, to emphasize that we are
adding the event handler to exactly one element.)
Any descendant of sel
that matches descendant selector
will
delegate its click
events to the ancestor, which runs the given
function.
Look again how that works in the grocery example:
// a delegated click handler to delete any item
$("#groceries")
.one()
.on('click',
'button[data-role=done]',
function (event) {
let li = $(event.target).closest('li');
let item = $(li).find('.item').text();
console.log('remove '+ item);
removeItem(item);
$(li).remove();
});
Let's walk through this carefully.
- The
#groceries
just selects the element that we are adding the event handler to. We are not adding it to any of the LI or the buttons. In fact, that wouldn't work because there aren't any yet. Instead, we are adding the event handler to theol#groceries
element that is in the static HTML. - The
.one()
just makes sure we successfully selected one element, using my plug-in. - This says we are handling a
click
event. - This is where we say that the descendants we are interested in are
button
elements, in fact, buttons with adata-
attribute ofdata-role=done
. - This starts our event-handler function. It takes an event object as an argument.
- This uses
event.target
, which is the particular button that was clicked on, and then uses the jQuery.closest
method to climb up the DOM tree to find the first ancestor that is anli
. - This line searches down the tree from the
li
to find an element with the classitem
. It then pulls the text of the element out. - Log what we are removing, like
apples
- Use a separate function to remove the item from the data structure.
- Remove the
li
from the DOM.
We'll expand on some of these in the next sections.
Event.target¶
What is event.target
? Remember that event object that the browser
creates whenever an event occurs and is passed to our function as an
argument? Here's yet another thing that the event object does: it can
tell us the DOM element that got the event.
So, the event object:
- has a
preventDefault()
method - has a
target
property that tells us what DOM element got the event
Searching up the tree with .closest
¶
Sometimes, you want to search up the tree, from a particular
starting element, along its list of ancestors (but not off the list:
parent, grandparent, and great-grandparent, but not uncles,
great-aunts and the like). jQuery's .closest()
method is good for
that. For example, the following finds the closest ancestor that is a
DIV:
elt.closest('div')
The closest
method starts at the given node (elt
in the example
above) and goes from child to parent, up the ancestor chain, until
some ancestor matches the CSS selector that is the argument to
closest
. It stops at the first (closest) matching ancestor.
Searching down the tree with .find
¶
The complement of searching up the ancestor chain is searching down
the tree, from a particular starting element, among its descendants.
To do that, you can use jQuery's .find()
method:
elt.find('li');
That expression would find every LI
that is a descendant of
elt
. (Note the lack of symmetry here: .closest
finds one element
along an ancestor chain, while .find
finds all elements among all
the descendants.)
It's more efficient to search a small part of a document than to
search the whole thing. Furthermore, find
is necessary when
searching a branch that is not (yet) attached to the document. So,
find
is a useful method in jQuery.
Removing from a List¶
We should look briefly at the code to remove a grocery item from the data structure (a list of dictionaries). Here's the code:
function removeItem(item) {
let index = groceries.findIndex( (elt) => elt.item == item );
if( index != -1 ) {
groceries.splice(index, 1);
} else {
console.err('could not find', item);
}
}
Arrays have a findIndex
method that takes a callback function (like
the map
method). Here the callback function just checks whether the
item
property of the array element (a dictionary) matches the item
we are looking for. The method returns the index in the array. We can
then use the splice
method to remove it from the array.
Removing from the DOM¶
We can remove something from the page, pruning it and all its
descendants, by using jQuery's remove
method.
Cloning¶
Here's another dynamic grocery list that works exactly the same except for the dynamic construction of the DOM elements:
dynamic groceries with cloning
In the earlier version, we used jQuery to build each element, add
attributes, and using the jQuery .append()
method to combine them
all.
That works well for small bits of structure but it doesn't scale. At some point, if you're building a lot of structure, there's a better way, namely cloning.
Of course, to best motivate cloning over piecemeal building of structure, I should have an example with a huge amount of HTML. But that's annoying and difficult to read, so I'll keep the HTML the same as what we have now, which is modest in size, and you should check that these techniques are no harder if the HTML is a lot bigger.
Here's an example of the grocery list with a few items on it, repeating our earlier example:
<ol id="groceries">
<li><p><button data-role="done">done</button>
<span class="amount">4</span> of
<span class="item">apples</span></p></li>
<li><p><button data-role="done">done</button>
<span class="amount">1</span> of
<span class="item">bananas</span></p></li>
<li><p><button data-role="done">done</button>
<span class="amount">lots</span> of
<span class="item">chocolate</span></p></li>
</ol>
As you can see, each item's structure is the same, varying only in
the contents of the two span
elements.
Templates¶
We can write out a template of the structure in our static HTML page, and then copy or clone the template, replacing the parts that need to change, and appending the copy to our list.
(Indeed, the template tag that was added to the HTML language for the purpose of templating like we will do. It holds structure that will not be rendered. Unfortunately, the fact that it's never rendered makes debugging harder, so I use the idea but not the tag. Instead, I use CSS to make the template invisible when I'm ready. )
In the example above, the template looks like this:
<ul id="grocery-template" class="template">
<li><p><button data-role="done">done</button>
<span class="amount">amount</span> of
<span class="item">item</span></p></li>
</ul>
Here it is:
amount of item
I used a ul
element here so that the HTML is valid (li
elements
can only be children of ul
or ol
). In most situations, I would use
a div
, but it really doesn't matter.
Hiding the DIV Template¶
You'll notice that the template div (the outer one) has an ID and a class. Either can be used for styling. When we are done debugging all our templates, we can make them all disappear by adding this to our CSS file:
.template { display: none }
We used the class for styling (in this case, hiding) and we will use the ID used to specify which template we will use in a particular cloning operation.
In the example above, I just grayed-out the template, so you could
still see it. When I'm done, I would use the display:none
trick.
The Cloning Technique¶
Our strategy will be to
- clone the HTML stuff inside the template,
- modify its contents for a grocery item, and
- add the clone to the page.
Note that the clone is a complete copy of all of the HTML stuff, so the programming effort involved in step 1 is the same whether the template is a little bit of HTML or a lot. Therein lies the great advantage of cloning. Also, note that we are cloning the stuff inside the div, so the clone will not have an ID. Since IDs need to be unique identifiers, it doesn't make sense to have the same ID on every clone. Our clones will not have IDs.
In step 2, we insert or modify whatever stuff needs to be
customized. For our grocery items, we have to modify the text of the
two spans. In this step, we can use jQuery methods to .find()
parts
of the clone and modify them with .text()
.
Finally, in step 3, we add the clone to the page. The clone is initially offstage, so it's useless until it's added to the page.
Creating an Item By Cloning¶
Without further preamble, here's our code:
function addDomElement(item, amt) {
let clone = $("#grocery-template > li").clone();
clone.find(".item").text(item);
clone.find(".amount").text(amt);
clone.appendTo("#groceries");
}
We can use it to add a grocery item exactly like the first version of addDomElement
:
addDomElement('milk', '1 gallon');
Finding the Stuff to Clone
Notice that the selector string to find the thing to clone was
#grocery-template > li
. That selects the Dli
that is a child of
the element with id grocery-template
. There is only one such child,
so it finds exactly the thing we want. So the surrounding
ul#grocery-template
holds the thing we want to clone, but we clone
the thing inside the container. The container has an ID (so we can
specify it easily), but the clone does not have an id.
You'll also notice that the cloning takes one line of code, regardless of how big and complex our template is. Therein lies the advantage of cloning.
Modifying the Clone
In this example, it takes us two steps to modify the clone, both using
a chain of .find(selector).text(content)
. The more dynamic stuff,
the more code it will take. If the template is small and almost every
part needs updating, that would undercut the usefulness of cloning,
but that's not often the case.
Adding to the Page
The last step of adding the clone to the page is very easy here. It
usually is, unless the location to put the clone is difficult to
find. But in this case, we just need to append the dynamically created
checkbox to our static container, namely #groceries
.
No IDs¶
The purpose of IDs on elements is to uniquely identify them. If we are making copies of the template, and the template has an ID on any of its elements, that ID will be duplicated and no longer uniquely identify the element. Therefore:
Never put IDs on Dynamic Elements
While it's certainly possible to create IDs for dynamic elements and use them successfully, it's tricky and usually unnecessary. I suggest avoiding ID on all dynamic elements.
Summary¶
- DOM elements can be static (defined in the HTML file) or dynamic (added/removed by JavaScript/jQuery)
- DOM elements are dynamically created by creating them, one node at a
time, attaching them using tools like
append
until you have a branch of a tree. - The branch can then be added to the document, again using something
like
append
. - JS/JQ code can search a branch (whether attached to the document or
not) by using the
find
method - JS/JQ code can search up the document, from child to parent, using
the
closest
method. - Event handlers can use delegation, in which as static ancestor of a set of elements can handle the events for all of them.
- Event delegation is more efficient and also much more convenient for elements that are dynamically added and removed.
- Cloning is a powerful technique:
- Create some template HTML for your dynamic elements
- in JS, to add one dynamic element to your page:
- clone the template
- modify the clone
- add the clone to the page
- You can use CSS to hide the template