facebook youtube pinterest twitter reddit whatsapp instagram

Guide To AJAX (Asynchronous JavaScript and XML) and Using The XMLHttpRequest API

In this guide, you'll learn about AJAX, and more specifically how to implement an AJAX technique using XMLHttpRequest.

So, What is Ajax?

AJAX a.k.a Asynchronous JavaScript and XML is not necessarily a technology or a tool, it is merely a technique that describes the pattern of communicating with servers. To achieve the actual technique, we use the XMLHttpRequest API to communicate with servers. So, what does that do?

Using this API, you can send and receive information using various formats such as JSON, XML, text files, and HTML asynchronously. Asynchronously means you don't have to wait for one request to finish before the other runs, it is not synchronous; not occurring or existing at the same time or having the same period.

An example of this is exchanging data and updating the page without having to refresh the page, here is a real-life example of an asynchronous page:

As you can see, as I scroll through the page, the data are been updated without having to refresh the page. The updates are done behind the scene, which is cool, although, you wouldn't want to turn all your page into an Ajax which is what they did in the video above, you can simply just do it for a specific section of my page, maybe loading more blog post without having to refresh or just to load more data in general.

The opposite of asynchronous is synchronous, for example, if you click a button to load more posts, the request is sent to the server, and the response is returned. You cannot interact with any other page elements until the response is received and the page is updated.  Here is a real-life example:

You can see we have to wait for the server to handle the request, we then get a result. I hope you get the differences. Another thing I want to note is that using Ajax doesn't mean you are not sending a request to the server, it still sends a request to the server but only does that for that specific portion you need behind the scene, which is much faster than synchronous.

Using XMLHttpRequest(XHR) Object Methods With Plain Text

Okay, so, this is what we are going to do, we would start working with plain text, and we would create an XHR request that loads the text data into our web page asynchronously when a button is clicked.

I'll be using the halfmoon CSS framework styling for this, so, in the HTML, add the following:

Note: You can use whatever CSS framework you want or you can simply use your CSS styles, this is just for illustration purposes.

<body>
<div class="content-wrapper text-center" id="container">
  <div class="content-wrapper text-center">
    <div class="container-fluid">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-md-8">
            <div class="card bg-light">

              <div class="card-body">
                <h2 class="card-header">AJAX Request</h2>
              </div>
              <div id="form-container" class="container-fluid">
                <div class="row justify-content-lg-start">

                  <!-- Submit Button -->

                  <div class="col-6 col-sm-5 col-md-4 col-lg-3 m-auto">
                    <input class="btn mt-10" id="load-text" type="submit" value="Load Plain Text">
                  </div>

                <!-- Text would be added here-->
                  <div id="text"></div>

                </div>
              </div>

            </div>
          </div>
        </div>
      </div>
    </div>

  </div>
</div>

</body>
<script src="js/main.js"></script>

The above is just the body element, so, you might want to add the necessary stuff in your HTML head tag, also, create a js folder, and create a main.js file. Lastly, create a folder name - "data", and inside it create a file text.txt and add whatever you want in there, I added the following:

This is an ajax request, not that bad if you ask me, or is it

This is how the HTML structure looks like:

3. HTML Structure for Ajax

To start with, we add an event listener to the button, we then instantiate the XHR object like so:

// Create Event Listener For the Load Plain Text Button
document.getElementById('load-text').addEventListener('click', function (e){

    // Once the click event occur, we instantiate XHR Object
    const XHR = new XMLHttpRequest();

    }

    e.preventDefault() // Prevent default behaviour of the button if there is any
})

Once we are done initializing, we can do:

// Create Event Listener For the Load Plain Text Button
document.getElementById('load-text').addEventListener('click', function (e){

    // Once the click event occur, we instantiate XHR Object
    const XHR = new XMLHttpRequest();

    /*
      The first parameter is the request you want,
      the second is the url or the location the file is located, and
      the third takes true or false, true meaning you want async, and false otherwise
      */
    console.log(XHR.readyState)
    XHR.open('GET', 'data/text.txt', true) // Open the request
    XHR.send(); // Send the request

    XHR.onreadystatechange = function () { // Callback function

        // onreadystatechange property defines a function to be executed when the readyState changes.
        if (XHR.readyState === XMLHttpRequest.DONE) {
            if (XHR.status === 200) {
                document.getElementById('text').innerHTML = `<p>${XHR.responseText}</p>`;
            } else {
                alert('Error Returning The Request');
            }
        }
    }
    e.preventDefault() // Prevent default behaviour of the button if there is any
})

Before I explain how it works, this is the result:

4. XHR Object Methods With Plain Text

We got our data loaded asynchronously with no form of waiting for the entire page to reload before getting the data.

These are the steps we took:

  • We instantiate a new XHR object
  • We use open to specify the type of request we want to send, since we are requesting for a file, we use the GET request, we specify the file we want to open, and we set async to true, we then use send() to send the request
  • The readystatechange holds the status of the XMLHttpRequest, so, the onreadystatechange property defines a function to be executed when the readyState changes.
  • So, we said if (XHR.readyState === XMLHttpRequest.DONE), meaning if the request finished and response is ready, we do the next line, and that is
  • checking if the XHR status is 200, meaning if the HTTP status code is OK with no error, we then access the data
  • In our case, we did:  document.getElementById('text').innerHTML = `<p>${XHR.responseText}</p>`;which returns the server response as a string of text
  • If the HTTP status code is other than 200, we alert an error: alert('Error Returning The Request');

Here is a table for the readystate values:

onreadystatechangeDefines a function to be called when the readyState property changes
readyState ↓Holds the status of the XMLHttpRequest.
(0)request not initialized
(1)server connection established
(2)request received
(3)processing request
(4)(complete) or (request finished and response is ready)

and here is a table for basic HTTP status code:

Full list here

HTTP status codeIndicate whether a specific HTTP request has been successfully completed or not
200OK
301Moved Permanently
403Forbidden
404Not Found
500Internal Server Error
503Service Unavailable

To simulate the readystate code, we can put a marker before the request has not been initialized to the point it got initialized, and completed:

5. XHR.readyState Simulation

So, when we had:  if (XHR.readyState === XMLHttpRequest.DONE)we are saying if the readystate equals 4. I hope you get that now.

So, instead of doing:

if (XHR.readyState === XMLHttpRequest.DONE) {
if (XHR.status === 200) {
document.getElementById('text').innerHTML = `<p>${XHR.responseText}</p>`;
} else {
alert('Error Returning The Request');
}
}

You can also do:

XHR.onreadystatechange = function () { // Callback function

    // onreadystatechange property defines a function to be executed when the readyState changes.
    if (XHR.status !== 200 && XHR.readyState !== 4) {
            alert('Error Returning The Request');
        } else {
            document.getElementById('text').innerHTML = `<p>${XHR.responseText}</p>`;
        }
}

It would still give you the same result, it's all about the logic.

If you want to use a spinner or progress bar before the request completes, you can use the .onprogress method, something like this:

// listen for `progress`, u can use for spinners
XHR.onprogress = function ()  {
    console.log('Progressing');
}

You can also listen for an event like so:

    // listen for `progress` event, u can use for spinners
    XHR.onprogress = function (e)  {
        // e.loaded returns how many bytes are downloaded
        // e.total returns the total number of bytes
        console.log(`Uploaded ${e.loaded} of ${e.total} bytes`);
    }

6. onprogress event

and yes, the file size is 61 bytes:

7. text.txt file size

So, you can really do amazing stuff with XHR. However, the onprogress method is a bit confusing, and you might not get the result you expected, I'll show you the way I do the spinner in a later section.

In the event of a communication error (such as the server going down), an exception will be thrown in the onreadystatechange method when accessing the response status. You could wrap if..then statement in a try...catch like so:

XHR.onreadystatechange = function () { // Callback function

    // onreadystatechange property defines a function to be executed when the readyState changes.
    try {
        if (XHR.status !== 200 && XHR.readyState !== 4) {
            alert('Error Returning The Request');
        } else {
            document.getElementById('text').innerHTML = `<p>${XHR.responseText}</p>`;
        }
    } catch (e) {
        alert('Something Went Wrong: ' + e.description);
    }
}

Using XMLHttpRequest(XHR) Object Methods With JSON

Working with JSON is no different from plain text, except that you need to parse the JSON data. For this section, we would be using a fake JSON API, so, this time we would be testing an external fake JSON API, as you would most likely be working with an external API.

Go to https://jsonplaceholder.typicode.com/and scroll to the resources section to see a couple of examples you can play with, I would be using the users' resources, which is here: https://jsonplaceholder.typicode.com/usersby default it gives you an array of 10 users, to access user one, you can simply add 1 to the URL like this: https://jsonplaceholder.typicode.com/users/1

We can put this into action, I'll be using the following HTML structure:

<body>
<div class="content-wrapper text-center" id="container">
  <div class="content-wrapper text-center">
    <div class="container-fluid">
      <div class="container">
        <div class="row justify-content-center">
          <div class="col-md-8">
            <div class="card bg-light">

              <div class="card-body">
                <h2 class="card-header">Ajax - Working With JSON Data</h2>
              </div>
              <div id ="form-container" class="container-fluid">
                <div class="row justify-content-lg-start">

                  <!-- Form Input -->

                  <form id="json-one-user" class="form-group m-auto">
                    <label class="w-300" for="user-id"></label>
                    <input autofocus class="form-control " id="user-id" placeholder="Enter User ID: 1 - 10"
                           type="text" value="">
                    <!-- Get Single User Submit Button -->
                    <input class="btn mt-10" id="one-json-data" type="submit" value="Get User">
                    <p class="mt-5m-auto">OR</p>
                  </form>

                  <!-- Get All User Submit Button -->
                    <div class="justify-content-center m-auto">
                      <input class="btn mt-10" id="all-json-data" type="button" value="Get All Users">
                    </div>

                  <div id="loading" class="mt-20 container-fluid"></div>

                  <!-- Table To Hold The Music List -->

                  <!-- Inner bordered table -->
                  <table class="table table-inner-bordered">
                    <thead>
                    <tr>
                      <th>#</th>
                      <th>Name</th>
                      <th>Address</th>
                    </tr>
                    </thead>
                    <tbody id="user-list">
                    <!-- We would dynamically insert the tbody row data -->
                    </tbody>
                  </table>

                </div>
              </div>

            </div>
          </div>
        </div>
      </div>
    </div>

  </div>
</div>

</body>
<script src="js/main.js" ></script>

The structure looks like this:

8. Ajax - Working With JSON Data Structure

Let's work with the single user input first, for that, I have:

document.getElementById('json-one-user').addEventListener('submit', function (e){

    // Once the click event occur, we instantiate XHR Object
    const XHR = new XMLHttpRequest();

    // Get values entered into the input box
    const userID = document.getElementById('user-id').value;

    XHR.open('GET', `https://jsonplaceholder.typicode.com/users/${userID}`, true)
    document.getElementById('user-id').value = '' // Clear the input
    XHR.send();

    XHR.onreadystatechange = function () { // Callback function
        // onreadystatechange property defines a function to be executed when the readyState changes.
        try {
            if (XHR.readyState === XMLHttpRequest.DONE) {
                if (XHR.status === 200) {
                    const user = JSON.parse(XHR.responseText);

                    const list = document.getElementById('user-list');
                    // Create tr element
                    const row = document.createElement('tr');

                    // Insert cols
                    row.innerHTML = `
                  <td>${user.id}</td>
                  <td>${user.name}</td>
                  <td>${user.email}</td>`;
                    list.appendChild(row);

                } else {
                    alert('Error Returning The Request');
                }
            }
        } catch (e) {
            alert('Something Went Wrong: ' + e.description);
        }
    }
    e.preventDefault();
});

This is no different from the other examples, except with a couple of tweaks:

  • For the single user, I listen to submit event type, which would only fire when a form is been submitted. So, I targeted the form ID: json-one-user
  • Once the click event occurs, we instantiate XHR Object
  • We get the value entered into the input box: const userID = document.getElementById('user-id').value;
  • We then open and send the request depending on which ID the user enters
  • For the onreadystate, we didn't do anything different than what we've been doing before, except that we parsed the returned JSON, and insert it into the table columns.

Here is a visual illustration of how it works:



Cool. But if you are like me, you might want to add a spinner while the request is processing, and as you can see, if you pass a request that is not available, you get an error, so you can add a spinner. I got one from flaticon for this illustration, so, create folder name, icon, and add the spinner in the folder, rename the spinner to loading.svg.

Create a CSS/app.css folder, and add the following:

.icon {
    display: inline-block;
    width: 50px;
    height: 50px;
    animation: rotation 1s infinite linear;
}

@keyframes rotation {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(359deg);
    }
}

Don't bother adding a container to the HTML, I already added one right off bat, so, we can then do this in our JS file:

document.getElementById('json-one-user').addEventListener('submit', function (e){

    // Once the click event occur, we instantiate XHR Object
    const XHR = new XMLHttpRequest();

    // Spinner
    const image = document.createElement('img');
    const divLoader = document.getElementById('loading');

    image.setAttribute('src', 'icon/loading.svg') // Set src attribute
    image.className = "icon";
    // Append the image to div loader
    divLoader.appendChild(image);

    XHR.onload = function (){
        let geticon = document.querySelector('.icon');
        geticon.remove()
    }

    // Get values entered into the input box
    const userID = document.getElementById('user-id').value;

    XHR.open('GET', `https://jsonplaceholder.typicode.com/users/${userID}`, true)
    document.getElementById('user-id').value = '' // Clear the input
    XHR.send();

    XHR.onreadystatechange = function () { // Callback function
        // onreadystatechange property defines a function to be executed when the readyState changes.

        try {
            if (XHR.readyState === XMLHttpRequest.DONE) {
                if (XHR.status === 200) {

                    const user = JSON.parse(XHR.responseText);

                    const list = document.getElementById('user-list');
                    // Create tr element
                    const row = document.createElement('tr');

                    // Insert cols
                    row.innerHTML = `
                  <td>${user.id}</td>
                  <td>${user.name}</td>
                  <td>${user.email}</td>`;
                    list.appendChild(row);

                } else {
                    alert('Error Returning The Request');
                }
            }
        } catch (e) {
            alert('Something Went Wrong: ' + e.description);
        }
    }

    e.preventDefault();
});

Immediately after instantiating the XHR object, we added the spinner, and we use the onload method to remove the spinner once the process is completed, so, instead of checking the progress of the response, we loaded a spinner immediately the user clicks the button, and we ended it whenever the request is completed using the onload method:

XHR.onload = function (){
        let geticon = document.querySelector('.icon');
        geticon.remove()
 }

.onload is the method called when an XMLHttpRequest transaction completes successfully.

This is the end result of that:

10. Spinner for getting single user

Awesome. For the Get All Users Button, we add the following event listener and function:

document.getElementById('all-json-data').addEventListener('click', function (e){

    // Once the click event occur, we instantiate XHR Object
    const XHR = new XMLHttpRequest();

    // Spinner
    const image = document.createElement('img');
    const divLoader = document.getElementById('loading');

    image.setAttribute('src', 'icon/loading.svg') // Set src attribute
    image.className = "icon";
    // Append the image to div loader
    divLoader.appendChild(image);

    // Remove the spinner once the request is completed
    XHR.onload = function (){
        let geticon = document.querySelector('.icon');
        geticon.remove()
    }

    XHR.open('GET', 'https://jsonplaceholder.typicode.com/users', true)
    XHR.send();

        XHR.onreadystatechange = function () { // Callback function
        // onreadystatechange property defines a function to be executed when the readyState changes.

            try {
                if (XHR.readyState === XMLHttpRequest.DONE) {
                    if (XHR.status === 200) {

                        const users = JSON.parse(XHR.responseText);

                        const list = document.getElementById('user-list');

                        users.forEach(function(user){
                            // Create tr element at each iteration
                            let row = document.createElement('tr');
                            // Insert cols
                            row.innerHTML = `
                    <td>${user.id}</td>
                    <td>${user.name}</td>
                    <td>${user.email}</td>`
                            list.appendChild(row);
                        });

                    } else {
                        alert('Error Returning The Request');
                    }
                }
            } catch (e) {
                alert('Something Went Wrong: ' + e.description);
            }
    }

    e.preventDefault();
})

This is no different from the first one, except that the following request: https://jsonplaceholder.typicode.com/users returns an array of users, so, we need to loop through it, which I did here:

users.forEach(function(user){
// Create tr element at each iteration
let row = document.createElement('tr');
// Insert cols
row.innerHTML = `
    <td>${user.id}</td>
    <td>${user.name}</td>
    <td>${user.email}</td>`

// Append to table
list.appendChild(row);
});

I am putting: let row = document.createElement('tr'); because I want to assign new item as it loops through, if it is not in the loop, you would get only the last item.

So, this is the end result:

11. Spinner for getting all users

That's it. Before I end this guide, I'll love to talk about callback functions as we've used them quite a bit in this guide...

Callback Functions

Callback or call-after function does what it says, it is a function or a set of code that is passed as an argument to other code or function, the other code you are passing the call back to is expected to call back (execute) the function you passed at a given time. It can either be synchronous (immediately) or asynchronous (at a later time, as we've done in the guide).

Here is an example of a simple callback function:

function greeting(firstName, callback) {
    return callback(firstName.toUpperCase());
}

function getUserInfo(firstName) {
    return firstName;
}

alert(greeting('the_devsrealm_guy', getUserInfo));

This is a really simple example:

  • The greeting function took two-arguments, one a firstName and the other a callback, we then return the callback(firstName.toUpperCase()), which would convert anything it receives to an uppercase.
  • The getUserInfo took a single parameter, we then return the firstName
  • we then do: alert(greeting('the_devsrealm_guy', getUserInfo)); which would pop an alert with the name: 'THE_DEVSREALM_GUY'.
  • Here is the thing, you can think of the getUserInfo function as a function that only gets the user info, and you can think of the greeting function as a function that collects that info and does some operation on it, and once it does it, it returns the result to the callback function.

If you still do not get that, let me break the code down a bit more:

function greeting(firstName, callback) {
    firstName = firstName.toUpperCase()
    return callback(firstName);
}

function getUserInfo(firstName) {
    return firstName;
}

console.log(greeting('the_devsrealm_guy', getUserInfo));

// Output => THE_DEVSREALM_GUY

As soon as you do: console.log(greeting('the_devsrealm_guy', getUserInfo)); the getUserInfo function won't be executed, and that is because we are calling the greeting function first, so, it takes what you passed to firstName, which in my case is 'the_devsrealm_guy', and a callback function (getUserInfo), inside the greeting function, it performs the action of converting all the string passed to the firstName to uppercase, once it is done, it return the result by passing it to the callback function.

Do you see what I did there? It is a function that is passed as an argument to another function, to be “called back” at a later time, so, the callback is actually waiting for the greeting function to perform its operation and give it the data.

Here is a funny illustration I did:

Callback function

Haha. Hope you get it now.

Another useful example of a callback, say we need a loop that counts from one to 10:

for (let i = 1; i <= 10; i++) {
    console.log(i);
}

We can actually put this in a function where the function takes an N number:

function countNum(n) {
    for (let i = 1; i <= n; i++) {
        console.log(i);
    }
}

countNum (5);

// Output =>
1
2
3
4
5

What if we want to do something other than logging the numbers? We can use a callback for that:

function countNum(n, action) {
    for (let i = 1; i <= n; i++) {
        action(i);
    }
}

function action(i) {
    alert(i);
}

countNum(5, action);

It would give you five alert; 1, 2, 3, 4, 5. You can see where I am going, the countNum function takes the n parameter and the callback:actionit would call later each time it loops through.

The above examples are synchronous callback, as it is executed immediately. We also have an asynchronous callback, they are often used to continue code execution after an asynchronous operation has completed.

Let's see an example of an asynchronous callback, but let's do a comparison of an actual synchronous code and then we do an asynchronous callback:

For Synchronous:

// Synchronous 
const posts = [
  {title: 'Post One', body: 'This is post one'},
  {title: 'Post Two', body: 'This is post two'}
];

function addPost(post) {
  // Delay for 3 seconds
  setTimeout(function() {
    posts.push(post); // Add new object on to the end of the array
  }, 3000);

}

function getPosts() {
  setTimeout(function() {

    posts.forEach(function(post){
      console.log(`${post.title}`);
    });

  }, 1000);
}

addPost({title: 'Post Three', body: 'This is post three'});

getPosts();

What do you think you would get?

Well, your output would be:

Post One
Post Two

Wondered why the third post didn't get added, this is because the code is synchronous. The addPost function got delayed by three seconds, so, before it could catch up, the getPosts() function has been called even as we delayed the getPosts function for a second.

This is where asynchronous comes in...So...

For Asynchronous:

// Asynchronous Callback
const posts = [
    {title: 'Post One', body: 'This is post one'},
    {title: 'Post Two', body: 'This is post two'}
];

function addPost(post, callback) {
    // Delay for 3 seconds
    setTimeout(function() {
        posts.push(post);
        callback(); // callback the getPosts function
    }, 3000);
}

function getPosts() {
    // Delay for a second
    setTimeout(function() {

        posts.forEach(function(post){
            console.log(post.title);
        });

    }, 1000);
}

addPost({title: 'Post Three', body: 'This is post three'}, getPosts);

For sure, the output is:

Post One
Post Two
Post Three

The addPost function takes in two argument, the first one is the post object, and the second one is the callback. When the addPost is called, it would add the post to the array, and execute the callback function before it times out.

Even if you have a nested timeout like so:

function addPost(post, callback) {
    // Delay for 3 seconds
    setTimeout(function() {

        setTimeout(function () {
            callback(); // callback the getPosts function
        }, 4000)

        posts.push(post);
    }, 3000);
}

The post is still gonna get added after a slight delay. This is a simple example of an asynchronous callback.

Tiny HTTP Library (XHR) ES6 - Class

I wanted to create a new post for this but I decided that I better add it to this guide to keep things organized.

So, in this section, we would create a custom tiny HTTP library that can

  • GET a request - GET is used to request data from a specified resource.
  • POST a request - POST is used to send data to a server to create/update a resource.
  • PATCH a request - PATCH updates part of the resource at that client-defined URL
  • DELETE a request - The DELETE method deletes the specified resource.

https://jsonplaceholder.typicode.com/ let us play with all the above HTTP requests, we can use it to mimic an actual request.

Get a Request

We would create a new file for our library called tinyhttp.js which would contain the library code, and we would create a new file main.js which is where we would use the tinyhttp.js.

In tinyhttp.js, I would start by creating a class with a constructor like so:

// Class for our Tiny HTTP library
class TinyHTTP {

    constructor() { // the constructor
        this.http =  new XMLHttpRequest();
    }

}

The constructor is used to instantiate an object, so, whenever a new instance of the TinyHTTP class is instantiated it immediately instantiate the XMLHttpRequest.

So, the next thing we would be doing is creating a get method in our TinyHTTP class:

// Class for our Tiny HTTP library
class TinyHTTP {

    constructor() { // the constructor
        this.http =  new XMLHttpRequest();
    }

    // GET Method
    Get(url) {
    this.http.open('GET', url, true)
    this.http.send();

        /*
        #
        # The reason why I did accessthis = this; is because "this" is used to access a property of an object
        # but if you do "this.http" in a function, it would have return an "undefined" error, and that is because the "this"
        # in a function has a scope of that function and not of the object. So, in a function, it doesn't know this.http exist
        # To access the this.http, we can use an arrow function or set "this" to a variable, so as to capture it in the object scope.
        #
        # Some people use "let self = this", but, I prefer "let accessthis = this" cos it explains why I am doing it.
        # and in the onreadtstateexchange function, you replace the "this" with "accessthis"
        #
         */
        let accessthis = this;
        this.http.onreadystatechange = function () { // Callback function
            // onreadystatechange property defines a function to be executed when the readyState changes.
            try {
                if (accessthis.http.readyState === XMLHttpRequest.DONE) {
                    if (accessthis.http.status === 200) {
                        return accessthis.http.responseText;
                    } else {
                        alert('Error Returning The Request');
                    }
                }
            } catch (e) {
                alert('Something Went Wrong: ' + e.description);
            }
        }

    } // End of addMusicToList method --  httpGet(url)

}

All that I am doing is no different from what we've been doing before except that we are making this a library so, we aren't hardcoding anything.

So, the Get(URL) method, takes in a URL, and in the body of the method:

We use open to specify the type of request we want to send, since we are requesting an external URL, we use the GET request to the URL we want to open, and we set async to true, we then use send() to send the request

I already explained the reason I used  let accessthis = this;but I'll explain it again here: "this" is used to access a property of an object but if you do "this.http" in a function, it would return an "undefined" error, and that is because the "this" in a function has a scope of that function and not of the object.

So, in a function, it doesn't know the this.httpexist. To access  this.http we can use an arrow function or set "this" to a variable, so as to capture it in the object scope.

Some people use let self = this but, I prefer "let accessthis = this" cos it explains why I am doing it, and in the onreadtstateexchange function, you replace the "this" with "accessthis", but we've already done that.

So, in the main.js, I can use the library to get a request like so:

const http = new TinyHTTP;
const user = http.Get('https://jsonplaceholder.typicode.com/users')

console.log(user)

You would expect this to work, but you would get an undefined variable, here is an illustration that would ease the explanation:

12. Test Tinyhttp library

Do you see what happened there? The user constant is undefined, and this is because XHR is asynchronous, so, you need to use a callback as we've done in our callback examples.

But before we do that, here is the culprit that is causing the undefined error:

-------------------
-------------------
if (accessthis.http.status === 200) {
     return accessthis.http.responseText; // <- This is the culprit
}
-------------------
-------------------

As soon as you do this:

const user = http.Get('https://jsonplaceholder.typicode.com/users')
console.log(user)

The send() function is called, and the  return accessthis.http.responseText; is immediately executed before the function has gotten any response, even if you delay console.log(user) with setTimeout, you'll still get undefined. This is because you can't return in AJAX, you need to pass the data to a function called a callback, which would then handle the data.

Shouldn't the console.log(user) return something when delayed with setTimeout? It should but it won't, here is why:

JavaScript is a single-threaded language, meaning that it has one call stack.  Only one thing can happen at a time, and everything else is blocked until an operation completes. Even with multiple cores, you could only get it to run tasks on a single thread, called the main thread.

When you run an async code, think about the future value, it doesn't necessarily mean the next value, it can be a later value. So, when you run a synchronous code it doesn't know about the async code, it would run immediately, which is why we get "undefined" no matter the trick you try to get the value.

Another way you can think of this is to imagine async code as nodes, one node knows when to stop, and it doesn't necessarily have to wait for one node to complete its operation before it jumps to the other nodes, they'll circle around and do their task, and once all the operation is completed, they'll shut the execution, the point is, they do not have to wait for each other. A synchronous code doesn't know this, which is the reason you can't pair them together.

How do you fix this?

You need to set up a way to deal with an async value when returned in the future, and a good way to tackle this is to use a callback function, you've seen this in our callback section before, so, you can add a callback argument like so in the Get method of our TinyHTTP class:

  // GET Method
    Get(url, callme) {
    this.http.open('GET', url, true)
    this.http.send();

        let accessthis = this;
        this.http.onreadystatechange = function () { // Callback function
            // onreadystatechange property defines a function to be executed when the readyState changes.
            try {
                if (accessthis.http.readyState === XMLHttpRequest.DONE) {
                    if (accessthis.http.status === 200) {
                        callme(accessthis.http.responseText); // <- return the callback here:
                    } else {
                        alert('Error Returning The Request');
                    }
                }
            } catch (e) {
                alert('Something Went Wrong: ' + e.description);
            }
        }

    } // End of Get method --  http.Get(url)

I added a callback argument: Get(url, callme) { and I changed return accessthis.http.responseText; to callme(accessthis.http.responseText);.

If you prefer you can do return callme(accessthis.http.responseText);it is your call. To get the data, you can do:

const http = new TinyHTTP;
http.Get('https://jsonplaceholder.typicode.com/users', callme);

function callme(users) {
    console.log(users);
}

This is the way we used it in the callback section. You can also do it directly or inline like so:

const http = new TinyHTTP;
http.Get('https://jsonplaceholder.typicode.com/users', function (users) {
    console.log(users);
});

You would get the same result.

We are handling the error directly in our Get method, if you prefer, you can also use a callback, and return the result:

this.http.onreadystatechange = function () { // Callback function
            // onreadystatechange property defines a function to be executed when the readyState changes.
            try {
                if (accessthis.http.readyState === XMLHttpRequest.DONE) {
                    if (accessthis.http.status === 200) {
                        callme(null, accessthis.http.responseText); // <- return the callback here:
                    } else {
                        callme('Error Returning The Request: ' + accessthis.http.status); // <- Another callback
                    }
                }
            } catch (e) {
                console.log('Something Went Wrong: ' + e.description);
            }
        }

As you can see, I have modified the first callback to contain null as the first argument: callme(null, accessthis.http.responseText);this is a node.js convention or I saw it there first. If there is an error, the first parameter is passed an error, if otherwise, the first parameter is passed null, which would then return the accessthis.http.responseText

Now, you can format the main.js code this way:

const http = new TinyHTTP;
http.Get('https://jsonplaceholder.typicode.com/users', function (err, users) {
    (err) ? console.log(err) : console.log(users);
});

I am using a ternary operator, you can also do it the normal way like so:

const http = new TinyHTTP;
http.Get('https://jsonplaceholder.typicode.com/users', function (err, users) {\
    if (err) {
        console.log(err);
    } else {
        console.log(users);
    }

});

This allows a user to easily know whether or not an error occurred. If null was not the first argument passed on success, you would have to find out yourself using your own method, which can be a bit complex.

Post

Now, that we've seen a GET request, used to request data, we would be creating a POST method for our library, this way, we can add new data through our library. In the TinyHTTP class, create a Post method with three arguments like so:

    // POST Method
    Post(url, data, callme) {
        this.http.open('POST', url, true)
        this.http.setRequestHeader('Content-type', 'application/json');
        this.http.send(JSON.stringify(data));

        let accessthis = this;
        this.http.onreadystatechange = function () { // Callback function
            // onreadystatechange property defines a function to be executed when the readyState changes.
            try {
                if (accessthis.http.readyState === XMLHttpRequest.DONE) {
                    if (accessthis.http.status === 201) { // 201 is used to check if a request has been created
                        callme(null, accessthis.http.responseText); // <- return the callback here:
                    } else {
                        callme('Error Posting The Request: ' + accessthis.http.status); // <- Another callback
                    }
                }
            } catch (e) {
                console.log('Something Went Wrong: ' + e.description);
            }
        }

    } // End of Post method --  http.Post(url, data)

The POST method takes in three arguments:

  • URL - The location or the URL you want the data to be created
  • data - The data you are creating
  • callme - The callback

When you are using the POST request, you need to set the value of an HTTP request header. A request header is an HTTP header that is used to provide information about the request context, this way, the server knows what type of data it is receiving. Since we are posting JSON data, we use:

this.http.setRequestHeader('Content-type', 'application/json');

In the onreadystatechange, I change the http status condition to 201, 201 is used to check if a request has been created, if we use 200, it would have failed since we are not requesting a resource. If you don't have the patience of knowing what all HTTP status code means, you can use the onload function to check if the request is successful instead:

    // POST Method
    Post(url, data, callme) {
        this.http.open('POST', url, true)
        this.http.setRequestHeader('Content-type', 'application/json');
        this.http.send(JSON.stringify(data));

        let accessthis = this;
            try {
                this.http.onload = function() {
                    callme(null, accessthis.http.responseText);
                }
            } catch (e) {
                console.log('Something Went Wrong: ' + e.description);
            }

    } // End of Post method --  http.Post(url, data)

I removed the need for onreadystatechange and the conditional checking of the http status code. I think this is better and simpler. Lastly, we use JSON.stringify(data) to convert javascript object to JSON, here: this.http.send(JSON.stringify(data));

To make use of the POST request, we would create a data, and use the POST method like so in our main.js file:

// Create User
const data = {
    name: 'the_devsrealm_guy',
    email: 'devsrealm@mail.com'
};

// Add User
http.Post('https://jsonplaceholder.typicode.com/users', data, function(err, user) {
    (err) ? console.log(err) : console.log(user);
});

// => Output
{
  "name": "the_devsrealm_guy",
  "email": "devsrealm@mail.com",
  "id": 11
}

https://jsonplaceholder.typicode.com took care of the ID increment, note that the request isn't actually created, it is simply a fake REST API for testing things out.

Patch a Resource

This method would let us update a request:

    // Patch Request
    Patch(url, data, callme) {
        this.http.open('PATCH', url, true);
        this.http.setRequestHeader('Content-type', 'application/json');
        this.http.send(JSON.stringify(data));

        let accessthis = this;
        this.http.onload = function() {
            callme(null, accessthis.http.responseText);
        }
    }

This is similar to the POST request, the only difference is when making use of it:

// Updated User data
const data = {
    name: 'the_devsrealm_guy',
    email: 'devsrealm@mail.com'
};

// Update DATA
http.Patch('https://jsonplaceholder.typicode.com/users/2', data, function(err, user) {
    (err) ? console.log(err) : console.log(user);
});

As you can see, I am specifying the URL I want to update, and the data object would update whatever key/value in the data object with the one in the actual URL.

Delete a Resource

This is really straight forward, we are deleting a specified resource:

    Delete(url, callme) {
        this.http.open('DELETE', url, true);
        this.http.send();

        let accessthis = this;
        try {
            this.http.onload = function() {
                if(accessthis.http.status === 200) {
                    callme(null, 'User Deleted');
                } else {
                    callme('Error: ' + accessthis.http.status);
                }
            }
        } catch (e) {
            console.log('Something Went Wrong: ' + e.description);
        }
    } // End of Deleted method --  http.Delete(url, callme)

Delete method doesn't need a data argument as we only need the URL of whatever we want to delete. Also, when the resource is deleted, we return a message, here:

if(accessthis.http.status === 200) {
callme(null, 'User Deleted');

To make use of it in your main.js file, you do:

// Delete User
http.Delete('https://jsonplaceholder.typicode.com/users/2', function(err, user) {
    (err) ? console.log(err) : console.log(user);
})

// => Output
User Deleted

This is the full TinyHTTP library:

'use strict';
// Class for our Tiny HTTP library
class TinyHTTP {

    constructor() { // the constructor
        this.http =  new XMLHttpRequest();
    }

    // GET Method
    Get(url, callme) {
    this.http.open('GET', url, true)
    this.http.send();

        /*
        #
        # The reason why I did accessthis = this; is because "this" is used to access a property of an object
        # but if you do "this.http" in a function, it would have return an "undefined" error, and that is because the "this"
        # in a function has a scope of that function and not of the object. So, in a function, it doesn't know this.http exist
        # To access the this.http, we can use an arrow function or set "this" to a variable, so as to capture it in the object scope.
        #
        # Some people use "let self = this", but, I prefer "let accessthis = this" cos it explains why I am doing it.
        # and in the onreadtstateexchange function, you replace the "this" with "accessthis"
        #
         */
        let accessthis = this;
        this.http.onreadystatechange = function () { // Callback function
            // onreadystatechange property defines a function to be executed when the readyState changes.
            try {
                if (accessthis.http.readyState === XMLHttpRequest.DONE) {
                    if (accessthis.http.status === 200) {
                        callme(null, accessthis.http.responseText); // <- return the callback here:
                    } else {
                        callme('Error Returning The Request: ' + accessthis.http.status); // <- Another callback
                    }
                }
            } catch (e) {
                console.log('Something Went Wrong: ' + e.description);
            }
        }

    } // End of Get method --  http.Get(url)

    // POST Method
    Post(url, data, callme) {
        this.http.open('POST', url, true)
        this.http.setRequestHeader('Content-type', 'application/json');
        this.http.send(JSON.stringify(data));

        let accessthis = this;
            try {
                this.http.onload = function() {
                    callme(null, accessthis.http.responseText);
                }
            } catch (e) {
                console.log('Something Went Wrong: ' + e.description);
            }

    } // End of Post method --  http.Post(url, data, callme)

    // Patch Resource
    Patch(url, data, callme) {
        this.http.open('PUT', url, true);
        this.http.setRequestHeader('Content-type', 'application/json');
        this.http.send(JSON.stringify(data));

        let accessthis = this;
        this.http.onload = function() {
            callme(null, accessthis.http.responseText);
        }
    } // End of Patch method --  http.Patch(url, data, callme)

    Delete(url, callme) {
        this.http.open('DELETE', url, true);
        this.http.send();

        let accessthis = this;
        try {
            this.http.onload = function() {
                if(accessthis.http.status === 200) {
                    callme(null, 'User Deleted');
                } else {
                    callme('Error: ' + accessthis.http.status);
                }
            }
        } catch (e) {
            console.log('Something Went Wrong: ' + e.description);
        }
    } // End of Deleted method --  http.Delete(url, callme)

}

Really simple, and tiny. This would conclude the guide on XHR, good luck, and let me know if you have any questions.

Related Post(s)

  • Getting Started With JavaScript (Introduction)

    This is my first JavaScript guide on this blog, and in this guide, we would go over the introduction of JavaScript, but first… What is JavaScript? JavaScript is a client-side scripting language su

  • Operator in Javascript

    Operators in javascript can either be used for logical expression where you connect two or more expressions and can be a comparison expression where you compare two values. You can also use an operat

  • Exploring Data Types, and Variables in JavaScript

    In JavaScript or any programming language, a data type is an attribute of data that tells the interpreter how the programs intend to use the given data. JavaScript support a couple of data type whic

  • Creating a Loader and Remove it After a Specific Time Using JavaScript/CSS/SVG

    In this guide, we would create a loader, and remove it after a specific time using CSS and JavaScript. The icon would be in SVG format, and the rotation would be done in CSS, while the removal after

  • Object Oriented Programming in JavaScript (The ES5 Way)

    In this guide, you'll learn and understand how to create and use Objects in JavaScript (ES5). While ES6 already supports creating classes (which is the way you create objects in Java, PHP, etc), they

  • Working With The Local & Session Storage In JavaScript

    We previously take a deep dive into the Windows Object, Properties, Methods, and even the Document Object itself, in this guide, we would explore working with the local and session storage in JavaScr