AJAX HTML Responses

Chapter 6 11 mins

Learning outcomes:

  1. Working with HTML responses
  2. HTML as a string
  3. HTML as an object

Introduction

In the previous AJAX Response Handling chapter we learnt a solid amount of stuff on how to handle AJAX responses and work with the data sent along. In the discussion we specifically explored text files, JSON and lastly XML. We also left you to think a bit on how HTML might be handled in AJAX.

If you did put in the thought and came up with some intuition on the topic that's good and partially desirable for this chapter.

Here we'll see how to work with the most common response format of the web i.e HTML. We'll understand how to parse HTML into a tree, work with HTML inside strings, extract given information from them and a lot more on this way. Let's enroll!

HTML as a string

Recall from the previous chapter, how AJAX responses settle inside responseText by default, once they are received completely. The property holds the response as a text value i.e as a string.

While textual representations work well with text and JSON files, HTML is not a friend of text. You can't do much if you've got some HTML as a string value - for example you can't traverse up or down elements, extract their inner content or change values of attributes; at least without a heck of a work!

HTML works best with an object representation, technically called a DOM Model. Being able to convert textual HTML into a DOM tree, referred to as parsing the HTML, is always the best deal if you can get it. You can learn more about the structure of this model in the JavaScript HTML DOM Tree chapter.

Fortunately, the restaurant AJAX does offer this scrumptious meal to you and you'll more than likely be well off with buying and eating it. (Sorry to get you into food related analogy.)

But discussions aren't always best; sometimes we need real examples as well to illustrate the whole idea, just like we'll do over here. Consider the following snippet of the file foods.html:

<!DOCTYPE html>
<html>
<head>
<title>Foods</title>
</head>
<body>
<h1>My favourite foods</h1>
<p>Pizza for $6.50</p>
<p>Chocolate for $3.00</p>
</body>
</html>

We'll fetch this file using AJAX, on the click of a button, and then display its content using document.write() and responseText.

Live Example

The method document.write() first parses its argument string and then writes it to the document. Likewise responseText was able to go well with this method, in the example above.

Let's consider another example.

Suppose we have two pages home.html and about.html shown below:

home.html
<!DOCTYPE html>
<html>
<head>
<title>Home</title>
<script src="someScript.js"></script>
</head>
<body>
<h1>Hello World!</h1>
<p>This is the home.</p>
<p><a href="about.html" id="about">About us</a></p>
<script src="ajax-navigation.js"></script>
</body>
</html>
about.html
<!DOCTYPE html>
<html>
<head>
<title>About</title>
<script src="someScript.js"></script>
</head>
<body>
<h1>About us</h1>
<p>Some information over here.</p>
</body>
</html>

A user on home.html goes to about.html using AJAX-based navigation. When clicked on the link, a request is made to about.html, its response received completely and then finally injected into the document using document.write().

ajax-navigation.js
document.getElementById("about").onclick = function(e) {

   e.preventDefault(); // prevent from opening link

   var xhr = new XMLHttpRequest();
   xhr.open("GET", this.href, true);

   xhr.onreadystatechange = function() {
      if (this.readyState == 4 && this.status == 200) {
         document.write(this.responseText);
      }
   }

   xhr.send();

}

Although this idea, works perfectly there are some hidden problems in it that make it quite inefficient and practically useless.

First of all writing the content of about.html using document.write() has the effect of reparsing someScript.js. Since, nothing is different in both the HTML pages apart from their body content it doesn't make any sense to reparse the entire script, at least in this case.

However, if you do need to reparse it, even then document.write() has its problems present.

For instance, if the script executes on the onload or DOMContentLoaded events, document.write() won't fire these events and thus leave the script indirectly unattended.

Moreover this method doesn't normally parse its argument string - each script or stylesheet doesn't block the render of the HTML. If you know about this concept of render-blocking execution you'll realise that this would mean seeing unstyled content unless the stylesheet, or script, loads completely.

In short, document.write() is near useless for complex and smooth AJAX navigations around the web. Had it been this easy to work with HTML responses, we wouldn't have seen applications operating on thousands of lines of code to handle the responses fluidly! Believe it!

This example clearly clarifies one important fact about the document.write() method - it shall not be used in complex applications at any cost.

Developers have to come up with other methods to tackle the HTML and write it to the document. The methods have to allow extracting portions of the HTML quickly and easily and update only those pieces of content that actually need to be updated.

Extending to this conversation, what if we just don't want to write the whole HTML to the document - what if we only want to extract some content out of it? In these cases there has to be a simpler way to traverse through the entire HTML, once again quikly and efficiently. Is there any such way? Well definitely yes!

HTML as an object

As said in the previous chapter, just like we can use responseXML to get an object representation of a requested XML document, we can use this property for HTML documents too.

However the following code gives an unexpected result when run:

var xhr = new XMLHttpRequest();
xhr.open("GET", "foods.html", true);
xhr.onreadystatechange = function() {
   if (this.readyState == 4 && this.status == 200) {
      console.log(this.responseXML); // null
   }
}
xhr.send();

Instead of getting a DOM object returned in line 5, we get a null return value which indicates nothing went inside responseXML.

Now this happens not because the HTML was invalid or some other network error was incurred, but due to the absence of the responseType property.

The responseType property

The property responseType tells the browser how to handle the response to a given request. By default, its value is set to "text" which means to extract the response as a text value.

Try to make sense out of the property responseType from its name: it tells us about the type of the response. Such a descriptive name!

Other values include "arraybuffer", "blob", "document" and "json".

The value we are interested in right now is "document". It tells the browser to take the response as a document i.e an HTML document tree.

Once we've set this property's value to "document", we can then dispatch out our request and be rest assured that the response will be available inside responseXML as a parsed DOM object.

Following we fetch the file foods.html once again but this time instead of writing it entirely using the document.write() method, we extract out its content from <body> and finally print it to the current document's <body> element.

var xhr = new XMLHttpRequest();
xhr.open("GET", "foods.html", true);
xhr.responseType = "document";

xhr.onreadystatechange = function() {
   if (this.readyState == 4 && this.status == 200) {
      var respDocument = this.responseXML; //  the parsed HTML document
      document.body.innerHTML = respDocument.body.innerHTML;
   }
}

xhr.send();

First we get the innerHTML of the body of the response HTML document which is saved into the variable respDocument and then put it inside the body element of the current document - by assigning it to document.body.innerHTML.

Live Example

In the example above, we only replaced the body of the current document using the body of foods.html, however left of its title.

Your task is to write some additional code to change the current document's title to the one for foods.html.

To change the title of the current document with the one for foods.html we just need to assign the property document.title the value respDocument.title, which is the title of the requested foods.html document.

Following is the previous code with the added statement to update the title as well:

var xhr = new XMLHttpRequest();
xhr.open("GET", "foods.html", true);
xhr.responseType = "document";

xhr.onreadystatechange = function() {
   if (this.readyState == 4 && this.status == 200) {
      var respDocument = this.responseXML; //  the parsed HTML document
      document.title = respDocument.title;
      document.body.innerHTML = respDocument.body.innerHTML;
   }
}

xhr.send();

In conclusion

Summarising all this discussion, if you have to work with HTML in AJAX as a DOM tree, then you ought to know about the responseType property and its role in sending a parsed DOM object into responseXML.

Perhaps this is the most ideal way of working with HTML as an object, otherwise one can go with other parsing mechanisms such as DOMParser() or the poor performant regular expressions.

HTML is the core of the web, undoubtedly the core of AJAX navigation, and not surprisingly, a format whom XMLHttpRequest() well entertains these days! Get solid amounts of practice in working with HTML and soon you'll find yourself close to real professional programmers!