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.
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:
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:
- initializes our
ul#groceries
item from the global variable - sets up an event handler for adding one item to the groceries
- 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.