Chapter 9: Interactive Data on the World-Wide Web
9.1 Responding to User Actions
Web pages can be very interactive, and JavaScript provides the means to respond to user-generated events, perhaps providing information and/or modifying the page.
A click of a mouse, entering a field on a form, even scrolling an object into view can be a signal to process some interesting bit of JavaScript code.
Buttons
Checkboxes
Radio Buttons
Menus and Option Lists
Field Sets
DOM Events in General
Event Data
Summary
Exercises
Buttons
After hyperlinks, the most obvious type of user interaction on a Web page is with a button, which generally changes something about a page that isn’t clearly referenced by a visible element. Buttons are commonly used to submit forms, but we also saw an example in Chapter 6.3:
The Geography of New England
and , Superintendent of Schools, Belfast, Maine.
where the header is defined as
<div id="gneHeader">
<h1>The Geography of New England</h1>
....
</div>
#gneHeader { width: 100%; border: solid !important; box-sizing: border-box !important;
display: none; position: fixed; top: 0; left: 0; z-index: 1000; }
Such a button is implemented with a single in-line element, viz.
<input type="button" value="Show Header" onclick="toggleHeader(this)" />
There are different types of input elements (more of which we’ll see in a bit), and for buttons the attribute value
provides the label inside the button and explains its purpose.
When a button is clicked, an event is generated in the browser, and it executes the JavaScript code in the attribute onclick
, called the event handler. In this case it calls the function toggleHeader()
and hands it a reference to itself, this
:
function toggleHeader(input)
{
var header = document.getElementById('gneHeader');
if (input.value == 'Show Header')
{
header.style.display = 'block';
input.value = 'Hide Header'
}
else
{
header.style.display = 'none';
input.value = 'Show Header';
}
}
In this function, the header with ID gneHeader
is first looked up in the DOM, so that its style
attribute can be modified by setting its property display
to either 'block'
or 'none'
. Effectively this adds the attribute style='display: ...;'
to the header (if it’s not already there), and sets a value for it.
Which of these actions is implemented depends on the value of the button’s attribute value
: if it is 'Show Header'
, the header should be displayed, and if it is 'Hide Header'
, it should be hidden. Finally, button.value
is set to the other of its two possible values so that the user will know what the effect of clicking the button will be. These two sequential changes happen so quickly in your browser that they’ll seem simultaneous!
The event handler must, of course, have been defined by the time the click occurs, preferably before the button is even written onto the page. So in this case it’s advisable to place it in the document head
, either directly or in a referenced file.
Checkboxes
When you have an HTML element whose condition you want to switch between two states, you can use a checkbox to control it.
Checkboxes are usually the most appropriate control for binary conditions such as on/off, yes/no, and true/false, where a single label can be used and the opposite condition is obvious.
For example, our map from chapter 6 can have its two layers, polygons and tiles, turned on and off independently of each other with a pair of checkboxes:
The HTML code to implement the checkbox response is similar to the button code:
<label>
<span class="label">Layer Visibility:</span>
<input type="checkbox" value="neSVG" checked onchange="toggleLayer(this)" /> Polygons
<input type="checkbox" value="neTiles" checked onchange="toggleLayer(this)" /> Tiles
</label>
<div id="nepm2">
<svg id="neSVG" viewBox="-59619 82359 643486 810982">
....
</svg>
<div id="neTiles">
....
</div>
</div>
The <input>
element has a new property, checked
, which is a Boolean (true or false) value whose presence indicates that the checkbox should be checked initially, corresponding to the layers being visible.
The label
element is important to associate a descriptive label with a given set of input
elements, facilitating user understanding of their purpose and providing for consistent formatting:
label .label { display: inline-block; width: 120px; text-align: right; padding-right: 4px; }
All of the tiles are contained within a single element <div id="neTiles">
, so they can be turned on and off as a group, just as all of the SVG paths are grouped with <svg id="neSVG">
.
The event handler is this time assigned in the HTML to the change
event rather than the click
event, though the effect is the same in this case — the user cannot click without making a change. The handler also looks similar to the earlier button code, but is here implemented with jQuery:
function toggleLayer(input)
{
var layer = $('#'+input.value);
if (input.checked)
layer.css('display', 'block');
else
layer.css('display', 'none');
}
The value
that is provided by the <input>
element to the event handler corresponds to the id
of the layer element to be controlled, either 'neSVG'
or 'neTiles'
, so in jQuery it can be looked up by prepending '#'
to it and handing it to $()
.
When you click on a checkbox, its new state is provided in the property input.checked
:
- If a user unchecks the box, this property is set to
false
before calling the event handler, and the layer display is set tonone
and it disappears. - If a user checks the box, this property is set to
true
and the layer display is set toblock
and it reappears.
Radio Buttons
When you need to select from a few items that are mutually exclusive (usually more than two), you can use radio buttons rather than checkboxes.
For example, we can set the color of the polygon layer with radio buttons, since only one color can be visible at a time:
<label>
<span class="label">Polygon Color:</span>
<input type="radio" name="polygonColor" value="red" checked onchange="polygonColor(this)" /> Red
<input type="radio" name="polygonColor" value="green" onchange="polygonColor(this)" /> Green
<input type="radio" name="polygonColor" value="blue" onchange="polygonColor(this)" /> Blue
</label>
An additional property name
is required for radio buttons so that the browser knows which ones should be together in a one-or-none group. The checked
property is again present as an initial value, but should only be assigned to at most one of the buttons in the group. If none of the buttons are checked, the browser depends on the user to select one.
The event handler is again assigned in the HTML to the change
event rather than the click
event, and here the effect could be different — the already-checked button could be clicked on with no effect.
function polygonColor(input)
{
$('#new_england_states').css('stroke', input.value);
}
The value
that is provided by the <input>
element is simply the text representation of the color to be used for the SVG element group referenced by $('#new_england_states')
, e.g. 'blue'
. It is set with a simple assignment to the latter’s style
attribute with the jQuery method .css()
.
Menus and Option Lists
When you have more than a few items to select from (and generally only when listing them all will take up too much screen room), you can use a menu or option list:
<label>
<span class="label">Select State:</span>
<select type="menu" name="states" onchange="highlightState(this)">
<option value="" selected>None</option>
<option value="Connecticut">Connecticut</option>
<option value="Maine">Maine</option>
<option value="Massachusetts">Massachusetts</option>
<option value="NewHampshire">New Hampshire</option>
<option value="RhodeIsland">Rhode Island</option>
<option value="Vermont">Vermont</option>
</select>
</label>
<div id="nepm2">
<svg id="neSVG" viewBox="-59619 82359 643486 810982">
<g id="new_england_states" transform="translate(0,810982) scale(1, -1)">
<path id="Maine" ... >
<path id="Vermont" ... >
<path id="NewHampshire" ... >
<path id="Massachusetts" ... >
<path id="Connecticut" ... >
<path id="RhodeIsland" ... >
</g>
</svg>
....
</div>
By default <select>
elements display as a pop-up menu with only the first item visible, but the attribute size
controls the number of options visible at once; if it’s less then the total number it appears as a scrollable option list, e.g. if size="4"
:
The attribute selected
can be used to provide an initial selection or set of selections, which corresponds to the attribute checked
for checkboxes and radio buttons.
By default, only one option can be selected, like a set of radio buttons. To allow more than one of the options to be selected at once, like a set of checkboxes, include the boolean attribute multiple
.
Like groups of checkboxes and radio buttons, it’s a best practice to provide a label for menus, since it’s not always clear what the options represent, and it provides an important reference point.
The JavaScript code to highlight a state is more complicated than the previous examples:
function highlightState(select)
{
// Find current polygon color
var colors = $('[name="polygonColor"]');
for (var color = 0; color < colors.length; color++)
if (colors[color].checked)
{
color = colors[color].value; // reuse var
break;
}
// Deselect current states
var options = select.options; // Array of elements
for (var option = 0; option < options.length; option++)
$('#'+options[option].value)
.css('stroke', color);
if (select.selectedIndex == 0) return; // None
// Select new state by coloring it and
// moving it to the end so it appears on top
var state =
$('#'+options[select.selectedIndex].value)
.css('stroke', 'rgb(176,255,255)');
state.remove();
$('#new_england_states').append(state);
}
- The first order of business is to find the current polygon color so that a currently highlighted state (if any) can be reset. Using a jQuery selection that matches all of the radio buttons,
$('[name="polygonColor"]')
, the function loops through them to find the one that is currently selected. (Note: the property.selected
is different than having the attributeselected
, which only sets the initial selection, if any.) - The
<select>
element comes with the property.options
which is an array of the<option>
elements it contains. Their.value
property corresponds to theid
of the state element to be controlled, e.g.'NewHampshire'
. The latter can be looked up in jQuery by prepending'#'
to this value and handing it to$()
. Looping through them we can set theirstroke
to the current color (which most of them already are, but this is faster than testing them first). - Once the current color has been restored, we can determine the selected option using the property
select.selectedIndex
, and if it refers to the first item, “None”, return at the point. If it’s another state, we can set itsstroke
to the highlight color'rgb(176,255,255)'
. - To make sure the border of the highlighted state is completely visible, it needs to be drawn on top of all of the other states, which means removing it and appending it at the end of the SVG group '#new_england_states', since later vectors are drawn on top of earlier ones.
Field Sets
Often you may have several sets of controls whose purpose is closely related, e.g. the map controls seen previously. It is helpful to user comprehension to group them together within a <fieldset>
element, with a <legend>
that describes what they have in common. (The name comes from another type of control that we’ll see later, the text field.)
<fieldset>
<legend>Map Controls</legend>
<label>Layer Visibility: .... </label>
<br />
<label>Polygon Color: .... </label>
<br />
<label>Select State: .... </label>
</fieldset>
fieldset { border: solid 2px; display: inline; }
legend { font-weight: bold; }
DOM Events in General
When an HTML element to be modified is in someways visual and static, then it’s often better to let it be the target of user actions, rather than creating a separate control. (There should, however, be some way that the user can recognize that there will be an effect, such as a link or icon.)
Most of the objects corresponding to elements in the Document Object Model respond to a large number of events, not just clicks and mouseovers, but possibly also scrolling, keystrokes, window resizing, etc. See the Mozilla Event documentation for a complete list.
In every case of a recognized event, you can assign a corresponding event handler to generate a response.
For example, for the Massachusetts data set we saw previously:
var mass = [
{ County: 'Barnstable', Organization: 1685, Area: 409,
"Population 1910": 27542, "County Seat": 'Barnstable' },
{ County: 'Berkshire', Organization: 1761, Area: 966,
"Population 1910": 105259, "County Seat": 'Pittsfield' },
{ County: 'Bristol', Organization: 1685, Area: 567, "Population 1910": 318573,
"County Seat": [ 'Fall River', 'New Bedford', 'Taunton' ] },
'....'
];
we generated the following table:
But this table is different than the one before, because when you point at the column headers it fires an event known as mouseover, resulting in a pop-up table of summary information. Moving the mouse out of the column headers fires another event known as mouseout, making the table go away. Try it!
Statistics of the State of Massachusetts by Counties from the Federal Census of 1910.
We also saw the summary function previously:
var massSummary = summarizeJSONtable(mass);
⇒ {
"Organization": {count: 14, total: 23944, max: 1812, min: 1643,
mean: 1710, stdev: 67, total: 23944 },
"Area": {count: 14, total: 8039, max: 1556, min: 51,
mean: 574, stdev: 382, total: 8039},
"Population 1910": {count: 14, total: 3366416, max: 731388, min: 2962,
mean: 240458, stdev: 231958, total: 3366416
}
Each of these summary objects can be turned into a jQuery table with another function:
function summaryTable(summary)
/* Takes a summary object produced by summarizeJSONtable()
and creates a jQuery table listing its properties and values. */
{
var tbody = $('<tbody>');
var stats = [ 'count', 'max', 'min', 'mean', 'stdev', 'total' ];
for (var s = 0; s < stats.length; s++)
{
var tr = $('<tr>');
tr.append($('<th>').text(stats[s] + ':').attr('class', 'numcol'));
tr.append($('<td>').text(summary[stats[s]]).attr('class', 'numcol'));
tbody.append(tr);
}
return $('<table>').append(tbody);
}
Here the stats are explicitly provided in an array to establish a particular order, and each one is used to create a row element containing the property stats[s]
and its value summary[stats[s]]
. These are appended to the tbody
element in turn, and the result is appended to a table
element which is returned as the function result.
The summary table is positioned with the following CSS:
.textcol { text-align: left; }
.numcol { text-align: right; }
.numhead { position: relative; text-align: right; }
.summaryTable { position: absolute; z-index: 1; top: 0; left: 100%;
display: inline-table;
border: solid 1px gray; box-shadow: 2px 2px rgba(0, 0, 0, 0.4);
padding: 4px; background-color: white;
font-weight: normal; text-align: left; }
Because the summary
object only has properties for the columns that are numeric, we can use that characteristic to assign those columns right justification, but more importantly give the column heads, <th>
elements, relative positioning with class="numhead"
, so that they become the positioning context for the summary tables.
The summary tables are aligned with the top of the <th>
elements with top: 0;
, and shifted to their right edge with the full-width offset left: 100%
.
To make the table appear to “pop-up” from the background, a box shadow is set to offset 2 pixels right and down, and using the color function rgba(red, green, blue, alpha)
, where the alpha channel sets transparency on a scale of 0 (completely transparent) to 1 (completely opaque).
The summary table has display
set to none
so that it doesn’t appear initially. The event handler toggleTable()
sets it to table
in response to mouseover
events and back to none
in response to mouseout
events, using the jQuery method toggle()
:
function toggleTable() { $(this).find('table').toggle(); };
The variable this
refers to the <th>
element that is the recipient of the mouse focus, and the find()
method locates the one <table>
element within it, the summary table. (This is faster than searching the child nodes directly.)
The rest of the code is very similar to the previous implementation using jQuery:
<table>
<caption>Statistics of the State of Massachusetts by Counties from the Federal Census of 1910.</caption>
<thead id="thead"></thead>
</table>
var popupMenuIcon = 'https://cschweik.gitbooks.io/community-service-with-web-based-gist/content/icons/button-close_branch-macintosh.png'
function generateCountyTable2()
{
var tr = $('<tr>');
for (var p = 0; p < massProperties.length; p++)
{
var th = $('<th>').text(massProperties[p] + ' ');
if (massSummary.hasOwnProperty(massProperties[p])) // numeric
{
var img = $('<img>').attr('src', popMenuIcon)
var summary = summaryTable(massSummary[massProperties[p]])
.attr('class', 'summaryTable');
th.attr('class', 'numhead')
.append(img)
.append(summary)
.mouseover(toggleTable)
.mouseout(toggleTable);
}
else
th.attr('class', 'textcol');
tr.append(th);
}
$('#thead').append(tr);
var tbody = $('<tbody>');
for (county = 0; county < mass.length; county++)
{
var tr = $('<tr>');
var someplace = mass[county];
for (var p = 0; p < massProperties.length - 1; p++)
{
var td = $('<td>');
if (massSummary.hasOwnProperty(massProperties[p])) // numeric
td.attr('class', 'numcol');
else
td.attr('class', 'textcol');
td.text(someplace[massProperties[p]])
tr.append(td);
}
// Last property is "County Seat", which could be an array or a single string
td = $('<td>');
var seats = someplace["County Seat"];
if (typeof(seats) == 'object') // array of towns
{
for (var seat = 0; seat < seats.length - 1; seat++)
{
td.append(document.createTextNode(seats[seat]));
td.append($('<br />'));
}
td.append(document.createTextNode(seats[seat]));
}
else // single town as a string
td.text(seats);
tr.append(td);
tbody.append(tr);
}
$('#thead').after(tbody);
}
The primary difference from the previous implementation of this code is that the <th>
elements are tested to see if they are numeric with the condition if (massSummary.hasOwnProperty(massProperties[p]))
, and if they are they receive the following additions:
- A
popupMenuIcon
jQuery image is generated and then added after the column namemassProperties[p]
; - The summary table is generated and assigned the
class
ofsummaryTable
, and then added after the icon; - The
<th>
element is given theclass
ofnumhead
; - The
<th>
element is assigned the event handlertoggleTable
for both of the eventsmouseover
andmouseout
.
Because the summary table is a descendant of the <th>
element, the latter is extended outward with the table, and you can move the cursor over the table and mouseout
won’t fire.
The jQuery event handler assignments seen here, e.g. th.mouseover(toggleTable)
, could also be achieved with the fundamental DOM methods on which they are built, for example:
th[0].addEventListener('mouseover', toggleTable);
(If th
is a jQuery object, then th[0]
is the corresponding DOM element if the jQuery selection matches only one item, as in this case.)
This is another example of how jQuery can provide easier and perhaps even more readable code.
Even the buttons seen earlier can be assigned event handlers directly in Javascript, e.g.
<input type="button" value="Show Header" onclick="toggleHeader(this)" />
could have the attribute id="toggleHeader"
and then
document.getElementById('toggleHeader').addEventListener('click', toggleHeader);
or using jQuery:
$('#toggleHeader').click(toggleHeader);
This approach has the advantage of allowing you to register more than one event for an element. It also lets you store all JavaScript references separately from the HTML.
In addition, the referenced object is known within the function by the keyword this
, so that the argument passed to the function can instead be a set of event data.
Event Data
When an event is generated in the DOM, it also produces a set of data about itself in an event object that is provided as the first argument in the event handler (if it’s assigned as described in the previous section).
A good example of this is click
, a Mouse event that provides a collection of information about clicks including their location.
When applied to the map, which is contained in <div id="nepm2">
, the map coordinates can be calculated using the SVG viewBox
information:
function position(mouse) {
// Find map position in document:
var nepm2position = $(this).offset();
// Calculate relative position of click on map:
var x = mouse.pageX - Math.floor(nepm2position.left);
var y = mouse.pageY - Math.floor(nepm2position.top);
// Find map dimensions in document:
var nepm2width = $(this).width();
var nepm2height = $(this).height();
// Use DOM method here since jQuery doesn’t recognize name spaces
// svgViewBox = [ left, bottom, width, height ]
var svgViewBox = document.getElementById('neSVG')
.getAttribute('viewBox').split(' ');
// Transform position on map into map coordinates:
var lcx = Math.round(svgViewBox[2] * x / nepm2width +
+svgViewBox[0]);
var lcy = Math.round(svgViewBox[3] * (1 - y / nepm2height) +
+svgViewBox[1]);
window.alert('X: ' + x +
'\nY: ' + y +
'\nMap X: ' + lcx +
'\nMap Y: ' + lcy );
};
// Assign event handler to click event:
$('#nepm2').click(position);
Summary
- HTML provides a large number of user controls such as buttons, checkboxes, radio buttons, menus, and option lists.
- User actions such as
click
,change
,mouseover
, etc., produce events that can be linked to JavaScripts to respond to them in some way. - Checkboxes are usually the most appropriate control for binary conditions such as on/off, yes/no, and true/false, where a single label can be used and the opposite condition is obvious.
- Radio buttons provide a set of mutually exclusive options, usually for three or more items.
- A menu or option list can provide many options that aren’t comfortably handled by a set of checkboxes or radio buttons.
- Multiple controls that are closely related, e.g. a set of checkboxes or radio buttons, as well as menus or option lists, should be grouped within a
<label>
element with some text that describes their purpose. - Several sets of related controls can be grouped together in a
<fieldset>
element that provides a larger context for them with its<legend>
text. - Any HTML element can respond to events when that makes sense in the conventions of a graphical user interface.
- When event handlers are assigned using DOM methods (directly or with jQuery), they can access detailed information about the events. The element itself can also be created with DOM methods.
Exercises
In the Exercises in Chapter 7, you created a Web page for Massachusetts called gne.html
with styles stored in gne.css
and scripts stored in js
. You can download the reference version of these documents from http://nrcwg01.eco.umass.edu/gne7.2x/ (that’s advised here because the towns have been implemented in an easier-to-use format — check it out!). In the exercises below you’ll add some interactivity to these documents. Remember to use the console to test out the different pieces of your code!
- For the Massachusetts map, add the three types of controls demonstrated above:
<fieldset> <legend>Map Controls</legend> <label> <span class="label">Layer Visibility:</span> <input type="checkbox" ...> </label> <br /> <label> <span class="label">Polygon Color:</span> <input type="radio" ...> </label> <br /> <label> <span class="label">Select State:</span> <select ...> </label> </fieldset>
- The click event described above currently pops up its information in a window alert. Write it instead to display the information in a table that appears whenever the cursor is over the map, changes as the cursor moves, and disappears when the cursor is no longer over the map. In addition to the events
mouseover
andmouseout
, you’ll needmousemove
.