From Data to DOM

This reading supplements the reading from Chapter 11, titled Data to DOM. We know from our earlier work with jQuery that one way to manipulate the DOM is to dynamically add elements. This chapter walks us through the code to have our Coffee Order app add a checkbox item to the page (in a list of Pending Orders) every time a coffee order form is submitted.

The key concepts are dynamically constructing DOM elements and, later, calling a method while being able to specify the value of this.

If you don't have the book, here's a link to a copy of a scan of the chapter. To respect the author's copyright, the link is only valid on-campus or with a password. Ask Scott if you don't have that password.

FEWD Chapter 11

CheckList in ES6

Chapter 11 is about coding the CheckList module. Here's that code, re-written to use modern ES6 modules and classes. I also reformatted it a little and added a few additional comments.

/* Re-written from checklist.js. Defines a class where each instance
 * is associated with a set of HTML checkboxes, each corresponding to
 * a coffee order. The key (unique identifier) for a coffee order is
 * an email address. Adding a coffee order removes any prior orders
 * for that same email and adds a Row to the set of checkboxes. (Row
 * is an internal class.) Clicking on a checkbox removes that checkbox
 * from the document and invokes a callback function.

* Rewritten by Scott Anderson, Summer 2020
*/

// Private helper functions
function makeDescription(order) {
    let description = order.size + ' ';
    if (order.flavor) {
        description += order.flavor + ' ';
    }
    description += order.coffee + ', ';
    description += ' (' + order.emailAddress + ')';
    description += ' [' + order.strength + 'x]';
    return description;
}

function makeRowElt(order) {
    let $div = $('<div>', {
        'data-coffee-order': 'checkbox',
        'class': 'checkbox'
    });

    let $label = $('<label>');

    let $checkbox = $('<input>', {
        type: 'checkbox',
        value: order.emailAddress
    });

    $label.append($checkbox);
    $label.append(makeDescription(order));
    $div.append($label);
    return $div;
  }

class CheckList {
    // The constructor takes a selector string which specifies the
    // container HTML element for the set of checkboxes.
    constructor(selector) {
        if (!selector) {
            throw new Error('No selector provided');
        }

        // find the container element and store it for later
        this.$element = $(selector);

        if(this.$element.length == 0) {
            throw new Error(`No element was found for selector ${selector}`)
        }
        if(this.$element.length > 1) {
            throw new Error(`Too many elements were found for selector ${selector}`);
        }
    }

    // method to add a callback function to the checklist. This function is invoked
    // anytime a checkbox is clicked. Its argument is the email address of the order.
    addClickHandler(callback) {
        // Use jQuery's delegated handler mechanism
        this.$element.on('click',
                         'input', // Only interested in INPUT descendants
                         function (event) {
                             let email = event.target.value;
                             this.removeRow(email);
                             callback(email);
                         }.bind(this)); // note the .bind()
    }

    // Adds a row to the list. Argument is an object with all the
    // information about the coffee order.
    addRow(coffeeOrder) {
        this.removeRow(coffeeOrder.emailAddress);
        let rowElt = makeRowElt(coffeeOrder);
        this.$element.append(rowElt);
    }

    // Remove existing orders using that email address, if any.
    removeRow(email) {
        // This selector is complicated by needing to put quotation marks around
        // the email address. We use single-quotes outside and double-quotes within
        let sel = '[value="'+email+'"]';
        // use jQuery chaining to find existing orders, if any, go up
        // to the checkbox, and prune the whole subtree
        this.$element
            .find(sel)
            .closest('[data-coffee-order="checkbox"]')
            .remove();
    }
}

// extra exports for debugging and experimentation
export { CheckList, makeDescription, makeRowElt };
// export { CheckList };

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.

Your book does a very good job of describing this, with good pictures, so this section is just a short recap:

  • We will build a complete branch first, before adding it to the tree
  • We will build each node of the branch independently, adding them to the branch using jQuery's .append method to make one node a child of another.
  • We can build a simple node like this: $("<div></div>")
  • To build a node with attributes, supply a JS object literal with the attribute names and values:

$("<input></input>", {'type': 'checkbox', 'value': 'fred'})

Your book's authors do this as part of an object constructor, with the finished branch stored as an instance variable. Only later is the finished branch grafted onto the document.

Calling Methods

In the next part of the chapter, page 235, the authors modify the form submission handler to invoke two methods on two objects. Here's the code:

formHandler.addSubmitHandler(function (data) {
    myTruck.createOrder.call(myTruck, data);
    checkList.addRow.call(checkList, data);
});

They introduce the JavaScript call method, which is a way to invoke a function as a method and supply the value of this in doing so.

It's an interesting example of the use of call, but it's unnecessary. You have an object and its argument, and you know what method to run, so the code is equivalent to two ordinary method calls:

formHandler.addSubmitHandler(function (data) {
    myTruck.createOrder(data);
    checkList.addRow(data);
});

(In fact, their solution code uses the above code, rather than the code in the book.)

We will have occasion to see .call() later in the course, when we learn about object inheritance.

Searching down the tree

We have used jQuery many times to search the DOM. For example:

$("#fred")

$("[data-coffee-form=myform]")

The expressions above search the entire document for elements that match the given selector, but what if you already have a jQuery wrapped set for part of the document (a subtree), and you want to search that subtree? In that case, you can use jQuery's .find() method:

$("#fred").find('[data-coffee-form=myform]')

Or, if you already had something saved in a variable:

var $elt = $("#fred");
...
$elt.find("[data-coffee-form=myform]");

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 tool.

Searching up the tree

Sometimes, you want to search up the tree, along your 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. It stops at the first (closest) matching ancestor.

Delegation

Event delegation is a powerful jQuery technique that allows you to put one event handler on an ancestor and delegate to it handling all the events of a certain kind for all of its descendants. For example, try clicking on the items on this grocery list:

  • apple
  • banana
  • chocolate

Rather than put an event handler on each item (here only three, but you should see how long my grocery lists get, especially when I'm hungry), we can put one event handler on the UL element, and say

"anytime an LI is clicked, run this function"

We do that with the following code:

$("#groceries").on(
    'click', // first arg is the event, as usual
    'li',    // this argument is new; the descendant who delegates the click
    // the function moves to third, but is otherwise unchanged
    function (event) {
        var clickee = event.target;
        var text = $(clickee).text();
        alert("You clicked on the "+text);
});

Note that the event.target is the actual element that was clicked on. More on it below. You can also use this which will be the delegating element (the LI in our example above). Usually this and event.target are the same, but there are subtle differences which we'll get to later.

Motivation

What is event delegation? Why do it? In Ottergram, there were 10 pictures of otters, to which we attached an handler to each one, so 10 nearly identical event handlers. In the Concentration assignment, we creating 16 nearly identical event handlers. In both of these, 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

In Chapter 11, CoffeeRun will dynamically add checklist items for each order, and we want to have a event handlers for them. Or rather, we want to have one event handler that will handle all the dynamically added checklist items. This is the event delegation pattern they describe on page 241. The memory aspect is nice, but dealing with dynamically added DOM elements is what makes delegation great.

Event Delegation Pattern

If we have a list of groceries like this:

  • apples
  • bananas
  • chocolate

The DOM elements might look like this:

ul with three groceries

With event delegation, we put the event handler on the UL, rather than on each of the li elements. The screenshot shows that the event handler is on the ul, not the three li elements. Because of the way browsers work, clicking on an li counts as clicking it's parent (the ul), it's grandparent, and each of its ancestors, in turn, all the way to the body element. This is called having the event bubble up.

This 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.

Here's the working example of dynamic groceries. It's less than 40 lines of JS/JQ code and the same number of lines of HTML, so please take a minute to read it. The next section goes over the code in detail.

Grocery Code

Let's look carefully at a little of the HTML and JS/JQ code. First, the HTML has an empty grocery list. We could add a few items, and we will, but using JS:

<ul id="groceries"></ul>

Second, there's a simple form to add an item to the grocery list:

<form id="add-form">
    <p><label>item <input type="text" name="item"></label>
        <button type="button" id="add">add item</button></p>
</form>

Note the IDs above; we'll be referring to these elements from our JS/JQ code.

First, there's an array of groceries. This array is dynamic in that there are functions to add and remove items from the groceries:

// initialized when the page loads
var groceries = ['apples', 'bananas', 'chocolate'];

function addItem(item) {
    groceries.push(item);
}

function removeItem(item) {
    let index = groceries.findIndex(function (elt) { return elt == item });
    if( index != -1 ) {
        groceries.splice(index, 1);
    }
}

(I don't want to get distracted from the event handling, but you should take a few minutes to think about how the code to remove an item works. It uses findIndex to find the index of the item, so if item is "apples" the index is zero, and so forth. If the item is not found, findIndex returns -1. If the item is found, we remove it using the splice method, which takes arguments of the index to start deleting at and how many items to delete.)

Next, we have function, initDOM that does three things:

  1. initializes our ul#groceries item from the global variable
  2. sets up an event handler for adding one item to the groceries
  3. sets up a delegated event handler for deleting one item from the groceries

Let's look at those one at a time.

initializing The following code iterates over our grocery list, creating an li for each one, adding the item as the text content of the li and appending the li to the #groceries DOM element. jQuery really shines here, because the code is marvelously concise.

    let $ul = $("#groceries");
    groceries.forEach(function (elt) {
        $("<li>").text(elt).appendTo($ul);
    });

adding The following code adds an event handler in the normal way to the #add button of the form. The handler finds the input whose name attribute is item and extracts the value (that the user typed in). It adds the item to the global variable, and then uses code very similar to the code we just saw for adding the new item to the end of the #groceries list. Finally, it resets the form.

    // a normal click handler to add one item
    $("#add").click(function () {
        let val = $("[name=item]").val();
        console.log('add '+ val);
        addItem(val);
        $("<li>").text(val).appendTo($ul);
        $("#add-form")[0].reset();
    });

deleting Lastly, we add a handler to the (empty) #groceries DOM element, saying that clicking on any li descendants should run this event handler. There aren't any li descendants right now, but there will be later.

The event handler uses this (which is the particular li element that we clicked on) and uses the .text() method to find out the item itself. The handler then removes the item from the global variable and the DOM element (the li) from the #groceries DOM element.

    // a delegated click handler to delete any item
    $ul.on('click', 'li', function (li) {
        let item = $(this).text();
        console.log('remove '+ item);
        removeItem(item);
        $(this).remove();
    });

Keep this example in mind and return to it as often as necessary.

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).on('click',
          'descendant selector',
           function () { ... });

Any descendant of sel that matches descendant selector will delegate its click events to the ancestor, which runs the given function.

Why use event delegation? First, it's more efficient to have one event handler than many. Second, if we are dynamically adding and removing things from the list (as we are in the CoffeeRun app), the event handling is much simpler and cleaner if we don't have to worry about adding and removing event handlers as well. Because the ancestor that we attach the event handler to is static (defined in HTML page, rather than being dynamically added), we can count on it always being there, unchanged.

Event Delegation and THIS

Here's a slightly different grocery list, with some more structure in each item. Compare clicking on the emphasized stuff versus non-emphasized.

  • apples, specifically Honeycrisp
  • bananas, which I like but my kids don't
  • chocolate, especially dark chocolate
<ul id="groceries2">
   <li>apples, specifically <em>Honeycrisp</em></li>
   <li>bananas, which <em>I</em> like but my kids <em>don't</em></li>
   <li>chocolate, especially <em>dark chocolate</em></li>
</ul>

Again, we just put one event handler in the page, attached to #groceries2 but handling any click on an LI that is a descendant of #groceries2.

$("#groceries2").on('click',
    'li', // this is new
    function (event) {
        var clickee = event.target;
        var target_text = $(clickee).text();
        var this_text = $(this).text();
        if( clickee == this ) {
            alert('both clickee and this are the same: '+this_text);
        } else {
            alert('clickee is '+target_text+' while this is '+this_text);
        }
});

Note that this is the li element, while event.target is the actual element that was clicked on, which might be descendant, such as the em. Try clicking apples and also Honeycrisp.

Most of the time, the event.target will be the same as the LI that was clicked on, but if the LI contains other HTML elements, as it does here, those descendants might end up being event.target. Sometimes the different will matter, but often not. Something to keep in mind.

Form Values and .val()

In Chapter 10, we learned how to serialize all of a form's values into an array of objects, and then iterate over that array to assemble them all into a single object.

That's a fine technique, but if you just want one value, there's an easier way. jQuery has a .val() method that can get the value of an input:

To get the value, just invoke .val() with no arguments:

$(selector).val()

For example:

$("[name=zip_code]").val()
$("[name=pizza_size]").val()

This method works on text input elements, select elements (drop-down menus), and textarea elements.

For radio buttons, you can use the :checked pseudo-class to get the value of the one that is checked:

$('input[type=radio][name=size]:checked').val();

You'll need to use .val() for the next assignment (Quiz)

It's also possible to set the value of an input, but we don't need that.

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.