Note: The code for this feature is available in Dojo 0.4 and later. IE 7 Support in Dojo 0.4.1 and later.
<!-- Security protection: uncomment the script tag to enable. --> comment and remove the comments from that opening script tag.<!-- Security protection: uncomment the script tag to enable. --> comment and remove the comments from that opening script tag.Most of the magic of the dojo.io package is exposed through the bind() method. dojo.io.bind() is a generic asynchronous request API that wraps multiple transport layers (queues of iframes, XMLHTTP, mod_pubsub, LivePage, etc.). Dojo attempts to pick the best available transport for the request at hand, and in the provided package file, only XMLHTTP will ever be chosen since no other transports are rolled in. The API accepts a single anonymous object with known attributes of that object acting as function arguments. To make a request that returns raw text from a URL, you would call bind() like this:
dojo.io.bind({
url: "http://foo.bar.com/sampleData.txt",
load: function(type, data, evt){ /*do something w/ the data */ },
mimetype: "text/plain"
});
That's all there is to it. You provide the location of the data you want to get and a callback function that you'd like to have called when you actually DO get the data. But what about if something goes wrong with the request? Just register an error handler too:
dojo.io.bind({
url: "http://foo.bar.com/sampleData.txt",
load: function(type, data, evt){ /*do something w/ the data */ },
error: function(type, error){ /*do something w/ the error*/ },
mimetype: "text/plain"
});
It's possible to also register just a single handler that will figure out what kind of event got passed and react accordingly instead of registering separate load and error handlers:
dojo.io.bind({
url: "http://foo.bar.com/sampleData.txt",
handle: function(type, data, evt){
if(type == "load"){
// do something with the data object
}else if(type == "error"){
// here, "data" is our error object
// respond to the error here
}else{
// other types of events might get passed, handle them here
}
},
mimetype: "text/plain"
});
One common idiom for dynamic content loading is (for performance reasons) to request a JavaScript literal string and then evaluate it. That's also baked into bind, just provide a different expected response type with the mimetype argument:
dojo.io.bind({
url: "http://foo.bar.com/sampleData.js",
load: function(type, evaldObj){ /* do something */ },
mimetype: "text/javascript"
});
And if you want to be DARN SURE you're using the XMLHTTP transport, you can specify that too:
dojo.io.bind({
url: "http://foo.bar.com/sampleData.js",
load: function(type, evaldObj){ /* do something */ },
mimetype: "text/plain", // get plain text, don't eval()
transport: "XMLHTTPTransport"
});
Being a jack-of-all-trades, bind() also supports the submission of forms via a request (with the single caveat that it won't do file upload over XMLHTTP):
dojo.io.bind({
url: "http://foo.bar.com/processForm.cgi",
load: function(type, evaldObj){ /* do something */ },
formNode: document.getElementById("formToSubmit")
});
Phew. Think that about covers the basics. Good thing you weren't planning on implementing all that stuff yourself, right?
As you have seen, Dojo provides powerful, yet simple, ways of performing a variety of I/O functions through the use of dojo.io.bind. However, during the development of a typical application, a developer will have many I/O calls to make and will typically gravitate towards a common way of making those I/O calls on both the server and the client. This will often include defining functions that take some input and perform the appropriate request, as well as hooking that request to a callback function to process the results. In effect, the developer is required to implement a way of marshaling the request to the server in a way that it expects and then to have the client receive the contents in a way it expects. Dojo's RPC service aims to make this less error prone, easy to do, and require less code.
Remote Procedure Calls (RPC), also know as Remote Method Invocations, are a mainstay of the client/server development world. Essentially, RPC allows a developer to invoke a method on a remote host. Dojo provides a basic RPC client class that has been extended to provide access to JSON-RPC services and Yahoo services. It was designed so that it is also fairly trivial to implement custom RPC services.
Let's pretend that we have a little application that we want to make some server calls with. For simplicity's sake, we'll say the methods we want the server to do are add(x,y) and subtract(x,y). Without using anything special, like an RPC client, we might do something like this:
add = function(x,y) {
request = {x: x, y: y};
dojo.io.bind({
url: "add.php",
load: onAddResults,
mimetype: "text/plain",
content: request
});
}
subtract = function(x,y) {
request = {x: x, y: y};
dojo.io.bind({
url: "subtract",
load: onSubtractResults,
mimetype: "text/plain"
content: request
});
}As you can see, this isn't particularly difficult. However, this is quite the simple application, despite our every attempt to make it complicated by having the server add or subtract two numbers instead of performing these operations in the client in the first place. What happens if our application is not so simple and has 30 different requests to make? I guess we would have to just write this same code over and over for each different request; each time making a request object, specifying URLs, potentially validating parameter types, and so on. This is simply error prone and boring to write.
Dojo's RPC clients simplify this whole process by taking a simple definition of the remote methods and application needs and generating client side functions to call these methods. A developer need only write this definition, and initialize a RPC client object and then all of these remote methods are available for the developer to use as normal.
The definition file, called a Simple Method Description (SMD) file, is a simple JSON string that defines a URL that will process the RPC requests, any methods available at that URL, and the parameters those methods take. The definition for our example above might look like this:
{
"serviceType": "JSON-RPC",
"serviceURL": "rpcProcessor.php",
"methods":[
{
"name": "add",
"parameters":[
{"name": "x"},
{"name": "y"}
]
},
{
"name": "subtract",
"parameters":[
{"name": "x"},
{"name": "y"}
]
}
]
}Once the definition has been created, the code its pretty simple. The definition can be supplied either as a URL to retrieve it, a JSON string, or a JavaScript object.
var myObject = new dojo.rpc.JsonService("http://localhost/definition.smd");
var myObject = new dojo.rpc.JsonService({smdStr: definitionJSON});
var myObject = new dojo.rpc.JsonService({smdObj: definition});
Thats it! Now all thats left is to call the method.
myObject.add(3,5);
I'll bet you are saying to yourself, "Nice try, but I want to get the results of the add method, not just call it." You are correct, but that is also simple to achieve. Recall that we are making asynchronous calls to the server. While we could make the request synchronous, it would likely provide for a bad user experience because it would block the user interface during the call. Instead, the return value of the myObject.add() call, is a deferred object. The deferred object, something that might be familiar to users of Twisted Python or MochiKit, allows a developer to attach one or more callbacks and errbacks to the resultant data event. Our simple example can be expanded as such:
var myDeferred = myObject.add(3,5);
myDeferred.addCallback(myCallbackMethod);
or more succinctly:
var myDeferred = myObject.add(3,5).addCallback(myCallbackMethod);
As you can see, we've added myCallbackMethod as a callback for the deferred object returned from myObject.add(). In this case myCallbackMethod will be called with parameter with a value of 8. Likewise, an errback method can be attached to the deferred object to process an errors returned from the server. We can add as many callbacks and errbacks to our deferred object as we want and they will be called in the order that they were connected to the deferred object.
This discussion has revolved around using dojo.rpc.JsonService, which is Dojo's JSON-RPC client. In addition to JsonService, Dojo offers an RPC client for connecting to Yahoo services, dojo.rpc.YahooService. The syntax and call structure is identical. While Dojo is currently limited to these two RPC clients, the design of the dojo.rpc.RpcService base class, which is inherited by dojo.rpc.JsonClient and dojo.rpc.YahooService allows a developer to easily customize and extend dojo.rpc.RpcService, to create services that meets their specific needs. These customizations will be discussed later in Part II when we discuss how to get the most out of Dojo.
dojo.io.bind and related functions can communicate with the server using various methods, called transports. Each has certain limitations, so you should pick the transport that works correctly for your situation.
The default transport is XMLHttp.
The IFrame I/O transport is useful because it can upload files to the server. Example usage:
<script type="text/javascript">
dojo.require("dojo.io.*");
dojo.require("dojo.io.IframeIO");
function mySubmit() {
dojo.io.bind ({
url: 'server.cfm',
handler: callBack,
mimetype: "text/plain",
formNode: dojo.byId('myForm')
});
}function callBack(type, data, evt) {
//The data object will be different
//depending on the mimetype used in the dojo.io.bind()
//call. See below for more info.
dojo.byId('result').innerHTML = data;
}
</script>
The response type from the above URL can be text, html, or JS/JSON.
IframeIO responses need to be a little different from the ones that are sent back from XMLHttpRequest responses. Because an iframe is used, the only reliable, cross-browser way of knowing when the response is loaded is to use an HTML document as the return type.
If the return type (specified by the mimetype) is text/plain, text/javascript or text/json, then the server response should be an HTML page that has a <textarea> element. The data that you want returned to the dojo.io.bind() load callback should be the text inside the textarea element. For the text/javascript or text/json return types, the text inside the textarea element will be converted to JavaScript or JSON, repectively, and that will be the data sent to the load callback.
If the return type is text/html as the return type, then the data parameter will be the complete HTML document that is in the iframe.
For IframeIO, XML responses are not supported because we can't get a nice cross-browser solution. If you want text/html as the mimetype, what you get back is the document object for the document in the iframe.
See these tests for more info:
text/plain: http://archive.dojotoolkit.org/nightly/tests/io/test_IframeIO.text.html
text/html: http://archive.dojotoolkit.org/nightly/tests/io/test_IframeIO.html.html
text/javascript: http://archive.dojotoolkit.org/nightly/tests/io/test_IframeIO.html
Due to security restrictions, XMLHttp cannot load data from another domain. The ScriptSrcIO transport is useful for doing this. Yahoo's RPC service is implemented using ScriptSrcIO.
To use ScriptSrcIO, use the following require statements
and use the normal dojo.io.bind() method.
To force a ScriptSrcTransport request, use transport: "ScriptSrcTransport" in the keyword arguments to dojo.io.bind(). The mimetype argument is also required.
Example:
dojo.require("dojo.io.*");
dojo.require("dojo.io.ScriptSrcIO");
dojo.io.bind({
url: "http://example.com/json.php",
transport: "ScriptSrcTransport",
mimetype: “application/json",
jsonParamName: "callback",
content: { ... }
});ScriptSrcIO (which provides ScriptSrcTransport) allows for four basic types of requests:
Each type uses [script src="url"][/script] to accomplish the request.
Here is a list of bind() keyword arguments that are supported for all types of requests. The four types of transport requests are:
Simply adds a script element with a src. Does not do any polling and does not expect a callback. Also does not support any timeouts. Example:
dojo.io.bind({
url: "http://the.script.url/goes/here",
transport: "ScriptSrcTransport",
mimetype: “text/javascript"
});Adds a script element with a src. It will poll to see if a typeof expression does not equal undefined. When the typeof check succeeds, a load callback is called. Timeout and error callbacks are supported with this type of request.
Example:
dojo.io.bind({
url: "http://the.script.url/goes/here",
transport: "ScriptSrcTransport",
mimetype: "text/javascript",
checkString: "foo", //This means (typeof(foo) != undefined) indicates that the script loaded.
load: function(type, data, event, kwArgs) { /* type will be "load", data and event null, , and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
error: function(type, data, event, kwArgs) { /* type will be "error", data and event will have the error, , and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
timeout: function() { /* Called if there is a timeout */},
timeoutSeconds: 10 //The number of seconds to wait until firing timeout callback in case of timeout.
});
Adds a script element with a src. This sort of usage allows using services that use the JSONP convention to specify the callback that the server will use. Specify the name of the JSONP callback parameter using jsonParamName. Yahoo! Web Services use a jsonParamName of "callback". Some other services use jsonParamName of "jsonp". Timeouts are supported with this type of request. Example for a data service that uses "callback" as the URL parameter:
dojo.io.bind({
url: "http://the.script.url/goes/here",
transport: "ScriptSrcTransport",
mimetype: "application/json",
jsonParamName: "callback",
load: function(type, data, event, kwArgs) { /* type will be "load", data will be response data, event will null, and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
error: function(type, data, event, kwArgs) { /* type will be "error", data will be response data, event will null, and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
timeout: function() { /* Called if there is a timeout */},
timeoutSeconds: 10 //The number of seconds to wait until firing timeout callback in case of timeout.
});
Here is a real example of using JSONP to look up the del.icio.us bookmarks.
<style type="text/css">
.bookmarks {
width: 300;
background: lightGray;
border-style: solid;
border-width: 2px;
border-color: black
}
</style><script type="text/javascript" src="dojo.js"></script>
<script type="text/javascript">
dojo.require("dojo.io.*");
dojo.require("dojo.io.ScriptSrcIO");
dojo.addOnLoad(getBookmarks);
function getBookmarks() {
dojo.io.bind({
url: "http://del.icio.us/feeds/json/dojomaster",
transport: "ScriptSrcTransport",
jsonParamName: "callback",
load: function(type, data, event, kwArgs){showBookmarks(data);},
mimetype: "application/json",
timeout: function() {alert('timeout');},
timeoutSeconds: 10
});
}
// The code for showing the bookmarks is courtesy of del.icio.us
// http://del.icio.us/help/json
function showBookmarks(posts) {
var ul = document.createElement('ul');
for (var i=0, post; post = posts[i]; i++) {
var li = document.createElement('li');
var a = document.createElement('a');
a.style.marginLeft = '20px';
var img = document.createElement('img');
img.style.position = 'absolute';
img.style.display = 'none';
img.height = img.width = 16;
img.src = post.u.split('/').splice(0,3).join('/')+'/favicon.ico'
img.onload = showImage(img);
a.setAttribute('href', post.u);
a.appendChild(document.createTextNode(post.d));
li.appendChild(img);
li.appendChild(a);
ul.appendChild(li);
}
document.getElementById("container").appendChild(ul);
}
function showImage(img){ return (function(){ img.style.display='inline' }) }
</script>
<div id="container" class="bookmarks"></div>To customize this scirpt simply change the URL http://del.icio.us/feeds/json/dojomaster to include your del.icio.us user name. This example shows the bookmarks for the user "dojomaster".
Adds a script element with a src. Uses the Dynamic Script Request convention to specify the callback that the server will use. Multipart requests (splitting a long request across multiple GET requests) is supported. Timeout and error callbacks are supported with this type of request. Example:
dojo.io.bind({
url: "http://the.script.url/goes/here",
transport: "ScriptSrcTransport",
mimetype: "application/json",
useRequestId: true, //adds the _dsrId to request with a generated ID. If a specific request ID is wanted, use apiId: "myId" instead
//optional: forceSingleRequest: true, //Will not segment the request to multipart requests even if it is a long URL.
constantParams: "name1=value1&name2=value2" //params to be sent with each request that is part of a multipart request. See spec.
load: function(type, data, event, kwArgs) { /* type will be "load", data will be response data, event will be onscriptload event, and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
error: function(type, data, event, kwArgs) { /* type will be "error", data will be response data, event will be onscriptload event, and kwArgs are the keyword arguments used in the dojo.io.bind call. */ },
timeout: function() { /* Called if there is a timeout */},
timeoutSeconds: 10 //The number of seconds to wait until firing timeout callback in case of timeout.
});
ScriptSrcTransport supports the following arguments across all types of requests. In general, all of these arguments have the same meaning and use in XMLHTTPTransport.
The XMLHttp transport is the default transport.
It works well in most cases, but it cannot transfer files, cannot work across domains (ie, cannot connect to another site than the current page), and doesn't work with the file:// protocol.
Example usage:
<script type="text/javascript">
dojo.require("dojo.io.*");
function mySubmit() {
dojo.io.bind ({
url: 'server.cfm',
handler: callBack,
formNode: dojo.byId('myForm')
});
}
function callBack(type, data, evt) {
dojo.byId('result').innerHTML = data;
}
</script>