This tutorial steps you through creating offline web applications using Dojo
Offline.
Let's dive in and start using Dojo Offline.
First
download
the Dojo SDK and unzip it.
When you unzip the SDK, you will find the following directories:
Dojo Offline is an optional Dojo extension, and is therefore located in the dojox directory.
If you are looking to track down Dojo Offline's source code, most of it is in dojox/off/. The Dojo SQL layer is in dojox/_sql/, while Dojo Storage is in dojox/storage/. An autogenerated, JavaDoc-like API is available. When looking at the API docs or source code, many advanced options are available to deeply customize Dojo Offline; you can almost always safely ignore these unless you are an advanced user, and they usually say "For advanced usage; most developers can ignore this" in their documentation.
Dojo Offline has three main demos, a Hello World example, a more complicated
web-based editor named Moxie that includes an example server-side written in
Java, and a demo of Dojo Offline's SQL cryptography. You can play with hosted
versions of the Hello World example
here;
a hosted version of the Moxie editor
here;
and the SQL cryptography demo
here.
If you want to study the demo examples' source code, the Hello World example
is located in
dojox/off/demos/helloworld/,
while the Moxie editor is located in
dojox/off/demos/editor/.
You can see the SQL cryptography demo source in
dojox/_sql/demos/customers/customers.html.
The Hello World example has no server-side requirement; Moxie, however,
includes a full Java server-side that you can use as a template and
scaffolding. Running the server-side of Moxie is simple. Make sure you have
Java 1.5+ installed, and then just type:
java -jar editor.jar
while inside the directory
dojox/off/demos/editor/server/,
and the Moxie server-side will start running, with an embedded web-server
(Jetty) and relational database (Derby) already set up for you.
In a web-browser, you can now go to the following URL:
http://localhost:8000/dojox/off/demos/editor/editor.html
to run Moxie against the local server you just started.
For more details on the server-side portion of Moxie and how to build see the README file at dojox/off/demos/editor/server/README.
Now that you have awareness of the SDK, it's file layout, and the provided
demos, let's get down to illustrating what you need to do to create an
offline-aware application using Dojo Offline.
<script type="text/javascript" src="offline-sdk/dojo/dojo.js" djConfig="isDebug: true"></script>isDebug is a useful flag that when set to true will print out debug messages, and when set to false will hide them. In your own code you can add console.debug("some message"); to have these printed out to help with debugging. If you are using Firefox in conjunction with Firebug then these messages will print out to the Firebug console; otherwise they will print on to the web page itself, such as in Internet Explorer.
<script type="text/javascript" src="offline-sdk/dojox/off/offline.js"></script>
<style type="text/css">
@import "offline-sdk/dojo/resources/dojo.css";
/* Bring in the CSS for the default Dojo Offline UI widget */
@import "offline-sdk/dojox/off/resources/offline-widget.css";
</style>
<!--When the page loads, Dojo Offline will automatically find an element with the ID dot-widget and automagically embed the offline widget.
Place the Dojo Offline widget here; it will be automagically
filled into any element with ID "dot-widget".
-->
<div id="dot-widget"></div>
// set our application name
dojox.off.ui.appName = "Example Application";
The Offline Widget will use your application name to customize it's user-interface; that is how the offline widget, for example, can insert "Learn How to Use Hello World Offline" into its instructions without you having to manually edit the offline widget's HTML.
// automatically "slurp" the page andThe slurp() method is awesome; it will automatically scan your page and quickly figure out all your JavaScript and CSS files; grab any IMG tags in your source; and even grab any background URLs you might have in your attached CSS. The only thing it doesn't do is look at inline styles or scan your JavaScript for dynamic additions.
// capture the resources we need offline
dojox.off.files.slurp();
var myApp = {
initialize: function(){
alert("Dojo Offline and the page are finished loading!");
}
}
// Wait until Dojo Offline is ready
// before we initialize ourselves. When this gets called the page
// is also finished loading.
dojo.connect(dojox.off.ui, "onLoad", myApp, "initialize");
// tell Dojo Offline we are ready for it to initialize itself now
// that we have finished configuring it for our application
dojox.off.initialize();
myApp is some object that will hold all of the methods for our application;
myApp.initialize() is the method in particular that would initialize our application on page
and Dojo Offline load in some way. We use dojo.connect to connect to the dojox.off.ui.onLoad event;
when this fires, myApp.initialize() is called. At this point we could begin to manipulate the
DOM on the page, since it is loaded, or do further actions specific to your
application; in this case we just print an alert message.<html>
<head>
<script type="text/javascript" src="offline-sdk/dojo/dojo.js" djConfig="isDebug: true"></script>
<script type="text/javascript" src="offline-sdk/dojox/off/offline.js"></script>
<style type="text/css">
@import "offline-sdk/dojo/resources/dojo.css";
/* Bring in the CSS for the default
Dojo Offline UI widget */
@import "offline-sdk/dojox/off/resources/offline-widget.css";
</style>
<script>
// set our application name
dojox.off.ui.appName = "Example Application";
// automatically "slurp" the page and
// capture the resources we need offline
dojox.off.files.slurp();
var myApp = {
initialize: function(){
alert("Dojo Offline and the page are finished loading!");
}
}
// Wait until Dojo Offline is ready
// before we initialize ourselves. When this gets called the page
// is also finished loading.
dojo.connect(dojox.off.ui, "onLoad", myApp, "initialize");
// tell Dojo Offline we are ready for it to initialize itself now
// that we have finished configuring it for our application
dojox.off.initialize();
</script>
</head>
<body>
<!--
Place the Dojo Offline widget here; it will be automagically
filled into any element with ID "dot-widget".
-->
<div id="dot-widget"></div>
</body>
</html>
Important: I've noticed over and over in the frameworks I have created, such as Dojo Storage and the Really Simple History library, that developers get confused about an important point. Notice that configuring Dojo Offline must be done before the page loads:
<script>
// set our application name
dojox.off.ui.appName = "Example Application";
// automatically "slurp" the page and
// capture the resources we need offline
dojox.off.files.slurp();
// ...
// Wait until Dojo Offline is ready
// before we initialize ourselves. When this gets called the page
// is also finished loading.
dojo.connect(dojox.off.ui, "onLoad", myApp, "initialize");
// tell Dojo Offline we are ready for it to initialize itself now
// that we have finished configuring it for our application
dojox.off.initialize();
</script>
Notice that our calls to things like dojox.off.ui.appName and the dojo.connect to dojox.off.ui are at the top-level of the script tag done before the page loads; if you were to put these into a function and call that after the page has loaded, Dojo Offline will not work:
<script>
// this is wrong -- don't do this because it will not work
window.onload = function(){
// set our application name
dojox.off.ui.appName = "Example Application";
// automatically "slurp" the page and
// capture the resources we need offline
dojox.off.files.slurp();
// etc.
}
</script>
At this point we have the shell of an offline application using Dojo Offline and Google Gears. Let's delve into common issues that come up with offline applications now, such as toggling your user-interface in different ways if you are on- or off-line; knowing the network status; syncing; and more.
In the background Dojo Offline is checking to make sure that your web application is available on the network (for technical details on what it is doing see here). If your web application disappears or appears within a few seconds (five to thirty seconds), Dojo Offline will detect this and automatically inform your application so that you can move on- or offline, such as enabling or disabling UI elements that might not be available offline. Dojo Offline fires an event when the network status changes, which you can easily subscribe to using Dojo Connect:
dojo.connect(dojox.off, "onNetwork", function(status){
if(status == "online"){
alert("We are online!");
}else if(status == "offline"){
alert("We are offline!");
}
});
By default, the Dojo Offline UI widget also subscribes to these events, handling
most of the work of informing the user when they are on- or off-line so you
don't have to:
var searchButton = dojo.byId("searchButton");
dojox.connect(searchButton, "onclick", function(evt){
if(dojox.off.isOnline){ searchOnline(); }
else{ searchOffline(); }
});
This can be a nice pattern to separate on- and off-line code and easily toggle
between them without littering lower level code with dojox.off.isOnline network checks; just
do it at the dojo.connect level instead, which is cleaner.
An offline application is useless if it does not have access to data. Dojo
Offline provides two ways to store data, depending on your needs. The first is
Dojo Storage, which provides a simple, persistent hash table abstraction; and
Dojo SQL, which provides SQL based access to your data.
Dojo Storage contains a very simple hash table abstraction, providing methods such as put() and get(), which you can use to quickly store data without having to delve into SQL. Under the covers it saves this data into the Google Gears persistent storage system.
Saving data is easy. You can store simple strings:
dojox.storage.put("someKey", "someValue");
or more complicated JavaScript objects:
Loading data is just as easy; if you stored a JavaScript object, it will be returned to you as a JavaScript object:
var value = dojox.storage.get("someKey");
var car = dojox.storage.get("complexKey");
If the object is not found, null is returned.
Further info and advanced usage on Dojo Storage can be found here.
Dojo Storage can be great, but some times you need the full power of a relational data store. Enter Dojo SQL.
Dojo SQL is an easy to use, thin layer over Google Gear's relational storage layer. Executing SQL is easy:
var results = dojox.sql("SELECT * FROM DOCUMENTS");
Results are returned as ordinary JavaScript objects, unlike Google Gears, which makes working with SQL much easier. For example, if you have created a table named DOCUMENTS:
dojox.sql("CREATE TABLE IF NOT EXISTS DOCUMENTS ("
+ "fileName TEXT NOT NULL PRIMARY KEY UNIQUE, "
+ "content TEXT NOT NULL) ");
that has five rows in it (i.e. five documents), and you call dojox.sql("SELECT *
FROM DOCUMENTS"), an array
will be returned that has five rows, one for each document. Dojo SQL creates an
object for each row, automatically taking each of the column names, such as
fileName and content, and using those as the object literals:var results = dojox.sql("SELECT * FROM DOCUMENTS");
// document 1
alert("The first documents file name is " + results[0].fileName + " and it's content is " + results[0].content);
// document 2
alert("The second documents file name is " + results[1].fileName + " and it's content is " + results[1].content);
Inserting data is just as easy:dojox.sql("INSERT INTO DOCUMENTS (fileName, content) VALUES (?, ?)", fileName,
contents);
Simply put a question mark for each parameter in the SQL, and then provide the
actual variables that will fill them in as variable length arguments at the end.
You can use this for any SQL statement where you want to provide a parameter:dojox.sql("SELECT * FROM DOCUMENTS WHERE fileName = ?", someFileName);
dojox.sql("CREATE TABLE CUSTOMERS ("
+ "last_name TEXT, "
+ "first_name TEXT, "
+ "social_security TEXT"
+ ")"
);
For the first and last names, we don't care if they are stored in the clear.
However, for the social security column we would like to encrypt it.
var password = "foobar";
dojox.sql("INSERT INTO CUSTOMERS VALUES (?, ?, ENCRYPT(?))", "Neuberg", "Brad", "555-34-8962",
password,
function(results, error, errorMsg){
if(error){ alert(errorMsg); return; }
});
In the example above, we provide our three INSERT parameters as usual; however,
for the last one, the social security number, we put the ENCRYPT(?) keyword around
it. After providing these as variable arguments at the end, we provide a password.
The password is used for encryption rather than a key -- it is passed into the
underlying cryptographic system; you should not store this password as a
cookie or into Dojo Storage or Dojo SQL. Instead, the user should be prompted to
enter it when they start using your application. If you store it then a laptop
thief could simply use it to unlock the local data store.
var password = "foobar";
dojox.sql("SELECT last_name, first_name, DECRYPT(social_security) FROM CUSTOMERS",
password,
function(results, error, errorMsg){
if(error){ alert(errorMsg); return; }
// go through decrypted results
alert("First customer's info: "
+ results[0].first_name + " "
+ results[0].last_name ", "
+ results[0].social_security);
});
In this example we simply print out the decrypted results for the first row. If
the password is wrong then the decrypted results will still be decrypted; there is
no way to detect an incorrect password attempt programatically.Whether you use Dojo Storage or Dojo SQL, there is an important scenario you need to think about. If the user loads your application while offline, you must initialize your application using your stored data rather than making a network call. You should do this after Dojo Offline has loaded:
initialize: function(){
// initialize our UI using offline data if necessary
var documents = null;
if(dojox.off.isOnline){
// make a network call to get our data
// ...
}else{ // we are offline
documents = dojox.storage.get("documents");
}
}
In the example above, our initialize() method is called when Dojo Offline is done loading. Our UI must then get a list of documents to make available; if we are online (dojox.off.isOnline), then we just make a network call. If we are offline, then we just load our documents from offline storage, in this case Dojo Storage.
We cover how to get your downloaded data for offline use in the next section, covering syncing.
As soon as you start creating offline applications syncing becomes an issue.
However, if you are not careful, trying to figure out syncing can become an
endless black hole that causes your project to spiral and fail. Syncing can
either be relatively easy or infinitely complex, depending on how you approach
the issue. In order to help, Dojo Offline includes both a syncing framework,
named Dojo Sync, as well as a set of syncing guidelines to help you avoid sync
land mines. We've thought through many of the hard issues and created a path
for you to navigate through the sync process.
Lets begin with the guidelines.
First, user's view syncing as a bug, not a feature. They don't want to toy with sync interfaces -- they just want syncing to happen automatically.
Along these lines, user's don't understand merge interfaces. Developers are pretty comfortable with merging files, such as using Subversion or CVS. Users, however, don't care and don't understand merge interfaces. If you are popping up a complicated diff and merge UI as you do syncing, then you have failed.
Therefore, Dojo Sync recommends that rather
than showing merge interfaces when syncing, the client- or server-side should
automatically make a best guess, without UI intervention by the user.
If a document was modified locally while offline, and also modified on the
server by someone else, for example, they should be automatically merged
without user intervention. If there is a conflict, you should make a best
decision on which to keep based on your application. You could choose to keep
the newest one, for example, and could also make a backup in the document's
history list of the other change in case the user needs to retrieve something
from it. Do merging automatically, and decide
the best way your application can support this.
With all this said, users still need basic UI
feedback that syncing is occurring and where in the sync process they
are. Syncing should not be completely invisible; just as a browser has
a basic network throbber to show you network activity is happening without
showing the entire intricacy of the process, syncing is the same way. Users
want to know that syncing is occurring; see from a high-level where the
syncing is at (uploading, downloading, etc.); whether syncing succeeded or
failed; and want the option of finding out why syncing failed or any automatic
merges that might have happened. Even though users don't want to do merging,
they some times want the option to look at a sync log to see what was merged.
This should be done in a way that doesn't clutter the main sync UI (i.e. it
should be a sync log link, for example, that would display a pop up window
with a sync log of what was merged and what conflicted, and how the server did
automatic merging).
Finally, user's don't want to have to manually
always kick off syncing -- they just want the application to work correctly
offline and automatically start syncing at correct times. In light of
this, an application should always sync when it first loads and the network is
present, bringing down a subset of data to make available offline. Users
rarely know when they will be offline, so if the application simply always
syncs then there should be some set of data available offline when the user
loses the network. Likewise, when the network reappears, the application
should automatically sync any local changes back to the server, with the user
not needing to kick this off.
dojo.connect(dojox.off.sync, "onSync", this, function(type){
});
There are many different onSync events, given by the type variable, but you only need to be concerned with a few unless you are doing something unusual (you can see a full list of the onSync events here).
When the sync process reaches the downloading stage, onSync will fire with type equal to the string download. You should listen for the download event and proceed to download your data. In the example code below, when the sync framework wants us to download, we make a network call to some web service that fetches new documents, for example:
dojo.connect(dojox.off.sync, "onSync", this, function(type){
if(type == "download"){
// download new documents
dojo.xhrGet({
url: "/documents/download",
handleAs: "javascript",
load: function(data){
dojox.storage.put("documents", data);
dojox.off.sync.finishedDownloading();
},
error: function(err){
err = err.message||err;
var message = "Unable to download our documents from server: "
+ err;
dojox.off.sync.finishedDownloading(false, message);
}
});
}
});
In the example code above, we first subscribe to onSync events. When a sync event fires, if it is the download event, we then proceed to make an XMLHttpRequest call to some web service that downloads new documents, located at "/documents/download", which returns JavaScript JSON as its results. We use Dojo's easy to use dojo.xhrGet() method to do so. xhrGet() can take an error handler and a load handler, as well as allowing us to specify how the return results should be handled. In the example above, our handleAs: "javascript" parameter says that the results will just be handed to us as a JavaScript object.
Let's look at the load() method first. If the web service returned correctly, we simply take the data returned, which would be our list of documents, and store it right into Dojo Storage with a put() call; we could also have used Dojo SQL here and iterated over the results to write into a relational store if we wanted.
The next line is important:
dojox.off.sync.finishedDownloading()
You must call this method when you are done downloading. Since downloading data tends to be an asychronous process that involves talking to the network, Dojo Sync has no way to know when it can continue the syncing process after downloading is complete. When you call dojox.off.sync.finishedDownloading() from your network callback, it tells Dojo Sync to continue. If you do not, Dojo Sync will get stuck at the downloading phase and your user-interface will just show "Downloading new data..." forever.
Let's look at the error callback now. If an error occurs during downloading, you must also call dojox.off.sync.finishedDownloading(), but with two arguments. The first should be false to indicate that downloading was unsuccessful; the second argument should be the reason that downloading failed, which will be reported in the sync log at the end of the sync session.
We will see how to handle sync uploading in the next section, but first there is one more sync event that you will find useful in your code. When syncing is finished a sync event is fired with type "finished":
dojo.connect(dojox.off.sync, "onSync", this, function(type){
if(type == "download"){
// download data
// ...
}else if(type == "finished"){
// refresh our UI when we are finished syncing
var documents = dojox.storage.get("documents");
dojo.forEach(documents, function(i){
// update the UI list of documents with this new document
// ...
});
}
});In the example code above, when the finished event is fired, we load all of our new documents from Dojo Storage and then use each one to update our user-interface. The finished event can be useful for updating your UI once syncing is done.
Note that this event is fired whether an error ocurred or not during syncing; check dojox.off.sync.successful and dojox.off.sync.error for sync details. These are boolean properties that will tell you whether an error occurred or not.
Sync uploading is the trickiest part of syncing in general. Before we can tackle that, let's take a quick look at how Dojo Sync recommends you record user actions when they are done offline.
What do you do when a user is offline, but starts doing user actions that would normally cause a network call or which updates data?
// some new client added by sales person
var client = {lastName: “Doe”, firstName: "John", phoneNumber: “555-222-1212”);
// create an Action object to represent this action --
// these can have anything you want in them -- as you will
// see later, they should have enough data to help you replay them
// when we go back online
var action = {name: "new contact", data: client};
// add this to our offline action log
dojox.off.sync.actions.add(action);
// persist the contact into offline storage
dojox.storage.put("John Doe", client);
In the example code above, we first have an object that represents the new client that was created, John Doe. Next, we create an action object to represent this offline action; the action object can have any values you want -- it must simply have enough data for you to be able to replay this action when we go back online. In general, you will have an action name, such as "new contact", and some data that goes along with this action, which in this case is the newly created client.
When the network reappears, Dojo Sync kicks off and starts automatic syncing. After refreshing the offline UI, Dojo Sync then attempts to upload any changes that were made while offline. Basically, Dojo Sync tries to replay the action log you built up while the user was offline, executing each one one at a time.
However, Dojo Sync doesn't know how to replay actions -- thats the job of your application. You register yourself to know when replaying has occurred, and Dojo Sync calls your replay function for each action, simply handing it the action object you created earlier while offline. You can now use this action object to execute the action, but this time while online.
Let's build this code segment up one piece at a time. First, we register to know when replaying has occurred:
dojo.connect(dojox.off.sync.actions, "onReplay", this,
function(action, actionLog){
}
);
The function we give will be called over and over for each action that was added to the offline log. The offline log is just a persistent array, or list of actions in the order they were done by the user and added by you to the action log. This function is given each of the actions, one at a time, as the first argument action, and a reference to the action log itself, actionLog. Your code should then look at the action and try to replay it:
dojo.connect(dojox.off.sync.actions, "onReplay", this,
function(action, actionLog){
if(action.name == "new contact"){
}
}
);
In this case we just check the action.name property, which we set earlier when the user actually performed this action. If it is the value "new contact", then we try to simply create this new contact:
dojo.connect(dojox.off.sync.actions, "onReplay", this,
function(action, actionLog){
if(action.name == "new contact"){
var contact = action.data;
// create this new contact on the network
dojo.xhrPost({
url: "/contact/" + contact.lastName + "/"
+ contact.firstName,
content: { "content": dojo.toJson(contact) },
error: function(err){
var msg = "Unable to create contact "
+ contact.firstName + " "
+ contact.lastName + ": " + err;
actionLog.haltReplay(msg);
}
},
load: function(data){
actionLog.continueReplay();
}
});
}
}
);
There's alot going on in the code block above. First, if we see that the action was to create a new contact, "new contact", then we actually shoot off an HTTP POST to create this new contact. We get the contact to create from the data:
var contact = action.data;
One final property that is useful is dojox.off.sync.actions.isReplaying. Sometimes you want to share the same network code whether you are online or replaying an offline action, since you don't want to have to rewrite the network call. In this case in your load or error callbacks you can see if replaying is occurring, and if so tell the action log to continue replaying or to halt. If you are not replaying, you would just handle the load or error as normal.
During syncing, we always refresh the list of offline files. This is somewhat due to limitations in the underlying Google Gears APIs we use (for the technically inclined, we use Google Gear's ResourceStore instead of it's ManagedResourceStore). This can slow down syncing considerably, however, since every time we sync we also pull down the offline files.
Dojo Offline has an optional feature to speed this up. Your application can have an optional version.js file bundled in the same directory as it. During syncing, Dojo Offline will read this file to see if the version of the application has changed. If it has then we go ahead and refresh all the offline files; if not, then we skip this step.
The version.js file is very simple; it should simply have the version inside of it:
"07-12-2007.9"
This can be a string or number; it does not matter. We simply look to see if the value has changed. We also force an offline file refresh if you are in debug mode (i.e. djConfig.isDebug is true), or if you just debugged the prior time the page was loaded.
Simply place this file in the same directory as your main HTML file.
This is all your should need to know to work with Dojo Sync. For further details and advanced usage see here.
dojox.off.files.cache("images/some_image_added_by_javascript.png");
To add many files at once:dojox.off.files.cache([
"images/file1.png",
"images/file2.png",
"scripts/some_dynamic_script_tag.js"
]);
You can call cache() as many times as you want; duplicates are automatically removed, and new cached additions are simply added to the end of our internal cached file list.
Dojo Offline has a timer that every fifteen seconds is checking to make sure that your web application is available. By default, it looks for a simple static text file on your web server bundled with the Dojo Offline framework, located at dojox/off/network_check.txt. This file is very short, with just the number 1 in it. We use a static text file rather than hitting your main web page since the main web application is probably dynamically generated and hitting it every 15 seconds would impose scalability limits, while a static text file is simple to serve.
You shouldn't need to change this part of the framework, but if you need to the interval for checking can be set on dojox.off.NET_CHECK and the availability URL can be changed from the network_check.txt file to your own custom page by setting dojox.off.availabilityURL. If you want to completely disable this feature, you can set dojox.off.doNetChecking to false.
dojox.off.ui.onlineImagePath = "images/myonlineball.png";If you want to brand the Learn How page in some special way, its HTML is at dojox/off/resources/learnhow.html and its CSS is inside dojox/off/resources/offline-widget.css, near the bottom of the file.
The underlying Google Gears SQL data store must be opened before SQL can be executed, and should be closed when it is not used, though it will recover if you do not close it. Dojo SQL automatically does this open/close routine to simplify your code. When you execute a SQL statement with Dojo SQL, it will automatically open the database, execute your SQL, and close it. If you are working on performance sensitive code, where you have a for loop for example that is rapidly executing hundreds of inserts, this behavior could slow your app down considerably. In this case, you can just manually call dojox.sql.open() and dojox.sql.close() yourself, and Dojo SQL will automatically detect this and leave the database connection alone and open:
var bigData = getBigData(); // get an array that has a bunch of data in it
dojox.sql.open();
for(var i = 0; i < bigData.length; i++){
dojox.sql("INSERT INTO MYTABLE (LAST_NAME, FIRST_NAME) VALUES (?, ?)", bigData[i].lastName, bigData[i].firstName);
}
dojox.sql.close();
Google Gears also supports having multiple named relational data stores for a single application; Dojo SQL does not currently expose this functionality, and stores its dataset as a Google Gears relational data store under the name given by dojox.sql.dbName, which is currently PersistentStorage. Note that if you have multiple applications running on the same web site they will create their tables in the same data store, the PersistentStorage one, which can create conflicts; this is a known issue that will be fixed in the future.