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

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:

    dynamic groceries

    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:

    ol id="groceries"

    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.

    1. 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 the ol#groceries element that is in the static HTML.
    2. The .one() just makes sure we successfully selected one element, using my plug-in.
    3. This says we are handling a click event.
    4. This is where we say that the descendants we are interested in are button elements, in fact, buttons with a data- attribute of data-role=done.
    5. This starts our event-handler function. It takes an event object as an argument.
    6. 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 an li.
    7. This line searches down the tree from the li to find an element with the class item. It then pulls the text of the element out.
    8. Log what we are removing, like apples
    9. Use a separate function to remove the item from the data structure.
    10. 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

    1. clone the HTML stuff inside the template,
    2. modify its contents for a grocery item, and
    3. 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:
        1. clone the template
        2. modify the clone
        3. add the clone to the page
      • You can use CSS to hide the template