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

By Lyman R. Allen, formerly Instructor in Geography, State Normal School, Providence, Rhode Island,
and Alonzo J. Knowlton, 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:

Map Controls

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 to none and it disappears.
  • If a user checks the box, this property is set to true and the layer display is set to block 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().

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);
}
  1. 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 attribute selected, which only sets the initial selection, if any.)
  2. The <select> element comes with the property .options which is an array of the <option> elements it contains. Their .value property corresponds to the id 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 their stroke to the current color (which most of them already are, but this is faster than testing them first).
  3. 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 its stroke to the highlight color 'rgb(176,255,255)'.
  4. 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; }
Map Controls

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:

Statistics of the State of Massachusetts by Counties from the Federal Census of 1910.

The popup table of summary statistics for the column 'Population 1910'.
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!

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:

  1. A popupMenuIcon jQuery image is generated and then added after the column name massProperties[p];
  2. The summary table is generated and assigned the class of summaryTable, and then added after the icon;
  3. The <th> element is given the class of numhead;
  4. The <th> element is assigned the event handler toggleTable for both of the events mouseover and mouseout.

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!

  1. 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>
    
  2. 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 and mouseout, you’ll need mousemove.

results matching ""

    No results matching ""