Session 25: Cats and Dogs

A more serious title for this session might be More Events on Dynamic Web Pages.

Download this starter code to use during the session.

Cats: Practice

We have been learning about event listeners and recently learned the addEventListener() method. For example:

function respondToClick(e) {
  document.body.style.backgroundColor = "lightblue";
  document.body.innerHTML = "<h1>I am a little blue today</h1>";
}

let button = document.querySelector('button');
button.addEventListener('click', respondToClick);

Listeners are not limited to active HTML elements like buttons. As we saw in Session 19, any element can have a listener.

Open up the 'cats' index page in VS Code. The rendered page has an h1 heading, a paragraph with a poem, a paragraph with the author's name, and a hidden cat image. It also loads a nearly empty script file for us to program in.

Open the developer tools see the cats! message printed in the log.

Free the cat! Add an event listener to the poem's paragraph. In response to a double-click, the poem will:
  1. show the image
  2. set the background color of the body to lightgreen

Hints:

  • The double-click event is named dblclick.
  • You can use querySelector() to get the poem and the image.
  • You can remove the 'hidden' attribute from the image using, you guessed it, removeAttribute().

Look at a solution script running in the web page.

Any element can have a listener. There are many kinds of events.

Cats: More Kinds of Events and Listeners

Let's use the cats page to learn more about events, and to practice working with HTML, images, URLs, and the DOM. With a little JavaScript, we can create an interactive web page.

Load events

TL;DR: A web page can load images dynamically, rather than by hardcoding a URL in its HTML.

Modify the page's img tag: remove the src and hidden attributes, and add an id:

<img
  id="cat-picture"
  alt="a kitten sitting on a stone walkway"
  width="300"
>

Add modify the script to load a cat picture at the time the page loads:

let url = 'https://upload.wikimedia.org/wikipedia/commons/c/c1/Six_weeks_old_cat_%28aka%29.jpg';

window.onload = function(e) {
  let img = document.querySelector('#cat-picture');
  img.src = url;
  img.width="300";
};

This code moves the URL from the HTML file to the script file, where it is under program control. We can make the page even more dynamic by loading an image from a web service.

cataas.com site is "Cat as a service". It provides cat pictures as a service via URLs and parameters.

// move this inside the function
let url = 'https://cataas.com/cat';

Reload the page a few times. Do you see a different cat each time?

We can make our code more flexible by moving the code that load the image code to a named function we can call elsewhere.

function loadCatPicture(e) {
  let url = 'https://cataas.com/cat';

  let img = document.querySelector('#cat-picture');
  img.src = url;
  img.alt="a cat image loaded randomly from CATAAS, 'Cat as a service'"
  img.width="300";
}

window.onload = loadCatPicture;

Reload the page a few times. Do you still see a different cat each time?

Timer events

TL;DR: A web page can schedule events to occur at a later time.

Modify the script to update the picture after five seconds:

window.onload = function(e) {
  loadCatPicture(e);

  // Call the loadCatPicture() function again in 5 sec.
  // note: setTimeout() takes milliseconds --  5000 msec === 5 sec.
  setTimeout(loadCatPicture, 5000);
}

Reload the page. Why don't we see a new image after 5 sec?

Our browser is trying to be efficient, but it's getting in our way! It notices that the new URL is the same as the previous URL, so it uses the same image it loaded before. The URL is in its cache.

Let's add something unique (and meaningless) to the URL so that the browser won't cache the URL and so will always load the image again. In loadCatPicture(), add some code to tweak the URL before setting the img.src:

let timestamp = new Date().getTime();
url = url + '?t=' + timestamp;

Reload the page. Do you now see a new image after 5 sec? Yes!

This strategy works because the CATAAS server ignores the extra information on the URL. (We will learn about this kind of name/value pair in URLs, called URL parameters, in a little bit.)

We can also set timer events that run repeatedly after a delay.

Modify the script to update the picture every five seconds:

// remember: setInterval() also takes milliseconds --  5000 msec === 5 sec.
setInterval(loadCatPicture, 5000);

Reload the page. Do you see a new image every 5 sec?

Click events

TL;DR: A web page can respond to the mouse clicks in the window and on arbitrary elements.

Let's return to events caused by the user. We have seen that the page can update the picture after the user clicks anywhere in the window:

window.onload = function(e) {
  loadCatPicture(e);
}

window.onclick = loadCatPicture;

Click in the window to change the image.

We have also seen that the page can update the picture after the user clicks on a specific element, say, the poem.

In the HTML file, add an id to the poem paragraph:

<p id="poem-text">

Then replace the window's click event handler with one for the poem's text:

let poemText = document.querySelector('#poem-text');
poemText.addEventListener('click', loadCatPicture);

Click on the poem to change the image. Clicking elsewhere doesn't change the image.

Key press events

TL;DR: A web page can respond to the user pressing arbitrary keys.

At the bottom of the script, add code that changes the image whenever the user presses a key:

function onKeyPressDo(e) {
  let keyName = e.key;
  console.log('Key Press event', keyName);
  loadCatPicture(e);
}

window.addEventListener('keypress', onKeyPressDo);

Press a few keys and watch the image change.

Now let's modify the code to respond only when the user presses 'm' or 'n':

function onKeyPressDo(e) {
  let keyName = e.key;
  if ((keyName === 'm') || (keyName === 'n')) {
    loadCatPicture(e);
  }
  else {
    console.log('Ignoring keypress event: ', keyName);
  }
}

Press a few keys and watch the image change—or not.

URL parameters

TL;DR: A web server can accept information in the form of parameter strings at the end of a URL.

The cataas.com service supports image filters through the use of URL parameters. Two of its filters are ?filter=mono, which returns a grayscale version of the image, and ?filter=negate, which returns a negative of the image.

Let's modify the script so that it responds to 'm' or 'n' key presses by appending a filter to the URL.

First, let's change the keypress event handler to pass a second argument to loadCatPicture():

function onKeyPressDo(e) {
  let keyName = e.key;
  if (keyName === 'm')  {
    loadCatPicture(e, 'mono');
  }
  else if (keyName === 'n') {
      loadCatPicture(e, 'negate');
  }
  else {
    console.log('Ignoring keypress event: ', keyName);
  }
}

Next, let's change the loadCatPicture() function to use its second argument to append a fliter to the URL string. Notice the use of a template string for the filter.

function loadCatPicture(e, filter) {
  let url = 'https://cataas.com/cat';

  if (filter) {
    console.log('Using picture filter', filter);
    url = url + `?filter=${filter}`;
  }
  else {
    // the cache-busting code from before
    let timestamp = new Date().getTime();
    url = url + '?t=' + timestamp;
  }

  let img = document.querySelector('#cat-picture');
  img.src = url;
  img.alt="a random cat from CatAAS"
  img.width="300";
}

Press the 'm' and 'n' keys and watch the image change.

This approach works because of how JavaScript treats empty strings. If the caller does not pass a second argument, the filter parameter will be the empty string, which JavaScript interprets as false. It interprets non-empty strings as true.

The final result is a nifty little page that illustrates several kinds of events that happen in a web page.

Dogs: Practice

We spent the first part of the session with cats, so it's only fair we spend the rest of the session with dogs.

Open up the 'dogs' index page in VS Code. The rendered page has an h1 heading, a paragraph, and a dog image. It also loads two scripts: one that defines a JavaScript array of URLs for dog images and another empty script file for us to program in. The text on the page says nothing about dogs...

Open the developer tools see the dog! message printed in the log.

Herald the dog! Add an event listener to the window. On a load event, the window will:
  1. change the text of the heading to "Hounds, Hounds, Hounds!"
  2. remove the id attribute from the heading
  3. change the text of the paragraph to "Afghan, basset, and blood hounds"

Hints:

  • The load event is named load.
  • You can use querySelector() to get the heading.
  • You can remove the 'hidden' attribute from the image using removeAttribute().

Look at a solution script running in the web page.

In the developer tools, look at the HTML in Sources and the HTML in Elements.

The source is the view of the static DOM as loaded. The element is the view of the dynamic DOM as executed.

We can fix or expand on the HTML of a web page at load time, or any other time, using JavaScript.

Dogs: More Kinds of Dynamic Behavior

Let's use the dogs page to see a couple of more examples of how we can use JavaScript to create dynamic behavior on a web page.

Window events

TL;DR: A web page can respond to load events at different levels of the DOM.

You may recall from a slide in Session 23 that there is an event named DOMContentLoaded. We can define an event listener for it:

window.addEventListener('DOMContentLoaded', function(e) {
  console.log('window DOMContentLoaded');
});

We can also define an event listener for the dog image's load event:

let dog = document.querySelector('img');
dog.onload = function(e) {
  console.log('dog.onload');
};

Notice the order of the messages in the log. The window's DOMContentLoaded event is triggered before the image's load event.

Events with State

In the in-class lab exercise for Session 23, we saw that events can have state. We can add state to our dog page, too.

Dog CEO offers a dog images as a web service. It returns image URLs as part of a data object. The hounds.js script defines a long list of Afghan hound URLs as a JavaScript array. Let's enable our user to click on the dog image to retrieve the next image in line.

First, let's define a function that returns the next image URL in the list:

let pos = 0;
function getNextDogURL() {
    pos++;
    if (pos === hounds.length) { pos = 0; };
    return hounds[pos];
}

Then let's define a click event listener that calls the function:

function getNextDog(e) {
  dog.src = getNextDogURL();
}

dog.addEventListener('click', getNextDog);

Click on the image a few times. Do you see a different dog each time?

Scripts That Add Elements to a Page

TL;DR: A script can define HTML elements, too.

The hounds array is really long. Perhaps our users would like to see links to all the images so that they can browse randomly.

We did something just like this in Session 24 when we generated a list of IDS bundles and generated a table of city data. Let's do it again for the dogs.

First, let's add a button to the web page along with a place for the list:

<div id="dog-url-list"></div>   <!-- empty to start! -->

<button id="more-dogs">show more dogs!</button>

Next, let's define a function, makeImageList(), that builds an HTML ul element. To do that, we will want to:

  • make a new ol element
  • for each URL in the array:
    • make a new a element for the URL
    • make a new li element for the anchor
    • add the li element to the ol
  • add the ol element to the div

That translates into this JavaScript:

function makeImageList(e) {
  let ol = document.createElement('ol');

  for (let i = 0; i < hounds.length; i++) {
    url = hounds[i];

    let a = document.createElement('a');
    a.href = url;
    a.innerText = url;

    let li = document.createElement('li');
    li.appendChild(a);

    ol.appendChild(li);
  }

  let listDiv = document.querySelector('#dog-url-list');
  listDiv.appendChild(ol);
}

Finally, add this function as a click event listener on the button:

let moreDogsButton = document.querySelector('#more-dogs');
moreDogsButton.addEventListener('click', makeImageList);

Click the button. Do you see the list?

There are a few possible improvements we could make to our web page:

  • The button is still there, all the way at the end of the list. Let's remove it after we generate the list:
    ...
    e.target.remove();
    
  • Clicking on a link open the image in the current tab. Set the anchor to open in a new tab:
    ...
    a.target = '_blank';
    
  • Note: this one is more advanced. Feel free to skip it.
    Displaying the entire URL as the anchor's text is repetitive. Let's pull the image name from the URL and use that as the anchor's text:
    let urlParts = url.split('/');                   // string → array
    a.innerText = urlParts[urlParts.length - 1];     // use last element in array
    

The final result is a deceptively simple page that illustrates two kinds of dynamic behavior that can happen in a web page.

Bonus Challenge: When the user clicks on an item in the list, make that the image displayed at the top, a lá the 'featured image' from our lab exercise.

Bonus Challenge: Use JavaScript to create the button and the div and to add them to the page, rather than editing the HTML of the page directly.

Closing

Here are the final versions of the files:

The final project is ready for your attention. It is due in a few weeks, but don't let the time get away from you. You will want to have some time to create something you like. Feel free to use the examples from today and last week to inspire the dynamic behavior on your pages. Also feel free to ask for guidance or assistance in implementing your code, whether HTML, CSS, or JavaScript.

Midterm Exam 2 is next session: Thursday, November 21.