Handling Events with JavaScript

This is an action-packed reading. We've learned about JavaScript and the DOM, and now it's time to learn about events and event handlers. That will allow us to understand the Javascript/jQuery (JS/JQ) code that enables this interactive version of Ottergram:

interactive Ottergram

The idea is that each thumbnail will have some JS/JQ code attached to it such that when you click on the thumbnail, the detail image and its caption changes. This is a very cool and useful effect, often used in web pages with image galleries like this one.

Big Concepts

  • The act of clicking on a thumbnail is called an event
  • The code that is attached to each thumbnail is called an event handler, because it "handles" the event. That is, it runs when the event occurs.
  • The event handler is a function, because a function is code we can run whenever we want.

Don't get confused by the terminology. An "event handler" isn't a special kind of function; it's just a function that's used for that purpose. A "commuter train" isn't really a special kind of train; it's just a train that's used by commuters.

First Click Handler

Let's start with a very simple event handler. We're going to create a button and attach a click handler to it. The click handler is a function that does something simple but obvious (so we'll know when it runs).

Define the click handler function first:

function myClickHandler() {
    alert('you clicked me! you really clicked me!');
}

Defining the function doesn't run it, so we don't see the alert.

Now, we'll create the button. We'll give it an ID so that we can add the click handler to it. (We could use other selectors, and we will in Ottergram, but this is fine for now.)

<button id="myButton">Click Me</button>

Finally, we attach the click handler to the button. We'll use jQuery's .click method, which is simple and easy. The .click method takes one argument, a function. That function gets run (executed, invoked) whenever the user clicks on the selected DOM element(s). Here the selected DOM element is our button, and the function is myClickHandler.

$("#myButton").click(myClickHandler);

Notice that there are no parentheses after the name of the function! That's because we are not invoking the function now. We are giving it to the click method, like you would hand someone a small machine (such as a small food processor or electric toothbrush) without turning it on. It gets turned on when/if the user clicks the button.

If you prefer anonymous functions, the following is equivalent:

$("#myButton").click(function () { 
    alert('you clicked me! you really clicked me!');
    });

Again, no parentheses after the function expression.

To see this first event handler in action, follow that link.

Event Objects

You noticed in our first example that there were no calls to the event handler. But if you click the button, it does get called. It's called by the browser.

That's one of the things that makes programming in a browser different from the kind of programming you did in CS 111. In that course, your code called built-in functions or helper functions you wrote, but no one called your functions, so you had control over how your functions were called.

In a browser, we don't have that control. It turns out that the browser calls your function with an argument. Just one argument, but the argument is a complicated object that describes the event. In other words, when we set up the page, we hand myClickHandler to the browser and say "if anyone clicks on that button, call myClickHandler". If and when that click happens, the browser calls myClickHandler(event), where the argument describes the event.

The event handler can use that argument to find out information about the event. It can find out when it happened (time of day, down to the millisecond), the coordinates of the mouse when it happened, whether the shift key was pressed when it happened, and all kinds of stuff, most of which we will ignore.

The event object also has a few methods. One important one is .preventDefault(). If the event normally does something, calling event.preventDefault() stops that behavior from happening. Let's turn to that.

Second Event Handler

Instead of using buttons (which have no "normal" action"), we will use a hyperlink. In fact, we'll use two, just to contrast the effect of using event.preventDefault(). Here are the two hyperlinks:

<a id="cs" href="https://www.wellesley.edu/cs/">CS department</a>
<a id="mas" href="https://www.wellesley.edu/mas/">MAS department</a>

Now, we'll create an event handler function that takes an argument. As always, we can name the argument anything, but traditional names are event, evt or even e for the extremely terse.

// this function prints the input event 
// and allows the default behavior

function csHandler(evt) {
    console.log(evt);
    alert("CS is exciting");
}

// this function prints the input event 
// and then prevents the default behavior
// by using the preventDefault method of evt

function masHandler(evt) {
    console.log(evt);
    evt.preventDefault();
    alert("MAS is very cool");
}

Finally, let's attach them:

$("#cs").click(csHandler);
$("#mas").click(masHandler);

Again, there's no parentheses following the csHandler and masHandler. These names are being used like variables, just to hold some data. In this case, the data is a function.

To see these second event handlers in action, follow that link. Notice that both pop up an alert when you click the link but only one follows the link to the destination. The MAS handler prevents that default behavior.

You're welcome to look in the console at the event object, but you can ignore it. For now, it's just a thing that allows us to prevent the default behavior.

Ottergram's Event Handler, Design

We're now ready to design our Ottergram event handlers. Each handler needs to do several things:

  • It has to modify the detail image. It'll do that by changing the src attribute of the img.
  • It has to modify the detail image text. It'll do that by changing the contents of the span.detail-image-text.
  • It has to get the two values above. It'll read them off of special custom attributes of the thumbnail that it's attached to.

Finally, remember each thumbnail initially looks like this:

        <li class="thumbnail-item">
          <a href="imgs/otter1.jpg">
            <img src="imgs/otter1.jpg" alt="Barry the Otter">
            <span>Barry</span>
          </a>
        </li>

There are at least four DOM elements here: the li, the a, the img and the span. Which one will we attach the event handler to? We'll attach it to the a because all browsers already know that a elements are clickable, so our website will be more accessible (to users who need assistive technology) if we put our click handlers on something that is already clickable. (Elements like li, img and span are not normally clickable, so adding click handlers to them can be inaccessible.)

However, that means that the event handler will have to prevent the normal hyperlink behavior, but we know how to do that.

Data Attributes

The last section talked about the event handler reading values from special custom attributes of the thumbnail. Our HTML code will change from:

        <li class="thumbnail-item">
          <a href="imgs/otter1.jpg">
            <img src="imgs/otter1.jpg" alt="Barry the Otter">
            <span>Barry</span>
          </a>
        </li>

to

        <li class="thumbnail-item">
            <a href="imgs/otter1.jpg"
               data-role="trigger"
               data-image-url="imgs/otter1.jpg"
               data-image-title="Stayin' Alive">
              <img src="imgs/otter1.jpg" alt="Barry the Otter">
            <span>Barry</span>
          </a>
        </li>

Woah! What happened here? We added three pieces of information to the hyperlink. Will this mess up the normal functioning of the hyperlink? No. The rules of HTML say that any attribute that starts with data- is reserved for the web developer. So, we can put any junk there that we want, and the browser will ignore it. Here, we put three new attributes that store useful information for the new event handler. Each attribute name is the concatenation of data- and a description of the information.

The data-role="trigger" is something that we will use to select the elements that will cause the detail image to be updated. They are the "triggers", in the sense of something that initiates a process.

I used the word select on purpose because you can use attributes, including data- attributes, as selectors in CSS. If we wanted all our triggers to have red text decoration (underlines), we could do this:

[data-role="trigger"] {
    text-decoration: underline red;
    }

We won't do this, but we will use the CSS selector syntax to search the DOM and find the triggers, to which we will attach event handlers.

The other data- attributes are for the new URL for the src of the detail image and the text of the detail title. You probably noticed that the URL is the same as the src of the thumbnail image. That makes life simpler, but it didn't have to be the same. Maybe it's in fact a larger, more detailed image that has a separate URL from the thumbnail, which could have been compressed, cropped or whatever.

We'll need some other data- attributes on the elements we want to modify. So, we'll change that part of the HTML as follows:

      <div class="detail-image-container">
        <div class="detail-image-frame">
          <img data-role="target" class="detail-image" src="imgs/otter1.jpg" alt="Barry the Otter">
          <span data-role="title" class="detail-image-title">Stayin' Alive</span>
        </div>
      </div>

So, we have a target and a title as the two elements we'll modify.

First Steps

Let's first describe the behavior we want, before we get into the nitty gritty of the code that does it. The behavior we want is the user clicks on a thumbnail image (one of the little ones) and the browser should replace the detail picture (the big one) with the thumbnail image and also replace the caption on the detail picture. These actions will be carried out by three functions:

  1. imageFromThumb gets the URL of the new detail image
  2. titleFromThumb gets the text of the new caption for the detail image
  3. setDetails changes the detail image and its caption

This is all nice and modular.

The execution of all these modular pieces will be achieved by a high-level event handler (a function), and that event handler will be attached to each thumbnail image.

Here's a function that, given a particular thumbnail, will extract the value of the URL that we want:

function imageFromThumb(thumbnail) {
    return $(thumbnail).one().attr('data-image-url');
}

Remember, the $ is jQuery. We wrap the thumbnail with jQuery so that we can use jQuery methods. The one() method checks that the wrapped set is exactly one element (assuming you've loaded my bounds plugin). The attr() method reads an attribute off the element. So this function will return a string like imgs/otter1.jpg.

Here's a similar function for reading off the title:

function titleFromThumb(thumbnail) {
    return $(thumbnail).one().attr('data-image-title');
}

Before we get to modifying the detail from a thumbnail, let's look up the relevant DOM elements. These statements get executed once when the page loads, since they are defined at the top level of the file. These variables will all have values when the user gets around to clicking on a thumbnail.

var detailImage = $('[data-role="target"]').one();
var detailTitle = $('[data-role="title"]').one();
var thumbLinks = $('[data-role="trigger"]').some();

First, notice that we use the CSS attribute selector syntax as the first argument to jQuery to search the DOM and find the desired elements:

$('[data-role="target"]')

Why store these values in global variables? As efficient as searching the DOM is, we know that we don't need to search it again every time we want to find the target or the title, because they aren't going to change. So, we save a few microseconds by doing this. But saving time really isn't the point. We also notify anyone reading our code that these things aren't going to change. Finally, we can open a JS console and play with these values, which I encourage you to do.

Here's a function that takes two strings as arguments and modifies the two elements that we want to modify:

function setDetails(imageUrl, titleText) {
    $(detailImage).attr('src', imageUrl);
    $(detailTitle).text(titleText);
}

The jQuery .attr() method modifies an attribute. This is the same method that we saw earlier, but here we supply a second argument. The two-arg version modifies the attributes. Here, we modify the src attribute of an img, and that causes the browser to load and display the new image. Very cool! Similarly, the jQuery .text() method changes the text inside an element; here, we use it to modify the span that holds the detail title.

Go ahead and open a JS console and try modifying the details by hand, using code like this:

setDetails('imgs/otter1.jpg', 'Barry');
setDetails('imgs/otter2.jpg', 'Robin');

You can also use the set of thumblinks:

imageFromThumb(thumbLinks[0]);
titleFromThumb(thumbLinks[0]);
imageFromThumb(thumbLinks[1]);
titleFromThumb(thumbLinks[1]);

Both imageFromThumb and titleFromThumb return a string that we could use as one of the arguments to setDetails, thereby transferring the information from one of the thumbnails to the detail image. In fact, we can put them together in a single function that transfers both strings from the thumbnail to the details:

function setDetailsFromThumb(thumbnail) {
    setDetails(imageFromThumb(thumbnail), titleFromThumb(thumbnail));
}

We could test it like this:

setDetailsFromThumb(thumbLinks[0]);
setDetailsFromThumb(thumbLinks[1]);

Ottergram's Event Handler, Implementation

We're ready to implement Ottergram's event handlers. We'll actually have five event handlers, one for each thumbnail. The code will look identical, but each handler is slightly different because it references a different thumbnail through the magic of closures. More on closures later.

Here's the code to add an event handler to a thumbnail:

function addThumbClickHandler(thumb) {
    $(thumb).one().click(function (event) {
        event.preventDefault();
        setDetailsFromThumb(thumb);
    });
}

The handler is the anonymous function inside the .click() method. The .click method takes either a function expression or the name of a function as its argument; we saw both in our first click handler.

Here, the function expression is the better option, because the function needs to refer to the thumb variable that is the argument to addThumbClickHandler. (That reference to a non-local variable is what makes the function a closure, but we'll talk about closures later.)

Attaching the Event Handlers

Attaching the event handlers is then (relatively) simple. Remember the forEach() method that JavaScript arrays have? That method invokes a callback function (of one, two or three arguments) for each item in the array. jQuery implements a similar method called each that invokes a callback function for each element of a wrapped set. However, the order of the arguments for the callback function (the function that is the argument to the method) is reversed. Instead of the callback being function (elt, index) ... it's function (index, elt) ....1

function initializeEvents() {
    $(thumbLinks)
        .some()
        .each(function (index, elt) { addThumbClickHandler(elt); });
}

This code is straightforward: we take our wrapped set of thumbnails and map over it, adding a click handler to each one.

Notice that this function is just defined, not executed. To actually add the thumbnail handlers, we have to invoke it:

initializeEvents();

We do that at the bottom of the main.js file.

Strict Mode

We'll look at the complete code in a moment, but first we need to talk about strict mode. Traditionally, JavaScript has been pretty lax about enforcing syntax rules. Semicolons can sometimes be optional, and global variables don't have to be declared before they are used. Developers have found that the laxness can be a source of errors and bugs, so many developers now insist on strict mode.

Strict mode can either apply to an entire script (file) or just to a function. To apply it to the whole file, put the following at the top of the file:

'use strict';

However, there's a pitfall with this, namely that if we concatenate our file with another file where strict mode doesn't apply (which is sometimes done, but we won't), the resulting code will fail. So, another approach is to add the 'use strict'; line to each function that you want to be strict. It has to be the first line of the function, like this:

function foo() {
    'use strict';
    ...
}

We'll use the whole-file approach.

Ottergram JavaScript

Recall that we have to add script tags to our HTML file in order to load JS code. For Ottergram, we'll load three files: jQuery, my jQuery bounds plugin, and our Ottergram JS code. So the following three tags appear at the bottom of the HTML file:

<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js"></script>
<script src="https://cs.wellesley.edu/~anderson/js/bounds/bounds-plugin.js"></script>
<script src="scripts/main.js"></script>

Notice that two are absolute URLs and one is a relative URL, since scripts/main.js is an integral part of the app and should be copied from place to place, as opposed the external stuff like jQuery and my plugin.

Finally, here's our complete file of code:

/* Rewritten Summer 2021 by Scott Anderson 
   from the FEWD original to use jQuery and ES 6
   and other improvements.
*/

'use strict';

// Globals that store useful DOM elements
// Rely on jQuery to look up elements. 

var detailImage = $('[data-role="target"]').one();
var detailTitle = $('[data-role="title"]').one();
var thumbLinks = $('[data-role="trigger"]').some();

function setDetails(imageUrl, titleText) {
    $(detailImage).attr('src', imageUrl);
    $(detailTitle).text(titleText);
}

/* testing:

setDetails('imgs/otter1.jpg', 'Barry');
setDetails('imgs/otter2.jpg', 'Robin');

*/
    
function imageFromThumb(thumbnail) {
    return $(thumbnail).one().attr('data-image-url');
}

function titleFromThumb(thumbnail) {
    return $(thumbnail).one().attr('data-image-title')
}

/* testing. Or try any other valid index into thumbLinks

imageFromThumb(thumbLinks[0]);
titleFromThumb(thumbLinks[0]);
imageFromThumb(thumbLinks[1]);
titleFromThumb(thumbLinks[1]);

*/

function setDetailsFromThumb(thumbnail) {
    setDetails(imageFromThumb(thumbnail), titleFromThumb(thumbnail));
}

/* testing. Or try any other valid index into thumbLinks

setDetailsFromThumb(thumbLinks[0]);
setDetailsFromThumb(thumbLinks[1]);

*/

function addThumbClickHandler(thumb) {
    $(thumb).one().click(function (event) {
        event.preventDefault();
        setDetailsFromThumb(thumb);
    });
}

function initializeEvents() {
    $(thumbLinks)
        .some()
        .each(function (index, elt) { addThumbClickHandler(elt); });
}

initializeEvents();

Recap

Here are the main points:

  • we can add arbitrary information to a DOM element by adding attributes. To make sure that the attribute doesn't conflict with one that has meaning to the browser, we need to ensure that the first five characters of the attribute name is data-. For example, if I want to add information to an element about how fancy it is, I could add a data-fancy="plain" attribute or data-fancy="suave".
  • we can look up DOM element(s) using jQuery like $(selector).
  • the return value from jQuery's selector is a "wrapped set" which has jQuery methods we can use.
  • the jQuery .attr() method either looks up a DOM attribute (with one argument) or modifies a DOM attribute (with two arguments, the second being the new value). Changing the src attribute of an img element changes the picture that is displayed. We use that to change the detail picture.
  • The jQuery .text() method changes the text inside a DOM element. We use that to change the detail title.
  • we should specify "use strict" as a string at the top of our JS functions or file to ask the browser to be a bit less permissive about possible errors.
  • we can attach a function as an event handler for an event using jQuery's .click() method. The function will be invoked by the browser whenever the event occurs
  • the function that we attach can either be named or anonymous, meaning a function expression or function literal, like function () { ... }
  • the argument function is passed in without parens after it, because parens would invoke the function and only pass in the return value. Instead, we want to pass in the function itself
  • the callback function is invoked with a JS object that contains a bunch of information about the event. This is called the event object
  • the event object has a method called preventDefault() which, if invoked, will cause the browser not to do the "normal" thing, whatever that normal thing is. In Ottergram, we're clicking on a hyperlink, and the normal thing is to visit the URL specified in the href. We want to not do that, so the event handler executes event.preventDefault where event is just the parameter that gets assigned the event object.

Thumbnails

Note that in Ottergram, the same image file is used for both the thumbnail (small) and the detail (big) version. If there were many thumbnails, such that you expect most of them won't get clicked on, that would be a poor design, because your page would have downloaded many big images only to scale most of them down. Thus, your download would be increased for no benefit: a waste of time and bandwidth.

In Ottergram, there are only a few pictures, so the waste is negligible. In a different application, we might create special thumbnail versions. Note that their code doesn't assume that the URL of the detail image is the same as the URL of the thumbnail, so their code is ready for that improvement. That's good design.

Closures

If you're curious about closures, read about them now. Closures are a really important idea in web programming and are used often, in situations very similar to what we did in Ottergram. Well worth wrapping your head around.


  1. why this gratuitous difference? I have no idea. jQuery version 1.0 had the .each method, and it was released in 2006. JavaScript didn't add the .forEach method until June 2011, so it's the JavaScript language developers that introduced the incompatibility. However, I think the JS language has it right: I more often want the element than its index.