Topic 4: Using the Document Object Model

Querying and Manipulating HTML documents using the DOM

So far we have looked at how to parse JSON returned from web APIs, and populate the front end with the data contained within the JSON. We have also looked at how to send POST requests to the server via AJAX. However, we can build more sophisticated front-end applications using other techniques. Next week you will be introduced to the React framework for front-end development, however this week we will start by taking a look at the underlying mechanism for page manipulation, the Document Object Model (DOM).

Previously you've seen the innerHTML property which can be used to read, or change, the text within an HTML element. You've also looked at simple use of the Document Object Model (DOM) to access elements on a web page - specifically the use of getElementById() to access a specific page element using its ID.

However that's just the start: the DOM offers a whole range of ways to read and manipulate HTML pages or XML data. To understand how you can use DOM for document manipulation, you must understand the concept of nodes, which we will discuss below. The examples below use the DOM to manipulate HTML documents, however, more generally, the DOM is used for accessing and manipulating XML documents. An HTML web page is a particular, specific type of XML document. So, as well as using the DOM to query and manipulate web pages, we can use it in a more general sense to query and manipulate XML. In AJAX, this latter use of the DOM is used extensively.

The Concept of Nodes

Example of Nodes Terminology

<body>

<p> Welcome to the <em>wonderful!</em> world of dynamic text!</p>
</body>
  • The paragraph is a child node of the body
  • The paragraph contains three of its own child nodes:
  • The em itself contains one child node:
  • Nodes Hierarchy Diagram: Nested view

    <body>
    <p> Welcome to the <em>wonderful!</em> world of dynamic text!</p>
    
    </body>
    
    Nodes

    Nodes Hierarchy Diagram: Tree view

    <body>
    <p> Welcome to the <em>wonderful!</em> world of dynamic text!</p>
    
    </body>
    
    Nodes

    Adding a new node

    Example

    Replacing an existing node

    Example

    Getting all elements of a particular type

    Example

    Removing nodes

    Other useful features of the DOM

    (Source: Quirksmode, a very useful reference site for JavaScript and the DOM)

    Inspecting elements

    The inspect functionality in the browser will help you see what's going on - try it as you do the exercise for this week - right click on an element. Inspecting helps you to see the actual current DOM structure of the page - including any dynamically-created elements.

    Binding

    So far, we have been mostly using anonymous arrow functions as our event handlers, such as for button clicks. However, it's possible you might want to connect multiple page elements to the same event handler, and in this case, you would use a named rather than an anonymous event handler function. A good example could be the "like" functionality, which could be linked to both a "Like" button and a "thumbs-up" image. You could have a function to like a product, such as:

    function likeProduct(productID) {
        // send a request to the server to like the product with that ID
    }
    
    You could then supply this function to a button's addEventListener() as a callback function, e.g.:
    document.getElementById('likeButton').addEventListener("click", likeProduct);
    and
    document.getElementById('thumbsUpImage').addEventListener("click", likeProduct);
    This would indicate that likeProduct() would be called when the button or "thumbs up" image is clicked, at some future point in time. However, how might you pass across the product ID? You might possibly think you could do something like this:
    document.getElementById('likeButton').addEventListener("click", likeProduct(productID));
    However, this code would be wrong. This code will call likeProduct() immediately, not when the button is pressed. Why? When a function is specified with parentheses, e.g. likeProduct(productID), it is immediately called. By contrast, likeProduct without parentheses merely supplies the function as an argument, without calling it. It will be called at some future point in time, i.e. when the button is clicked.
  • So how can we pass arguments to a callback? We can do this by using binding. Binding allows us to bind specified arguments to a callback which will automatically be received when the function runs. This is easy to do, using JavaScript's bind() function. Here is an example:
    document.getElementById('likeButton').addEventListener("click", likeProduct.bind(this, productID));
    Note how we call bind() on the callback function and pass across this followed by a list of arguments to be bound. bind() returns a new version of the callback, with the appropriate arguments bound. The first argument, this here, allows us to specify the context of this in the callback, which is mostly of use in object-oriented programming. See the advanced material below for more detail.

    Exercise

    Ensure you have completed questions 1 to 4 from last week first.

    The main exercise is to add "Buy" functionality to your AJAX front-end fron last week, so that a user can buy a given song. Songs will be bought by sending an AJAX POST request to the "buy" route of your web API.

    Modify your AJAX front-end so that there is a "Buy" button for each search result. When the user clicks on the "Buy" button, you send an AJAX POST request to your "buy" route from week 1. To do this:

    Standard Questions

    1. For each song (i.e. within the forEach), create a paragraph using document.createElement(), and set its innerHTML to the details of that song, taken from the JSON. The basic logic you are aiming for is:
      songs.forEach ( song => {
          // Create the paragraph 
          // Set the innerHTML of the paragraph to the song details
          // Add the paragraph to the <div> (question 2)
          // Create a "Buy" button, with event handler (question 3 onwards)
      });
      
    2. Using appendChild(), append that paragraph to the results <div>.
    3. Create a button using the DOM, specifically document.createElement() (see above). You should create an input element and set its type to button using setAttribute(), e.g:
      const buttonElement = document.createElement("input"); 
      buttonElement.setAttribute("type", "button");
    4. Specify the text on the button by setting its value with setAttribute(), in the same way that you set the type to button.
    5. Using addEventListener() to add an event handler to the button. The event handler should be an arrow function. We did this last week with AJAX. For example:
      //Note the arrow function needs to be async if we are using await
      buttonElement.addEventListener ("click", async(e) => 
          { 
              // fill in your arrow function here.
              // It should call your "buy" web API route, passing in the ID of the current song.
          } 
      );
      
    6. In the button's event handler, send a POST request to the buy route on your server. Obtain the ID from the JSON returned from the search. For now, you do not need to pass any POST data as JSON to the route, just add the ID to the URL.
    7. Add the button you have created to your results <div> using appendChild() to append it.
    8. Add code to the AJAX front end to handle HTTP status codes other than 200. If they occur, display an appropriate message to the user with alert().

    More advanced questions

    1. In your Express server, modify your buy route from week 1 to allow the client to buy more than one copy of the song. The quantity to buy should be passed to the server as JSON POST data within the request body. You'll need to change the UPDATE statement to reduce the quantity by the specified amount.
    2. On the AJAX front end, using the DOM, add a quantity text field for each song, allowing the user to specify the quantity to buy. Give each text field an ID containing the song ID, e.g. song445 for the song with the ID 445. Then, in the function which sends the AJAX POST request to the server, add code to read the quantity from the text field and send it as JSON POST data to the server.
    3. Further modify your buy route so that it rejects the request with a 400 (Bad Request) error if the quantity requested from the client is 0 or less. Handle this error on the client with an alert() box.
    4. Rewrite your code to use a separate buySong() function, called when the user presses the Buy button, using bind().