AJAX Demystified - What Is AJAX?

In Part 1 of this series, we laid the foundation for learning AJAX by digging deep into the history of web development at the time when AJAX was created. In this next part of the series, we will learn what AJAX really is.

What is AJAX?

We learned from the previous blog post that AJAX was created to solve the problem with the "annoying" delay in the responsiveness of a sovereign web application. An AJAX application, therefore, eliminates the start-stop-start-stop nature of interaction on the Web by implementing a collection of four technologies that complement one another: CSS, DOM, JavaScript, and XMLHttpRequest.

  • JavaScript (JS): JavaScript is the glue that holds everything together in an AJAX application. JS is used to define user workflow and business logic of the app, manipulates and refreshes the UI via the DOM to continually redraw and reorganize the data presented to the users based on user's interactions via mouse and keyboard event handling.
  • CSS: In an AJAX application, the styling of a UI may be modified interactively through CSS.
  • DOM: The DOM presents the structure of web pages as a set of programmable objects that can be manipulated with JS. Scripting the DOM allows an AJAX application to modify the UI on the fly, effectively redrawing parts of the page.
  • XMLHttpRequest: The XMLHttpRequest object allows data retrieval from the webserver asynchronously. The data format is typically XML , but it works well with any text-based data. Nowadays, the most common data format used for this is JSON.

It's worth mentioning that three of the four technologies listed above ( CSS, DOM, JS ) have been collectively referred to as Dynamic HTML (DHTML). DHTML offered the ability to create interactive UI for web pages. However, it never overcame the issue of the full-page refresh since without communicating with the server, there was only so much it could do. AJAX makes considerable use of DHTML, but with the ability to make asynchronous requests to the server, AJAX has a lot more to offer.

XMLHttpRequest

XMLHttpRequest is a native API in JS that encapsulates the logic of sending HTTP requests without having to refresh a loaded web page - _ AJAX request. _ Even though developers rarely use XMLHttpRequest directly now, it's still the building block that works underneath many popular HTTP request modules such as Fetch and Axios.

The complete lifecycle of loading a document using XMLHttpRequest is as follows: _instantiate the XMLHttpRequest object, tell it to load a document, and then monitor that load process asynchronously using callback handlers. _

Now that you understand the high-level concept of AJAX, let's jump into some codes.πŸ‘©β€πŸ’» Let's try to implement AJAX using Vanilla JS. Why would we want to do that if there are several frameworks out there that have out-of-the-box AJAX technology? It's because we want to learn "under the hood", remember?πŸ˜‰

AJAX using Vanilla JS

We will be implementing AJAX using Vanilla JS to build a simple web application as shown below. When the Retrieve button is clicked, a "Hello World!" message returned by the backend server via an AJAX call is displayed right below the button without a page refresh. Try it for yourself!πŸ‘‡

A simple AJAX implementation takes four steps:

  • Create an instance of the XMLHttpRequest (aka XHR) object
  • Use the open() method of the XHR to specify what kind of data you want
  • Create a function to process the response
  • Use the XHR's send() method to send the request

Now, let's look at some code. For this simple demo, we have a simple backend web server written in Node.js to process AJAX requests. For the UI on the frontend, we have a simple HTML and a script for the Retrieve button's event handling.

Node.js Backend Web Server

The code for our Node.js web server is defined in the index.js file located in the root directory.

var http = require("http");
var fs = require("fs");

function render(path, contentType, fn) {
  fs.readFile(__dirname + "/" + path, "utf-8", function (err, str) {
    fn(err, str, contentType);
  });
}

//create a server object:
http
  .createServer(function (req, res) {
    var httpHandler = function (err, str, contentType) {
      if (err) {
        console.log(err);
        res.writeHead(500, { "Content-Type": "text/plain" });
        res.end("An error has occured: " + err.message);
      } else {
        res.writeHead(200, { "Content-Type": contentType });
        res.end(str);
      }
    };
    if (req.url.indexOf("/scripts/") >= 0) {
      console.log("Serving ajax.js");
      render(req.url.slice(1), "application/javascript", httpHandler);
    } else if (
      req.headers["x-requested-with"] === "XMLHttpRequest" &&
      req.headers["x-vanillaajaxwithoutjquery-version"] === "1.0"
    ) {
      console.log("Processing AJAX request");
      res.writeHead(200, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ message: "Hello World!" }));
    } else {
      console.log("Serving index.html");
      render("views/index.html", "text/html", httpHandler);
    }
  })
  .listen(8080); //the server object listens on port 8080

Here, we're using the http module to create an HTTP server. For those of you that are not familiar with Node.js, here is a brief explanation of the code.

The server is listening on Port 8080. The callback function we pass to the createServer() method is executed for each request that comes in. Whenever a new request is received, the request event is called, providing two objects: a request (an http.IncomingMessage object) and a response (an http.ServerResponse object). The request provides the request details through which we can access the request headers and request data. The response is used to populate the data returning to the client.

var http = require("http");

//create a server object:
http
  .createServer(function (req, res) {
    // callback to handle request is define here
  })
  .listen(8080); //the server object listens on port 8080

Let's look at the callback function used to handle requests.

if (req.url.indexOf("/scripts/") >= 0) {
  render(req.url.slice(1), "application/javascript", httpHandler);
} else if (
  req.headers["x-requested-with"] === "XMLHttpRequest" &&
  req.headers["x-vanillaajaxwithoutjquery-version"] === "1.0"
) {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ message: "Hello World!" }));
} else {
  render("views/index.html", "text/html", httpHandler);
}

This block of code checks the request URL to determine how the app should respond.

  • If the request came from the scripts directory, then the appropriate file is served with the application/javascript content type.
  • Any request with x-request-with headers set to XMLHttpRequest indicates an AJAX request. In this case, we simply return a "Hello World!" text message.
  • If it's neither of the two cases above, then the index.html file is served.

We haven't discussed the render() and httpHandler() functions yet, so let's do that now. The render() function asynchronously reads the contents of the requested file. It accepts the httpHandler() callback function, which is used to check for errors and write the content of the file to the response with the appropriate HTTP status code if everything looks good.

function render(path, contentType, fn) {
  fs.readFile(__dirname + "/" + path, "utf-8", function (err, str) {
    fn(err, str, contentType);
  });
}

<!--kg-card-end: markdown--><!--kg-card-begin: markdown-->

var httpHandler = function (err, str, contentType) {
  if (err) {
    console.log(err);
    res.writeHead(500, { "Content-Type": "text/plain" });
    res.end("An error has occured: " + err.message);
  } else {
    res.writeHead(200, { "Content-Type": contentType });
    res.end(str);
  }
};

Frontend UI Code

Now, let's look at our frontend code, which contains the /views/index.html file and the /scripts/ajax.js script used for the button event handling. The index.html is straightforward. It references the ajax.js script which is called when the user clicks the Retrieve button.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Vanilla Ajax without jQuery</title>
  </head>
  <body>
    <h1>Vanilla Ajax without jQuery</h1>
    <button id="retrieve" data-url="/">Retrieve</button>
    <p id="results"></p>
    <script src="/scripts/ajax.js" async></script>
  </body>
</html>

The ajax.js script is where most of the AJAX magic happens.

(function () {
  var retrieve = document.getElementById("retrieve"),
    results = document.getElementById("results"),
    toReadyStateDescription = function (state) {
      switch (state) {
        case 0:
          return "UNSENT";
        case 1:
          return "OPENED";
        case 2:
          return "HEADERS_RECEIVED";
        case 3:
          return "LOADING";
        case 4:
          return "DONE";
        default:
          return "";
      }
    };
  retrieve.addEventListener("click", function (e) {
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function () {
      console.log(
        "Inside the onreadystatechange event with readyState: " +
          toReadyStateDescription(xhr.readyState)
      );
      if (xhr.readyState === 4 && xhr.status === 200) {
        if (xhr.responseType === "json") {
          results.innerHTML = xhr.response.message;
        } else {
          results.innerHTML = JSON.parse(xhr.responseText).message;
        }
      }
    };
    xhr.open("GET", e.target.dataset.url, true);
    xhr.responseType = "json";
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.setRequestHeader("x-vanillaAjaxWithoutjQuery-version", "1.0");
    xhr.send();
  });
})();

We followed the five steps to implement AJAX described in the previous sections:

  1. Create an xhr instance of the XMLHttpRequest object using the XMLHttpRequest() constructor.

  2. Use the open() method of the XMLHttpRequest object to specify the kind of data being requested. It takes three arguments:

  3. Create a callback function to process the response. XHR has two properties used to indicate a response from the server:

  4. Finally, all we're left to do is send the request. But before we do that, we need to make sure we set the _ request headers _ accordingly to indicate that our request is indeed an AJAX request.

Tie it all together

Let's tit it all together - frontend and backend. When we start the server by running npm run start, the server returns the rendered index.html page to the browser. Since the index.html file references the ajax.js script, the browser issues another request to the server to get this file. Finally, when the user clicks the Retrieve button, the server returns the "Hello World!" message. This workflow can be observed via the server's terminal logger as shown below.

Serving index.html
Serving ajax.js
Processing AJAX request

AJAX Implementations

Below are some examples of AJAX implementations for your references:

  • Auto-suggestion: Almost all search engines nowadays have this feature implemented. However, it's not limited to just search engines. In fact, it can be used in any text box in web forms.
  • Infinite scrolling: Infinite scrolling is also implemented using AJAX. As the user scrolls downwards, more contents are fetched from the server and displayed on the page.
  • Self-Updating Stock Ticker: Most trading sites have the stock tickers updated with the latest rates on the market automatically without reloading the entire page.
  • Form Validation: For example, when a user enters an email address to sign up for an account, the user's input is sent to the database to check if it's an existing account. Depending on the server's response, the user is either allowed to proceed to the next step or an error is displayed on the screen. This whole process can be done asynchronously without reloading the whole page.

24