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.

  • Difficulty: Beginner
  • Dojo Version: 1.8

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 contents 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 to transfer 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>";
				}
			);
		});
	}
);

View Demo

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;
			});
		});
	}
);

View Demo

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;
			});
		});
	}
);

View Demo

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.

View Demo

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.

View Demo

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);
		});
	});
});
View Demo

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"
			});
		});
	}
);
View Demo

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>";
				}
			);
		});
	}
);
View Demo

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

Error in the tutorial? Can’t find what you are looking for? Let us know!