There is perhaps no bigger revolution that propelled JavaScript to where it is today than Ajax. This paradigm shift that started in the early 2000s brought JavaScript back to the center of Web development and restored the luster it had lost during its early years. And this whole revolution happened because of the rediscovery of a very interesting object called the XMLHttpRequest
.
In this chapter, we'll learn about the HTTP request and response process, and how XMLHttpRequest
fits into the equation. We'll also talk about the MooTools Request
class, a simple abstraction of the native XMLHttpRequest
API, and how it makes working with requests easier and more MooToolsian.
At the most basic level, browsers are tools for displaying resources on the Web, resources that are stored in special places called servers. At the heart of the Web is the communication between browsers and servers, and it is this communication that underlies everything we do online.
The communication between browsers and servers is a very complex topic, one that touches subjects we don't really need in our discussion. However, a certain part of browser-server communication—the request-response flow—is integral to understanding web applications, and we need a basic understanding of this process in order to fully grasp the things we'll see later.
When a browser loads a resource, it first sends a request to the server. A request is a set of directives containing information about what exactly we're requesting, as well as other criteria for the resource we're trying to fetch. Let's say we're loading http://foo.com/index.html
in our browser. The browser first connects to the server for foo.com
and then sends a request that looks like this:
GET /index.html HTTP/1.1 User-Agent: BrowserX/1.1 Host: foo.com Accept: text/html
This request has several parts. The first line starts with the method, which is a verb that describes the action we want to perform on the particular resource. In this example, we use the GET
method, which tells the server we would like to fetch the particular resource. Other supported verbs include POST
and PUT
for sending user data to a particular resource, DELETE
for removing a particular resource, and HEAD
for retrieving the headers for a resource.
Right after the method comes the resource URI, which describes the path of the resource we want to access. In our example, we told the server for foo.com
that we want to access the resource called /index.html
. Finally, the first line ends with an HTTP version identifier, which tells the server which version of the HTTP protocol we'd like to use.
The first line, which is called the request line, is the most important part of the request because it contains the essential information about the request. The next lines, called headers, contain other information about the request. A header is a key-value pair that takes the form <Header Name>: <Header Value>
. Each header appears on a separate line, and each describes a specific criterion or property of the request.
Our example has three headers. The first is User-Agent
, which identifies the current browser to the server. In our example, the browser is called BrowserX
and its version identifier is 1.1
. Normally, the value of the User-Agent
header is exactly the same string as the value of the navigator.userAgent
property we saw in Chapter 7. Next is an Accept
header, which tells the server what kinds of files we'd like to receive. In our example, we tell the server to give us files that have the mimetype text/html
, a common mimetype for HTML files. Finally, we have the Host
header, a required header in HTTP/1.1.
After sending this request to the server, the browser waits for a response. A response is a set of directives and properties the server sends back as an answer to a particular request. Our hypothetical foo.com
server, for example, could send us back this response in answer to our request:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 87 <html> <head> <title>Hello World!</title> </head> <body> Hello World! </body> </html>
The first line of the response begins with the HTTP version identifier, which tells the browser which version of the HTTP protocol the server used to answer the request. This is followed by the status code, a numeric value that determines the nature of the response. A 200
status code, for example, means that the request was correctly answered by the server and there were no errors in putting together a response. A 404
status code, on the other hand, means that the server couldn't find the particular resource asked for by the request. The status code is followed by the status text, which is a textual representation of the status code, in our case OK.
Because the first line of the response contains information about the status of the response, it's also called the status line.
Just as with requests, the headers come after the first line. The response headers, of course, describe the nature of the response, and they also give the browser some instructions regarding how to interpret the particular response. In our example, we have two headers: Content-Type
, which describes the mimetype of the response data, and Content-Length
, which is the length of the response data.
Right after the final header comes a mandatory blank line, followed by the most important part of the response: the body. In most cases, the body contains the actual file data for the particular resource, like in our example where we received the HTML source of the page as a body. This part of the response is the actual content, and is therefore the part we're most interested in.
With the response in hand, the browser can start parsing the HTML source and display the page. When a new resource is needed, the whole process is repeated. This request-response cycle is the lifeblood of browser-server interaction, and it affects the way we develop web applications in a massive way.
The basic request-response cycle fits the model of the web as a collection of hyperlinked documents. A browser first sends a request for a document to a server, and the server then sends a response back to the browser with the requested document. When the user clicks on a hyperlink, the process is repeated. The cycle of requesting a resource and then parsing the response for the resource is suitable for this model of the web because we're working with documents with a very limited interaction framework: open a resource, click links to open more resources, repeat.
Many web applications actually implement this same document-style interaction, and it works for the most part. First a page is shown to the user, then the user performs an action, such as filling in a form and submitting it, which triggers the browser to send a request to the server containing the data provided by the user. The server processes the data and sends a response containing the updated page, which is then displayed to the user by the browser. This process is repeated when the user performs another action.
While this works for simple applications, building more complex web applications that somehow mimic the desktop application model is hard to fit in this style. Desktop applications, which most users are familiar with, are often very dynamic: an action is immediately reflected in the interface, no loading or reloading involved. Simple web applications, on the other hand, feel a lot less like applications and more like web sites because they are too constrained by the hyperlink-style request-response cycle: if a part of the interface needs to be updated, the entire page has to be reloaded.
The problem isn't about developers not being smart enough to create desktop-style applications. Rather, it's about the lack of an API for issuing HTTP requests using JavaScript. Requests happen at the browser level: a default event or piece of JavaScript code triggers the browser to load a page, and the browser does the appropriate HTTP requests to load or reload a page. In the past, there was no infrastructure in place that let developers request data from servers directly using JavaScript.
The landscape changed, however, in the early years of the 21st century, with the rediscovery of a then obscure API. This API, originally developed for loading XML documents into JavaScript, became an instant hit because it enabled developers to make direct HTTP requests using JavaScript. Suddenly, JavaScript gained the necessary ingredient to create powerful web applications.
Using this API, web applications could load or post new content from the server without having to refresh the whole page. This helped developers create rich, complex applications that can rival desktop applications. Users no longer have to wait for a page to reload in order to see their changes, because applications can now make those changes in the background and then update the interface to reflect the changes.
JavaScript-based HTTP requests made web applications less like web sites and more like desktop apps, and the browser became the platform that enabled these rich, dynamic applications to run. JavaScript, as the language of the browser, became the old new cool thing, and the language regained popularity and regard within the developer community. And all of this is because of an obscure object called the XMLHttpRequest
.
That obscure object called the XMLHttpRequest
, or XHR, is at the heart of the new model of internal requests. First introduced by Microsoft in 1999, XHR is a special object that can be used to make HTTP requests—as well as receive appropriate HTTP responses—from within JavaScript. This object gives us a very simple API that can be used to build a request and send it to a server, as well as the appropriate means to handle the responses the server sends back. As its name implies, an XHR was originally used for retrieving XML documents, but it is flexible enough to be used for any kind of data.
You create an XHR object using the XMLHttpRequest
constructor:
var xhr = new XMLHttpRequest();
This snippet creates a new instance of XMLHttpRequest
, which we can now use to send requests to servers. Unlike most other constructors, the XMLHttpRequest
constructor takes no arguments, but rather depends on methods to set the appropriate values for the request.
Older versions of Microsoft Internet Explorer do not have an XMLHttpRequest
constructor. Instead, XHRs are created by instantiating ActiveX objects that implement the XMLHttpRequest API. Because this topic has already been covered in numerous books and articles, we won't talk about cross-browser XHR instantiation here.
The first of these methods is open
, which is used to initialize a request. It takes two required arguments: method
, which is an uppercase string indicating the HTTP method of the request, and url
, which is a string that points to the location of the resource we're trying to access. A third argument, async
, is used to determine the mode of the request. We'll look at how this third argument affects requests later, but for now we'll pass false
as the value of this argument.
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false);
In this snippet, we created a new XHR object called xhr
, then called its open
method to initialize it. We passed three arguments to open
: 'GET'
, which corresponds to the HTTP method GET, '
http://foo.com/index.html
'
, which is the resource we're trying to load, and a false
value, the use of which we'll find out later.
There are several important things you need to remember about these arguments. First, the method argument should always be in uppercase, so it's 'GET'
and 'POST'
, not 'get'
or 'Post'
. Second and more important, the value for the URI should be in the same domain as the current domain running the script. In our example, we assume that the script is running in the foo.com
domain, not www.foo.com
or dev.foo.com
.
That last point is important. One of the limitations of XHRs is imposed by the same-origin policy. This is a security concept that limits XHRs to requesting resources only from the same domain in which they are running. This security "feature" was put into place to prevent malicious scripts from sending or loading data from other malicious sites.
You'll notice that these arguments correspond to the request line of the HTTP request message:
GET /index.html HTTP/1.1
The HTTP version identifier, which is the last part of the request line, isn't included in the formal parameters of the open
method, because the browser itself sets which version of the HTTP protocol to use.
Another method of XHRs is setRequestHeader
, which is used to add appropriate headers to the request. It takes two arguments: header
, which is a string name of the header we're adding, and value
, which is the value of the header.
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'),
Here we added a single request header called 'Accept'
, which tells our server the kind of file we want to receive. Our request message now looks like this:
GET /index.html HTTP/1.1 Accept: text/html
Note that some common or required headers, such as Host
or User-Agent
, are added automatically by the browser, so we no longer need to add them explicitly.
At this point, our request is ready to be sent to the server. We do this using the send
method:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
Here we send the original example above by calling the send
method of our XHR object. This tells the browser to send an HTTP GET
request to foo.com
to retrieve the /index.html
resource.
Sometimes you'll need to send data with your request, especially for requests that use the POST
and PUT
methods. In that case, you'll need to pass the optional body
argument to the send
method. This argument should be a string that contains the data you want to send.
var data = 'name=Mark&age=23'; var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://foo.com/data/', false); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'), xhr.setRequestHeader('Content-Length', data.length); xhr.send(data);
In this snippet, we need to send the value of the data
variable to http://foo.com/data
. First we create a new XHR object, then we initialize it using the open
method, specifying the POST
method and the URL endpoint. We then set two necessary headers, Content-Type
and Content-Length
, which are used by the server in parsing the data. Finally, we send the request using the send
method, passing in the data
variable that becomes the body of our request. Our request message will look like this:
POST /data HTTP/1.1 Host: foo.com Content-Type: application/x-www-form-urlencoded Content-Length: 16 name=Mark&age=23
Now that we've sent the request, it's time to parse the response. When the data is received back from the server after sending the request, the browser will update the XHR object and put the data from the response into properties. Let's say that after sending our GET
example above, the server responds with the following:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 87 <html> <head> <title>Hello World!</title> </head> <body> Hello World! </body> </html>
We can access the parts of this response using the properties of the XHR object itself. The first property is status
, a numeric value that corresponds to the status code of the response:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.status); // 200
Here we sent the original GET
example, which returns the response we saw above. We then retrieve the status code of the response by accessing xhr.status
, whose value in this example is 200
, just like our response.
A related property, statusText
, gives us the status code plus the status text of the response as a string:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.statusText); // 'OK'
Response headers can be accessed using the getResponseHeader
method. This method takes a single argument, headerName
, and returns the value for that particular header:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.getResponseHeader('Content-Type')); // 'text/html' console.log(xhr.getResponseHeader('Content-Length')); // '87' console.log(xhr.getResponseHeader('Fake-Header')); // null
The getResponseHeader
method always returns a string containing the value of the header if the header is present, and null
if the header doesn't exist in the response. In this example, we used the getResponseHeader
method to retrieve the values of the Content-Type
and Content-Length
headers of the response. Because these two headers exist in the response, getResponseHeader
returns their values as strings. However, when we tried retrieving the value of Fake-Header
using the same method, we get null
because the response doesn't have this header.
You can retrieve all response headers using the getAllResponseHeaders
method. This method returns a string containing all the headers of the request, one header per line.
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.getAllResponseHeaders());
This snippet will log the following string:
'Content-Type: text/html Content-Length: 87'
Finally, you can access the body of the response itself using responseText
, which is a string containing the raw response body:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.responseText);
This will give us the following console output:
'<html> <head> <title>Hello World!</title> </head> <body> Hello World! </body> </html>'
The responseText
property contains the actual body of the response and is not parsed by the browser. This allows us to retrieve any kind of resource from the server for use in our applications.
One thing to note is that the value of responseText
can be null
or an empty string in some cases. If the server returned a response with no body, or if there was a server-based error, the responseText
property will be empty. However, if there was an error in the request itself or an error from the server side that resulted in a wrong response, the value of the property will be null
.
An easy way to guard against errors is to check the status code of the response. Usually, a response with a status code greater than or equal to 200
but less than 300
is considered a "successful" response from the HTTP protocol point of view. Therefore, we can add a simple if
statement to our code to check for success:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); if (xhr.status >= 200 && xhr.status < 300){ console.log(xhr.responseText); } else { console.log('Unsuccessful Request.'), }
Our if
statement in this example checks the status code of the XHR object to see whether its value is greater than or equal to 200
but less than 300
. If it is, it logs the responseText
property of the XHR. Otherwise, it will log 'Unsuccessful Request.'
. Because the response status for this example is 200
, we'll get a proper console output like the previous one.
Because the XMLHttpRequest
API was originally created with XML documents in mind, there's another property called responseXML
, which is a document object. If the content-type of the response body is an XML document, the browser will automatically try to parse the responseText
value into a DOM Tree object, and set it as the value of responseXML
. However, if the response is not an XML document—as in our case—the responseXML
property is set to null
.
There are times you'll need to cancel a request, and you can do that using the abort
method:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false);
xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); xhr.abort();
There are many reasons you might want to abort a request, such as if the user decides to cancel the action. Or you may want to set some sort of timeout for the request. The XMLHttpRequest
object doesn't natively support timeouts, which means it will wait for a response from the server as long as possible. You can use abort
together with the setTimeout
function to automatically cancel a request after a specific time:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html'), xhr.setRequestHeader('Accept', 'text/html'), // timeout setTimeout(function(){ xhr.abort(); }, 5000); xhr.send();
Here we combined the abort
method with setTimeout
to cancel the request after 5 seconds. If the request hasn't received a response after 5 seconds, the timeout function will automatically cancel the request, making it possible to create some sort of timeout function that can be used to cancel long-running requests.
The requests we made above using XHR objects are synchronous, which is a fancy term for blocking: after sending the request, the browser halts all processes in the current window until it receives a response. Only after getting a response back from the server will the browser continue execution.
While that's fine in some cases, synchronous requests are problematic in general because loading resources takes time. Since the browser blocks all processes during synchronous requests, your application will remain unresponsive during this time and the user won't be able to interact with it. If you're loading a large file, for instance, it might take a few seconds for the browser to finish loading the response from the server—a few seconds that might be enough reason for your bored user to stop using your application. Factor in the slow speed of some Internet connections plus the latency between the user's physical location and the server, and you get a user-experience nightmare.
Thankfully, we have an alternative: asynchronous requests. An asynchronous (or async) request happens in the background and is therefore non-blocking. Using an async request, we can send a request to the server and continue without waiting for a response. Because it doesn't block the browser from performing other processes, our application remains interactive while the request is being processed, and users won't notice anything happening until we get the response back and update the interface.
To use the async mode of XHRs, we must initialize our objects with the third argument for the open
method. This third parameter, called async
, takes a Boolean value: if the value is true
, the request will be asynchronous. In the previous examples, we passed false
to this argument, which made our requests synchronous. To make our request asynchronous, we simply have to change that value to true
:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
In this snippet, we changed the synchronous example from the last section into an asynchronous version by passing a true
value as the async
argument to open
. When we send our request using the send
method, it will now be done in the background, and the lines after the send
invocation will immediately be interpreted. This behavior is different from the synchronous example, where the script waits for a response from the server before executing the next lines.
The use of async requests, though, requires a different approach. Until the previous example, all of our code snippets were written with the synchronous calls in mind. This enabled us to access the values of the XHR object directly after our send
invocation:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', false); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.responseText.length); // 87
Since synchronous requests block further execution until the response arrives, we're able to get the response values like this. By the time our first console.log
line is evaluated, the response has already been received, so we can read the data immediately.
But what about async requests?
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); console.log(xhr.responseText); // undefined
Here we modified the example to use an async request instead of a synchronous one. Even if both examples point to the same URL, this example will always log undefined
for the responseText
property. This is because, unlike in our previous example, the JavaScript interpreter will not wait until the response is received before interpreting the line after the send
call. Instead, the interpreter triggers the browser to perform a non-blocking background request and then goes on to interpret the rest of the program.
Because the request is asynchronous, we won't be able to access the properties right after we invoke send
, because we don't know for sure whether the response has arrived from the server by the time the interpreter evaluates our access code—and often, the response arrives much later.
To work with asynchronous requests, then, we need a new plan of attack. Instead of immediately accessing the properties of the XHR after sending it, we need to defer this access until we actually get a response. In other words, we need to wait for the response to actually arrive before processing it. And the technique we'll use to do that should already be familiar to us, since we just discussed it in the previous chapter: events.
An XHR object supports one main event: the readystatechange
event. This event is dispatched by the XHR every time its ready state changes, which we'll discuss in a bit. In order for us to effectively use async requests, we must therefore attach an appropriate event handler for this event.
Attaching an event handler to an XHR, however, is a bit tricky. In browsers that support the standard model, we can use the addEventListener
method for this purpose. Internet Explorer, on the other hand, does not implement event methods such as attachEvent
on XHR objects. We must therefore use an older style of event attachment that's supported by all browsers—event handler properties:
var xhr = new XMLHttpRequest(); // attach handler xhr.onreadystatechange = function(){ console.log('Ready State Change.'), };
Instead of using a method to attach an event handler, we attached the event handler directly to the object through a property name that's composed of the "on" prefix plus the name of the event. In this case, we attached a readystatechange
event handler by assigning a function to the onreadystatechange
property of the XHR object. This method of attaching the event handler is part of the legacy event model, which comes from the DOM Level 0 days of the browser but is still supported by all major browsers.
Of course, attaching the event handler is just part of the equation. Take a look at the following example:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); // attach handler xhr.onreadystatechange = function(){ console.log('Ready State Change.'), }; xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
If we try running this in the browser, we'll get the following console output:
'Ready State Change' 'Ready State Change' 'Ready State Change' 'Ready State Change'
We got four log outputs, which means that our event handler was dispatched four times. Instead of just dispatching the event when it receives a response from the server, the XHR dispatches the readystatechange
event for every phase of the request-response cycle.
If we look back at the request-response cycle, we can see that there are four phases. The first phase is connecting to the server where the HTTP request message will be sent. The second phase is sending the actual request message to the server. The third phase is downloading the response data. The fourth phase is ending the download phase and parsing the data from the response. So the four phases are connect, send, download, and complete.
These four phases are connected to the "ready state" of the XHR. An XHR object has a special property, readyState
, that contains a numeric value that tells us the current phase the XHR object is in:
0
- the XHR has just been created, but its open
method hasn't been called yet so it remains uninitialized. This XHR phase does not correspond to a request-response flow phase.
1
- the XHR has been initialized, but the request hasn't yet been sent to the server using send
.
2
- the XHR's request has been sent, and the preliminary data—headers and status codes—are available.
3
- the XHR's response is being downloaded and the responseText
property already has partial data.
4
- the XHR's response has been downloaded completely and we can now process the data.
As the XHR moves from one phase to the next, it dispatches the readystatechange
event to inform us of the change of state. We can then use the readyState
property to check which phase it is in.
Let's modify the previous example:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); // attach handler xhr.onreadystatechange = function(){ switch(xhr.readyState){ case 1: console.log('1: Connect'), break; case 2: console.log('2: Send'), break; case 3: console.log('3: Download'), break; case 4: console.log('4: Complete'), break; } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
In our readystatechange
event handler, we used a switch
statement to check for the value of the readyState
property. Here's the corresponding console output from our snippet when run on the browser:
'1: Connect' '2: Send' '3: Download' '4: Complete'
The first line in our output, '1: Connect'
, is logged after we call the open
method. The open
method changes the state of the XHR from 0
to 1
, which dispatches the readystatechange
event. The second line is logged after we call send
, which changes the state from 1
to 2
, triggering another dispatch of the readystatechange
event. As the browser starts receiving data from the server, it changes the state of the XHR to 3
, dispatching a third readystatechange
event that logs our third line. Finally, the last readystatechange
event is dispatched when the browser finishes downloading the whole response from the server, thereby logging our fourth and last line.
The last phase is perhaps the most important of these four phases, because it's only when we have all of the response data that we can start parsing it for our purposes. This means that until the readyState
of our XHR object is 4
, we can't access the complete data from our response. With this in mind, we can now create a proper event handler for our async request:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); // attach handler xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ console.log(xhr.responseText); } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
Here we changed our readystatechange
handler by adding an if
statement that checks whether the readyState
property is equal to 4
. If the value of this property is anything other than 4
, the data isn't ready yet and we won't be able to read the full response body. But if the value is equal to 4
, our response has been completely downloaded, and we can begin using it or parsing it for our needs.
One thing we also have to factor in is the simple guard we included to check for failed requests. Remember that we checked the value of the response's status code in the previous section to determine whether the response was successful or not. We should also add that into our new event handler:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); // attach handler xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300){ console.log(xhr.responseText); } else { console.log('Unsuccessful Request.'), } } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.send();
Finally, we can also add a timeout that will automatically cancel our request if it takes too long:
var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://foo.com/index.html', true); // attach handler xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300){ console.log(xhr.responseText); } else { console.log('Unsuccessful Request.'), } } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.send(); // timeout setTimeout(function(){ xhr.abort(); }, 5000);
And with that, we now have our complete asynchronous XHR code.
Now that we've looked at the native XMLHttpRequest
implementation, you might have noticed a few things:
The initialization method open
has to be called separately from the constructor function, which doesn't fit with the normal JavaScript (or MooTools) style.
Native XHRs dispatch only a single event type in most browsers, which means that we have to cram all our code into a single event handler for both successful and failed requests.
Timeouts aren't natively implemented, so we need to add separate code to handle this for us.
We don't have the flexibility to handle different response types, and we have to parse the responses ourselves.
Because XHRs are used a lot these days, you'll probably encounter these issues in most applications you build. Therefore, we need a somewhat better API for working with XHRs to streamline the process for us.
Thankfully, MooTools provides us with the API we need: the Request
class. This special class is an abstraction of the native XHR class, and can be used as a replacement for native XHR code. Like the Event
type, the Request
class is a wrapper: it does not override the native XMLHttpRequest
type, but instead wraps it in order to add functionality. Unlike most of the constructors we've discussed so far, Request
is implemented as a class rather than as a type object in order to enable subclassing—which, as we'll see later in this chapter, makes Request
a really powerful feature of MooTools.
In the sections to come, we'll translate the following native XHR code to a version that uses the Request
class:
window.addEvent('domready', function(){ var notify = $('notify'), data = 'name=Mark&age=23'; var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://foo.com/comment/', true); xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300){ notify.set('html', xhr.responseText); } else { notify.set('html', '<strong>Request failed, please try again.</strong>'), } } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'), xhr.setRequestHeader('Content-Length', data.length); xhr.send(data); notify.set('html', '<strong>Request sent, please wait.</strong>'),
// timeout setTimeout(function(){ xhr.abort(); notify.set('html', '<strong>Request timeout, please try again.</strong>'), }, 5000); });
In this snippet, we send a POST
request to http://foo.com/comment/
in order to send the data to the server. We then wait for a response from the server to tell us whether the operation was successful, and this response is composed of an HTML string that we'll insert into the notify
element.
The first thing we need to do is to create a new request object using the Request
constructor. This constructor takes a single argument, options
, which is an object containing options for our request object.
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/', method: 'post', async: true });
Unlike the native XMLHttpRequest
API, the Request
API doesn't have a separate open
method to initialize the request. Instead, we pass the values that we'd normally pass to open
as values in our options argument. In this snippet, for example, we passed three options: url
, which is the URL of the location we're requesting; method
, which is the HTTP method to use for the request; and async
, which tells the Request
class that we want to use the asynchronous mode for this request.
You'll notice that the method
option isn't in uppercase, and that's okay. The Request
class allows any case for the method
option: we could have used 'POST', 'Post'
and even 'PoST'
. It's common, however, to use the lowercase style with Request
class instances.
Another thing we need to know is that most of the options for Request
have default values. For instance, the method
option is 'POST'
by default, and the async
option is true
by default. If the default values of these options are already set to what we need, we can simply omit them from our options object:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/' });
This snippet is the same as the previous one, even if we didn't include values for the method
and async
options.
When we create a new instance of Request
, the constructor automatically creates a native XHR object that will be used for the request. As I mentioned, the Request
class is a wrapper object, like the Event
type: it doesn't create a new object type of its own but only creates an abstraction for the native object. We can access this wrapped XHR object by accessing the xhr
property of our request object:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/' }); console.log(typeOf(request.xhr)); // 'object'
Now that we have our request object, we need to add the headers, and we do this using the setHeader
method. This method takes two arguments, name
and value
, which correspond to the header name and value:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/' }); request.setHeader('Accept', 'text/html'), request.setHeader('Content-Type', 'application/x-www-form-urlencoded'), request.setHeader('Content-Length', data.length);
Here we set three headers for our request: Accept, Content-Type
, and Content-Length
. You'll notice that the setHeader
method is very similar to the setRequestHeader
method from the native API, and they actually are somewhat similar in style.
One nice feature, though, is that we can actually pass these headers to the Request
options. This saves us three function invocations:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/', headers: { 'Accept': 'text/html', 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': data.length } });
Instead of calling setHeader
separately, we just pass a headers
option to the Request
constructor. This option should have an object value, with the keys of the object corresponding to the name of the header, and the value corresponding to the header value. This cleans up our code considerably, and makes the Request
declaration more expressive.
All request objects have a default Accept
header with the value 'text/javascript, text/html, application/xml, text/xml, */*'
. This means we can remove the Accept
header declaration in our code:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/', headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': data.length } });
Another feature of MooTools is the special urlEncoded
option, which automatically encodes our data into the application/x-www-form-urlencoded
type. It also automatically adds a Content-Type
and Content-Length
header to our request if the method used is POST
. We can therefore remove those two headers and use the urlEncoded
option instead:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/', urlEncoded: true });
By default, the urlEncoded
option of a request object is set to true
, which means we don't even need to explicitly add it. So, we can go back to our original request code once more:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/' });
At this point, we've replaced about half of our original native code with this simple code block. Now we need to consider the data to be sent. In the native model, we passed the data to the send
method of the XHR object. In MooTools, we can do the same using its send
method:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/' }); request.send(data);
Like the native send
method, the Request send
method can be invoked with an argument to send data to the server. Here we send the value of the data
variable to the server by passing it the send
method.
However, we don't need to use the send
method to pass data to the request; we can also declare the data to be sent using the Request
options:
var data = 'name=Mark&age=23'; var request = new Request({ url: 'http://foo.com/comment/', data: data });
Here we added a new option called data
to the options object. This option is used to declare the data that will be sent to the server, and in our case, we declared the value of this option to be the string value of our data
variable.
We aren't limited to using strings as data values in the Request
class though. MooTools allows us to send other values, such as objects:
var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } });
In this example, we declared the value of the data
option to be an object with two properties. When we send this request, MooTools automatically turns the object into a string that's readable by the server. The Request
class is able to process different kinds of objects like this: plain objects, arrays, and even form elements. This gives us flexibility in our code and enables us to transparently send complex JavaScript objects to the server.
The next thing we have to deal with is events. With native XHRs, we needed to attach a readystatechange
event handler and check the readyState
property, and then put our processing code inside the handler. For our example, we had a readystatechange
event handler that looked like this:
xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300){ notify.set('html', xhr.responseText); } else { notify.set('html', '<strong>Request failed, please try again.</strong>'), } } };
The MooTools Request
class, on the other hand, lessens the complexity of the readystatechange
event by providing not one but five main events:
The request
event is dispatched right after the request is sent.
The complete
event is dispatched when the request has finished.
The success
event is dispatched for a successful request.
The failure
event is dispatched for an unsuccessful request.
The cancel
event is dispatched when a running request is stopped.
You can manage event handlers for these events to your request object using the MooTools event methods such as addEvent
or removeEvent
.
The first event, request
, is dispatched right after the request is sent. It is useful for displaying notification messages or loading images in your interface to tell the user that something is happening. In our native example, we logged a message right after the request was sent. This is a good candidate for use with our request
event:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), } });
When our request object is sent in this example, the request event will be dispatched, which in turn invokes the event handler we attached. The HTML source of our notify
element will then be updated, informing our users that an action is taking place.
The next event, complete
, is dispatched when the request has been completed. This event, however, does not tell us whether the request was successful or not—it simply tells us that the request has been done. Therefore, this event shouldn't be used for data processing event handlers. Instead, it should be used for "cleanup" purposes, such as removing elements you've added during the request event.
var spinner = new Element('img', {src: 'spinner.gif'}); var request = new Request({ method: 'get', url: 'http://foo.com/index.html' }); request.addEvents({ 'request': function(){ spinner.inject(document.body, 'top'), }, 'complete': function(){ spinner.destroy(); } });
In this separate example, we attached event handlers for the request
and complete
events. For the request
event, we displayed a spinner image in our interface to tell the user that we're loading something. We then removed this spinner during the complete
event to signify that we finished loading the data. Since we're not doing anything like this in our original native example, we didn't attach a complete handler in the earlier example.
The next two events, success
and failure
, are the two most important events when it comes to requests, since these events are dispatched right after the complete
event to inform us whether our request was successful or not. To determine whether an event was successful, the Request
class uses a function named isSuccess
. When the request is completed, the request object will invoke this function to check whether the request was successful. If the function returns true
, then the request is successful and the request object will dispatch the success
event. If the function returns false
, the request is considered unsuccessful and the request object will dispatch the failure
event.
The default isSuccess
function looks like this:
isSuccess: function(){ var status = this.status; return (status >= 200 && status < 300); }
As you can see, the criteria used in the isSuccess
method are the same as those we used in the native example: if the status of the response is greater than or equal to 200
and is less than 300
, the request was successful.
There are times, though, when the default isSuccess
criteria doesn't suffice for your applications. Thankfully, the Request
class allows you to define your own isSuccess
function by passing it using the options object:
var request = new Request({ method: 'get', url: 'http://foo.com/index.html', isSuccess: function(){ return this.status == 200; } });
Here we define a different isSuccess
method by passing it as an option in our Request
declaration. The criterion used by our isSuccess
function in this case is stricter than the default one: only responses with the status code of 200
will be considered successful.
After consulting the isSuccess
function, the request will fire one of two events. The first event, success
, is dispatched when the request is successful:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23
} }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, 'success': function(text, xml){ notify.set('html', text); } });
The event handler for a request's success
event receives two arguments when invoked: text
and xml
. The second argument, xml
, is simply the value of the responseXML
property of the wrapped native XHR object. The text
argument, on the other hand, is a stripped version of the responseText
property value: all scripts tags are removed from the original responseText
value. For example, say we received the following response data:
<div>Hello World</div> <script> alert('Hello World'), </script>
The request object will parse this response data and strip out the script tag. The text argument that our success event handler receives will therefore look like this:
'<div>Hello World</div> '
The script tag was removed, leaving us with only the HTML source for the div.
MooTools strips out scripts for security reasons to prevent malicious scripts from being injected directly into the page. However, there are instances when you'd want to use those scripts and evaluate them in your application. The Request
class, therefore, provides a special option called evalScripts
. If you pass this option with the value of true, the Request
object will automatically evaluate your scripts after stripping them.
The evalScripts
option, however, only works for <script>
tags with bodies, such as <script>alert('Hello World'),</script>
. Tags that are implemented with the src
attribute, such as <script src="hello.js"></script>
, will not be automatically loaded or evaluated, but will still be stripped.
Another option related to evalScripts
is evalResponse
. If you set this option to true in your request declaration, MooTools will automatically evaluate the whole body of the response as a script. MooTools will also automatically evaluate the value of the response body if the Content-Type
of your response contains the words ecmascript
or javascript
.
Of course, there are cases where the automatic script stripping will not be what you want to do. In such cases, you can access the raw responseText
and responseXML
values using the response
object property of your request object, which stores the unprocessed response data from the server:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, 'success': function(){ notify.set('html', this.response.text); } });
In this snippet, we removed the formal parameters for our success
event handler, and instead accessed the raw response body using the response
object property of the request.
In contrast to the success event, the failure
event's handler functions receive only one argument: the wrapped native XHR object. MooTools passes the native XHR object so we can handle the failure ourselves. Take note, though, that even if we're not passed the response text and response xml values, we can still access them using the response
property object in our failure
event handler.
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, 'success': function(){ notify.set('html', this.response.text); }, 'failure': function(){
notify.set('html', '<strong>Request failed, please try again.</strong>'), } });
We didn't need any fancy error handling in our original native example, so we just attached a basic failure
event handler.
The next thing we need to do is to add a timeout. In our native example, we used the abort
method of the XHR object together with a setTimeout
call to cancel the request after a specific amount of time. The Request
class has a method called cancel
that is equivalent to abort
, and we can use that here:
var notify = $('notify'), var request = new Request({
url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, 'success': function(){ notify.set('html', this.response.text); }, 'failure': function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), } }); setTimeout(function(){ request.cancel(); notify.set('html', '<strong>Request timeout, please try again.</strong>'), }, 5000);
You'll notice that after we canceled our request, we updated our notify
element to show that our request has timed out. Instead of putting this directly in our setTimeout
function, we can also implement this as an event handler.
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 } }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, 'success': function(){ notify.set('html', this.response.text); }, 'failure': function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), }, 'cancel': function(){ notify.set('html', '<strong>Request timeout, please try again.</strong>'), } }); setTimeout(function(){ request.cancel(); }, 5000);
Request objects dispatch the cancel
event every time a running request is cancelled. In this example, our cancel
event handler will be dispatched when our request times out after 5 seconds.
While our snippet looks good right now, we can actually make it cleaner. The MooTools Request
class actually adds support for timeouts using the timeout
option. So instead of writing our request code as we did above, we can modify it to look like this:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 }, timeout: 5000 }); request.addEvents({ 'request': function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'),
}, 'success': function(){ notify.set('html', this.response.text); }, 'failure': function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), }, 'timeout': function(){ notify.set('html', '<strong>Request timeout, please try again.</strong>'), } });
Instead of using the cancel
method with setTimeout
, we simply added a timeout
option to our Request
declaration. The request class will automatically handle the timeout for us, and dispatch a timeout
event when the request times out. You'll notice that we also changed our cancel
event handler to a timeout
event handler since we're no longer handling the cancel
event.
At this point our code is almost complete, and we can now add the send
invocation to finish it. Before we do that, however, we need to check out one more feature of the request declaration. Instead of attaching event handlers using addEvent
or addEvents
, we can actually include them in the request declaration by using the "on" prefix form:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 }, timeout: 5000, onRequest: function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, onSuccess: function(){ notify.set('html', this.response.text); }, onFailure: function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), },
onTimeout: function(){ notify.set('html', '<strong>Request timeout, please try again.</strong>'), } });
We moved the event handlers from a separate addEvent
call to the actual Request
declaration by capitalizing their names then attaching an "on" prefix to them. This declaration form is similar to the one above, but it's cleaner and tighter and therefore used more often in development.
This brings us finally to the part where we send the request. To complete our code, we simply have to send the request by invoking the send
method:
var notify = $('notify'), var request = new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 }, timeout: 5000, onRequest: function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, onSuccess: function(){ notify.set('html', this.response.text); }, onFailure: function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), }, onTimeout: function(){ notify.set('html', '<strong>Request timeout, please try again.</strong>'), } }); request.send();
You already saw the send
method a few sections back when we discussed how to factor in the data being sent to the request. The MooTools send
method looks very similar to the native send
method for XHR objects: we can pass the string data to be sent by making it an argument to the send
method.
However, the send
method will only use the argument as the data for the request if the argument is a string or an element. If you pass an object to the send
method, for example, it won't be sent to the server. Thus, something like request.send({name: 'Mark'})
won't work.
This is because the send
method's argument isn't actually called data
—it's called options
. This options object is similar to the options object you pass to the Request
constructor, but it understands only three options: url, method
, and data
.
When you pass a string or element argument to send
, the method actually transforms it into the data
property of an options object. Thus, send('name=Mark')
is the same as doing send({data: 'Mark'})
. If we want to send an actual object using the send
method, we have to use a real options object, like send({data: {name: 'Mark'}})
.
Passing an options object to send
makes it possible to create reusable request objects.
var request = new Request({ link: 'chain', onSuccess: function(){ console.log(this.response.text); } }); request.send({url: '/index.html', method: 'get'}); request.send({url: '/comments', method: 'post', data: {name: 'Mark'}});
Here we created a single request object, then used options objects with the send
method so that we can send different requests using a single request object. Note that passing in different values for the options to send
does not change the actual option values of the request object. This means that even if we used a different url
value in our send
options, the actual url
of the request object won't be changed—it will still be a blank string, which is the default value.
You'll notice from our last example that we added a new option to the request declaration called link
. This option is used to set the behavior of the request object if a send
call is issued while a request is still running.
By default, the value of this option is 'ignore'
. In this mode, the request object will ignore additional calls to send
while it is running.
var request = new Request({ link: 'ignore', onRequest: function(){ console.log(this.response.text); } }); request.send({url: '/index.html', method: 'get'}); request.send({url: '/comments', method: 'post', data: {name: 'Mark'}});
In this snippet, only the first request—the GET
request to /index.html
—will be honored. Because we called send
immediately after sending the first request, the second request will be ignored. This is because the second time we invoked send
, our original request was still running. By default, all request objects use the ignore mode.
The second possible value for link is 'cancel'
. In this mode, subsequent calls to the send
method will cancel the current running request.
var request = new Request({ link: 'cancel', onRequest: function(){ console.log(this.response.text);
} }); request.send({url: '/index.html', method: 'get'}); request.send({url: '/comments', method: 'post', data: {name: 'Mark'}});
In this example, the second send
call will cancel the previous one. Therefore, the POST
request to /comments
will be the one honored by the request object and the first GET
request will be canceled.
The last possible value for link is 'chain'
. In this mode, requests will be "chained": if the send
method of the request object is called while it is running, the request will wait for the currently running request to finish before sending the new one.
var request = new Request({ link: 'chain', onRequest: function(){ console.log(this.response.text); } }); request.send({url: '/index.html', method: 'get'}); request.send({url: '/comments', method: 'post', data: {name: 'Mark'}});
Both requests will be sent in this example. First, the GET
request will be sent, and the second POST
request will be added to the request chain. When the GET
request is finished, the request object will automatically send the POST
request.
This brings us finally to the part where we send the request. To complete our code, we simply have to send the request itself by invoking the send
method:
window.addEvent('domready', function(){ var notify = $('notify'), new Request({ url: 'http://foo.com/comment/', data: { 'name': 'Mark', 'age': 23 }, timeout: 5000, onRequest: function(){ notify.set('html', '<strong>Request sent, please wait.</strong>'), }, onSuccess: function(){ notify.set('html', this.response.text); },
onFailure: function(){ notify.set('html', '<strong>Request failed, please try again.</strong>'), }, onTimeout: function(){ notify.set('html', '<strong>Request timeout, please try again.</strong>'), } }).send(); });
You'll notice that the original request variable assignment was removed from this snippet. Since we don't need to store the request, we can simply do away with the assignment and send the new instance directly. We also wrapped the whole snippet in a domready
event handler, as in our original example.
Let's take another look at the original native request code:
window.addEvent('domready', function(){ var notify = $('notify'), data = "name=Mark&age=23"; var xhr = new XMLHttpRequest(); xhr.open('POST', 'http://foo.com/comment/', true); xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ if (xhr.status >= 200 && xhr.status < 300){ notify.set('html', xhr.responseText); } else { notify.set('html', '<strong>Request failed, please try again.</strong>'), } } }; xhr.setRequestHeader('Accept', 'text/html'), xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'), xhr.setRequestHeader('Content-Length', data.length); xhr.send(data); notify.set('html', '<strong>Request sent, please wait.</strong>'), // timeout setTimeout(function(){ xhr.abort(); notify.set('html', '<strong>Request timeout, please try again.</strong>'), }, 5000); });
I think we will all agree about which code is better. Our Request
-based code is cleaner, easier to read, and more modular, and it fits the MooTools style perfectly.
A great thing about Request
is that it's implemented as a class rather than a type object. This gives us the opportunity to create subclasses that extend the functionality of Request
.
To understand how Request
subclassing works, we must first get a feel for the internals of the MooTools Request
class. Request
is a very simple class. It uses all of the three mixin classes we discussed in Chapter 5: Options, Events
, and Chain
. We use the Options
mixin for the initialize
method, which is how we're able to pass an options object when we create a new object. We use the Events
mixin to enable the request object to use event handlers and dispatch events. And we use the Chain
mixin to power the "chain" mode of request sending.
When a new Request
instance is created, the initialize
method does two things. First, it creates the internal XHR object that will be used to send the requests and stores it in the xhr
property of the instance. Second, it takes the options
object argument and merges it with the default options using the setOptions
method from the Options
class. Remember that this method enables us to define event handlers using the onEventName
format, which is how we're able to combine the event handler declaration with the other options in our request object instantiation.
At this point, the Request
instance contains a non-initialized native XHR object. Request
doesn't actually call the open
method of the XHR until later in the process. Rather, all processes at this point are "buffered" internally. For example, when we add new request headers using the setHeader
method, the Request
instance doesn't actually add them immediately to the XHR object using setRequestHeader
. Instead, it stores the headers first in the internal headers
property. This makes the class flexible enough so that changes can be easily made without having to reset the XHR instance.
The bulk of the Request
processes happens in the send
method. When called, it first sets the current request as "running," so that subsequent calls to it will be controlled. The method then prepares the internal options: it combines the options object passed to it (if there is one) to the option values defined during the creation of the request object.
The send
method then prepares the data for sending. If our data is a simple string, it does no further parsing. If our data is an element, it will first call the toQueryString
method of Element
to turn form elements into a query string value. And if our data is an object, it'll use the Object.toQueryString
generic to turn the object into a proper query string. Thus, no matter what kind of data we pass to Request
, it always turns it into a string.
The next step the method takes is to initialize the native XHR object by calling its open
method. It uses the prepared values from the options to perform this task: an uppercase version of options.method
for the method
argument, options.url
for the url
, and options.async
for the async
argument. It then attaches the readystatechange
handler for the XHR object: a method called onStateChange
, which we'll discuss in a second. Next, it adds the appropriate headers to the XHR object by looping through the internal header
property and adding them using setRequestHeader
. Finally, it dispatches the request event before sending the native XHR object.
The send
method, however, is only half of the puzzle. The other half is the readystatechange
event handler method, onStateChange
. After the send
method sends the request, control of the request goes over to the onStateChange
method, which waits for the wrapper XHR object to reach the ready state of 4
. When this happens, onStateChange
prepares the response
object property of the request, setting the raw responseText
value for response.text
and raw responseXML
value for response.xml
.
The onStateChange
method then calls the isSuccess
function to check whether the request was successful. If the request was successful, the method calls the success
method, which parses and prepares the response.text
value to strip out script tags. This success
method then passes this new formatted value to the onSuccess
method, which dispatches the complete
and success
events.
Unsuccessful requests, in contrast, will make the onStateChange
method invoke the failure
method, whose main job is to invoke the onFailure
method that dispatches the complete
and failure
events.
These four methods—success, onSuccess, failure
, and onFailure
—are the usual points for subclassing. Most Request
subclasses will override these four methods in order to create a specialized version of the class.
The most basic kinds of Request
subclasses add additional options and use a different parsing method for the responseText
value. Because it is the job of the success
method to parse the response data before passing it to success
event handlers, most subclasses override only this method for their purposes.
For example, let's take one of the Request
subclasses included in MooTools Core: Request.JSON
. This is a specialized request class used for JSON requests that automatically turns the response data into a JavaScript object. Normally, if we want to include a JSON request using the Request
class, we do this:
var request = new Request({ url: 'myfile.json', method: 'get', headers: { 'Accept': 'application/json' }, onSuccess: function(text){ var obj = JSON.decode(text); if (obj){ console.log(obj.name); } else { console.log('Improper JSON response!'), } } }).send();
Here we requested a JSON file from the server. We made sure that the server will only send us JSON back by attaching an Accept
header that has the value of the JSON mimetype. In our success
event handler, we then parse the text response from the server using JSON.decode
to turn it into a JSON object. If the parsing is successful, we'll get an object result, the name
property of which we output to the console. If the parsing fails, we log an error message.
Request.JSON
automates this process, and handles the parsing process using JSON.decode
internally.
var request = new Request.JSON({ url: 'myfile.json', method: 'get', onSuccess: function(obj){ console.log(obj.name); }, onFailure: function(){ console.log('Improper JSON response!'),
} }).send();
Instead of passing a string to the success
event handler, Request.JSON
passes an object that is the parsed value of the JSON response. This saves us from having to call JSON.decode
ourselves. Request.JSON
also automatically dispatches the failure
event in the case of the response failing the JSON.decode
parsing.
The source itself of Request.JSON
is very simple:
Request.JSON = new Class({ Extends: Request, options: { secure: true }, initialize: function(options){ this.parent(options); Object.append(this.headers, { 'Accept': 'application/json', 'X-Request': 'JSON' }); }, success: function(text){ var secure = this.options.secure; var json = this.response.json = Function.attempt(function(){ return JSON.decode(text, secure); }); if (json == null) this.onFailure(); else this.onSuccess(json, text); } });
The Request.JSON
class is a direct subclass of Request
. It overrides the original initialize
method in order to add the appropriate headers to the request instance, but it also retains the original initialize process from Request
using this.parent()
. You'll see that the success
method is the only real method that's overridden. The Request.JSON
version of the success
method first tries to turn the response data into a proper JSON object using JSON.decode
. If the process fails, it calls the onFailure
method, which dispatches the failure
event. If the process succeeds, it calls the onSuccess
method, passing in the newly parsed object. The onSuccess
method will then dispatch the event handlers for the success
event, passing the parsed object.
As this example shows, subclassing the Request
object is a very simple affair. The flexibility of the original Request
class itself makes the process really simple, and gives Request
the ability to be subclassed into new and more useful classes.
In this chapter we learned all about the HTTP request-response cycle and how it affects the design of our applications. We also found out about asynchronous requests, and how the native XMLHttpRequest
object enables us to issue requests from inside our code to create more powerful and dynamic applications. Finally, we talked about the MooTools Request
class, and how it provides a nice abstraction of the native XHR API.
In the next chapter, we'll learn about another fancy technique for improving our interfaces: animation. We'll talk about how animation is done in JavaScript, as well as how the Fx
classes give us a very powerful framework for complex animations.
So if you're ready, follow my lead and jump into the magic of animation.