Ajax with dojo/request
dojo/request is a new API (introduced in Dojo 1.8) for making requests to a server from the client. This tutorial introduces the dojo/request
API: you'll learn how to request a text file from the server, handle errors if they occur, post information to the server, take advantage of the notify API, and use the registry to use the same code to request data from different locations.
Getting Started
dojo/request
allows you to send and receive data to and from the server without reloading the page
(commonly known as AJAX). The new features introduced make written code more compact and the execution lightning
fast. This tutorial mentions dojo/promise
and dojo/Deferred
, which
dojo/request
uses for asynchronous programming. Because it's impossible to learn everything at
once, just keep in mind that promises and Deferreds allow for easier programming of non-blocking asynchronous code.
After this tutorial, you'll want to check out those tutorials.
Introducing dojo/request
Let's take a look at a simple example:
require(["dojo/request"], function(request){
request("helloworld.txt").then(
function(text){
console.log("The file's content is: " + text);
},
function(error){
console.log("An error occurred: " + error);
}
);
});
In a browser, the code above will execute an HTTP GET request using an XMLHttpRequest
to
helloworld.txt
and return a
dojo/promise/Promise
. If the request is successful, the
first function passed to then()
is executed with the text of the file as its only argument; if the
request fails, the second function passed to then()
will execute with an error object as its only
argument. But what if there is form data to send to the server? Or the response is JSON or XML? No problem —
the dojo/request
API allows for request customization.
The dojo/request API
Every request needs one thing: an end-point. Because of this, dojo/request
's first parameter is
the URL to request.
Web developers need flexibility in their tools in order to adapt them for their applications and for multiple
environments. The dojo/request
API takes this into account: the first, non-optional, parameter to
dojo/request
is the URL to request. A second parameter can be specified to customize a request using
an Object
. Some of the most-used options available are:
- method - An uppercase string representing the HTTP method to use to make the request.
Several helper functions are provided to make specifying this option easier (`request.get`, `request.post`, `request.put`, `request.del`).
- sync - A boolean that, if true, causes the request to block until the server has
responded or the request has timed out.
- query - A string or key-value object containing query parameters to append to
the URL.
- data - A string, key-value object, or
FormData
object containing data totransfer to the server.
- timeout - Time in milliseconds before considering the request a failure and triggering
the error handler.
- handleAs - A string representing how to convert the text payload of the response before
passing the converted data to the success handler. Possible formats are "text" (the default), "json", "javascript", and "xml".
- headers - A key-value object containing extra headers to send with the request.
Let's take a look at an example using some of these options:
require(["dojo/request"], function(request){
request.post("post-content.php", {
data: {
color: "blue",
answer: 42
},
headers: {
"X-Something": "A value"
}
}).then(function(text){
console.log("The server returned: ", text);
});
});
This example executes an HTTP POST request to post-content.php
; a simple object (data
)
is also serialized and sent as POST data with the request as well as an "X-Something" header. When the server
responds, the payload is used as the value of the promise returned from request.post
.
Examples: request.get and request.post
The following are some common uses of dojo/request
.
Display the contents of a text file on a page
This example uses dojo/request.get
to request a text file. A good use of this approach would be to provide text for terms and conditions or privacy for a site, because the text files would only be sent to the client if they were specifically requested, and it is easier to maintain text in a file than in code.
require(["dojo/dom", "dojo/on", "dojo/request", "dojo/domReady!"],
function(dom, on, request){
// Results will be displayed in resultDiv
var resultDiv = dom.byId("resultDiv");
// Attach the onclick event handler to the textButton
on(dom.byId("textButton"), "click", function(evt){
// Request the text file
request.get("../resources/text/psalm_of_life.txt").then(
function(response){
// Display the text file content
resultDiv.innerHTML = "<pre>"+response+"</pre>";
},
function(error){
// Display the error returned
resultDiv.innerHTML = "<div class=\"error\">"+error+"<div>";
}
);
});
}
);
Login demo
In the example below, a POST request is used to send the username and password to the server and the result from the server is displayed.
require(["dojo/dom", "dojo/on", "dojo/request", "dojo/dom-form"],
function(dom, on, request, domForm){
var form = dom.byId('formNode');
// Attach the onsubmit event handler of the form
on(form, "submit", function(evt){
// prevent the page from navigating after submit
evt.stopPropagation();
evt.preventDefault();
// Post the data to the server
request.post("../resources/php/login-demo.php", {
// Send the username and password
data: domForm.toObject("formNode"),
// Wait 2 seconds for a response
timeout: 2000
}).then(function(response){
dom.byId('svrMessage').innerHTML = response;
});
});
}
);
Headers demo
In the example below, a POST request is used as above, and the Auth-Token header is accessed.
To access the headers, use the promise.response.getHeader
method of the
original Promise
(The Promise
returned from the XHR
will
not have this property).
Additionally, when using promise.response.then
, the response will not be the data,
but an object with a data property.
require(["dojo/dom", "dojo/on", "dojo/request", "dojo/dom-form"],
function(dom, on, request, domForm){
// Results will be displayed in resultDiv
var form = dom.byId('formNode');
// Attach the onsubmit event handler of the form
on(form, "submit", function(evt){
// prevent the page from navigating after submit
evt.stopPropagation();
evt.preventDefault();
// Post the data to the server
var promise = request.post("../resources/php/login-demo.php", {
// Send the username and password
data: domForm.toObject("formNode"),
// Wait 2 seconds for a response
timeout: 2000
});
// Use promise.response.then, NOT promise.then
promise.response.then(function(response){
// get the message from the data property
var message = response.data;
// Access the 'Auth-Token' header
var token = response.getHeader('Auth-Token');
dom.byId('svrMessage').innerHTML = message;
dom.byId('svrToken').innerHTML = token;
});
});
}
);
JSON (JavaScript Object Notation)
JSON is a very common way to encode data for AJAX requests, because it is easy to read, easy to work with, and very compact. JSON can be used to encode any type of data: JSON support is included in or available for many languages, including PHP, Java, Perl, Python, Ruby, and ASP.
JSON encoded object
{
"title":"JSON Sample Data",
"items":[{
"name":"text",
"value":"text data"
},{
"name":"integer",
"value":100
},{
"name":"float",
"value":5.65
},{
"name":"boolean",
"value":false
}]
}
When handleAs
is set to "json"
, dojo/request
treats the response payload
as JSON data and parses it into a JavaScript object.
require(["dojo/dom", "dojo/request", "dojo/json",
"dojo/_base/array", "dojo/domReady!"],
function(dom, request, JSON, arrayUtil){
// Results will be displayed in resultDiv
var resultDiv = dom.byId("resultDiv");
// Request the JSON data from the server
request.get("../resources/data/sample.json.php", {
// Parse data from JSON to a JavaScript object
handleAs: "json"
}).then(function(data){
// Display the data sent from the server
var html = "<h2>JSON Data</h2>" +
"<p>JSON encoded data:</p>" +
"<p><code>" + JSON.stringify(data) + "</code></p>"+
"<h3>Accessing the JSON data</h3>" +
"<p><strong>title</strong> " + data.title + "</p>" +
"<p><strong>items</strong> An array of items." +
"Each item has a name and a value. The type of " +
"the value is shown in parentheses.</p><dl>";
arrayUtil.forEach(data.items, function(item,i){
html += "<dt>" + item.name +
"</dt><dd>" + item.value +
" (" + (typeof item.value) + ")</dd>";
});
html += "</dl>";
resultDiv.innerHTML = html;
},
function(error){
// Display the error returned
resultDiv.innerHTML = error;
});
}
);
In addition to the encoding the data as JSON in the response, set the Content-Type header to application/json, either using server configuration such as Apache's AddType or adding it to the header with the server side code.
JSONP (Javascript Object Notation with Padding)
AJAX requests are restricted to the current domain. If you need to request data from a different domain, you can use JSONP. When using JSONP, a script tag is inserted in the current page, the src file is requested, the server wraps the data in a callback function, and when the response is interpreted, the callback is called with the data as its first argument. JSONP requests are made with dojo/request/script.
Let's take a look at a few examples:
Using JSONP to request data from a server and handling the response
require(["dojo/dom", "dojo/on", "dojo/request/script",
"dojo/json", "dojo/domReady!"
], function(dom, on, script, JSON){
// Results will be displayed in resultDiv
var resultDiv = dom.byId("resultDiv");
// Attach the onclick event handler to the makeRequest button
on(dom.byId('makeRequest'),"click", function(evt){
// When the makeRequest button is clicked, send the current
// date and time to the server in a JSONP request
var d = new Date(),
dateNow = d.toString();
script.get("../resources/php/jsonp-demo.php",{
// Tell the server that the callback name to
// use is in the "callback" query parameter
jsonp: "callback",
// Send the date and time
query: {
clienttime: dateNow
}
}).then(function(data){
// Display the result
resultDiv.innerHTML = JSON.stringify(data);
});
});
});
Since the response is JavaScript, not JSON, the Content-Type header on the response should be application/javascript.
Using JSONP to request Dojo pull requests from the GitHub API
require(["dojo/dom", "dojo/on", "dojo/request/script",
"dojo/dom-construct", "dojo/_base/array",
"dojo/domReady!"
], function(dom, on, script, domConstruct, arrayUtil){
var pullsNode = dom.byId("pullrequests");
// Attach the onclick event handler to tweetButton
on(dom.byId("pullrequestsButton"), "click", function(evt){
// Request the open pull requests from Dojo's GitHub repo
script.get("https://api.github.com/repos/dojo/dojo/pulls", {
// Use the "callback" query parameter to tell
// GitHub's services the name of the function
// to wrap the data in
jsonp: "callback"
}).then(function(response){
// Empty the tweets node
domConstruct.empty(pullsNode);
// Create a document fragment to keep from
// doing live DOM manipulation
var fragment = document.createDocumentFragment();
// Loop through each pull request and create a list item
// for it
arrayUtil.forEach(response.data, function(pull){
var li = domConstruct.create("li", {}, fragment);
var link = domConstruct.create("a", {href: pull.url, innerHTML: pull.title}, li);
});
// Append the document fragment to the list
domConstruct.place(fragment, pullsNode);
});
});
});
Reporting Status
dojo/request/notify provides a mechanism to report the status of requests made with dojo/request
(or any provider within dojo/request
). Requiring dojo/request/notify
will allow the providers to emit events which can be listened to and used to report the status of requests. To listen for an event, call the return value of the dojo/request/notify
module with two parameters: an event name and a listener function. The following are the events that dojo/request
providers emit:
Supported dojo/request/notify events
- start - Emitted when the first in-flight request starts
- send - Emitted prior to a provider sending a request
- load - Emitted when a provider receives a successful response
- error - Emitted when a provider receives an error
- done - Emitted when a provider finishes a request, regardless of success or failure
- stop - Emitted when all in-flight requests have finished
Listeners of "start"
and "stop"
receive no arguments. Listeners of "send"
receive two arguments: an object representing the request and a cancel function. Calling the cancel function will cancel the request before it begins. Listeners of "load"
, "error"
, and "done"
receive one argument: an object representing the response from the server. Let's take a look at an example of this in action:
Using dojo/request/notify to monitor the progress of requests
require(["dojo/dom", "dojo/request", "dojo/request/notify",
"dojo/on", "dojo/dom-construct", "dojo/query",
"dojo/domReady!"],
function(dom, request, notify, on, domConstruct){
// Listen for events from request providers
notify("start", function(){
domConstruct.place("<p>Start</p>","divStatus");
});
notify("send", function(data, cancel){
domConstruct.place("<p>Sent request</p>","divStatus");
});
notify("load", function(data){
domConstruct.place("<p>Load (response received)</p>","divStatus");
});
notify("error", function(error){
domConstruct.place("<p class=\"error\">Error</p>","divStatus");
});
notify("done", function(data){
domConstruct.place("<p>Done (response processed)</p>","divStatus");
if(data instanceof Error){
domConstruct.place("<p class=\"error\">Error</p>","divStatus");
}else{
domConstruct.place("<p class=\"success\">Success</p>","divStatus");
}
});
notify("stop", function(){
domConstruct.place("<p>Stop</p>","divStatus");
domConstruct.place("<p class=\"ready\">Ready</p>", "divStatus");
});
// Use event delegation to only listen for clicks that
// come from nodes with a class of "action"
on(dom.byId("buttonContainer"), ".action:click", function(evt){
domConstruct.empty("divStatus");
request.get("../resources/php/notify-demo.php", {
query: {
success: this.id === "successBtn"
},
handleAs: "json"
});
});
}
);
dojo/request/registry
dojo/request/registry provides a mechanism to route requests based on the URL requested. Common uses of the registry are to assign a provider based on whether the request will be made to the current domain using JSON, or to a different domain using JSONP. You may also use this approach if the URLs can vary based on the operations in progress.
dojo/request/registry syntax
request.register(url, provider, first);
dojo/request/registry parameters
url - The url may be a string, regEx, or function.
- string - If the url is a string, the provider will be used if the url is an exact match.
- regExp - If the url is regular expression, the provider will be used if the regular expression matches the requested URL.
- function - If the url is a function, the function will be passed the URL and options object of the request. If the function returns a truthy value, the provider will be used for the request
- provider - The provider to use to handle the request.
- first - An optional boolean parameter. If truthy, registers the provider before other already registered providers.
Let's take a look at one final example:
Using dojo/request/registry to assign the provider based on the URL of requests
require(["dojo/request/registry", "dojo/request/script", "dojo/dom",
"dojo/dom-construct", "dojo/on", "dojo/domReady!"],
function(request, script, dom, domConstuct, on){
// Registers anything that starts with "http://"
// to be sent to the script provider,
// requests for a local search will use xhr
request.register(/^https?:\/\//i, script);
// When the search button is clicked
on(dom.byId("searchButton"), "click", function(){
// First send a request to twitter for all tweets
// tagged with the search string
request("http://search.twitter.com/search.json", {
query: {
q:"#" + dom.byId("searchText").value,
result_type:"mixed",
lang:"en"
},
jsonp: "callback"
}).then(function(data){
// If the tweets node exists, destroy it
if (dom.byId("tweets")){
domConstuct.destroy("tweets");
}
// If at least one result was returned
if (data.results.length > 0) {
// Create a new tweet list
domConstuct.create("ul", {id: "tweets"},"twitterDiv");
// Add each tweet as an li
while (data.results.length>0){
domConstuct.create("li", {innerHTML: data.results.shift().text},"tweets");
}
}else{
// No results returned
domConstuct.create("p", {id:"tweets",innerHTML:"None"},"twitterDiv");
}
});
// Next send a request to the local search
request("../resources/php/search.php", {
query: {
q: dom.byId("searchText").value
},
handleAs: "json"
}).then(
function(data){
dom.byId('localResourceDiv').innerHTML =
"<p><strong>" + data.name + "</strong><br />" +
"<a href=\"" + data.url + "\">" + data.url + "</a><br />";
},
function(error){
// If no results are found, the local search returns a 404
dom.byId('localResourceDiv').innerHTML = "<p>None</p>";
}
);
});
}
);
Best Practices
Best practices for using dojo/request
include:
- Careful choice of request method. Generally, GET is used for simple requests of data without security considerations. GET is often faster than POST. POST is usually used to send form data and when the data should not be passed on the URL.
- Use of HTTPS for data which should be protected and on HTTPS pages.
- Since AJAX requests don't refresh the page, most users appreciate status updates, from Loading ... to Done.
- Error callbacks should be used for graceful detection and recovery of request failures.
- Use available developer tools to resolve problems more quickly.
- Test your code carefully with as many browsers as possible.
Conclusion
dojo/request
provides a cross-browser compliant AJAX interface for requests to the current domain and others, including graceful error handling, support for notification, and request routing based on URL. The promise returned by dojo/request
is a promise, allowing a series of requests to be issued and the responses processed asynchronously. Pages can include content from multiple sources and use the data from each request as soon as it is available. Turbocharge your pages with dojo/request
!
Resources
- dojo/request Documentation
- JSONP Tutorial
- Getting Started with Deferreds Tutorial
- Dojo Deferreds and Promises Tutorial
- JSON Introducing JSON
- JSONP JSON-P Documentation
- Comparison of GET and POST
- Future and Promises Wikipedia article