Dynamic web applications that avoid page refreshes are very powerful, but this normally means that the Back and Forward buttons stop working in the browser. In addition, it can be hard to give the user an URL that can be bookmarked.
However, Dojo provides a solution to these issues, allowing a web application to capture the Back and Forward button clicks, and set a unique URL in the browser's location field. The solution is to use dojo.undo.browser.
The behavior described in this document is different from the 0.2.2 Dojo release. In that release, it was not very easy to track the state that corresponded to a "page", but it was possible to register callbacks for Back and Forward through a dojo.io.bind() call. For 0.3 and later, the Back/Forward/Bookmarking support was moved to a new module, dojo.undo.browser. As a result of the changes, you may see slightly different behavior as compared with 0.2.2. The main difference is the need to call dojo.undo.browser.setInitialState(state) to set the state for the page as it is first loaded by the browser.
Dynamic web applications that use things like XMLHTTPRequest and DOM updates instead of page refreshes do not update the browser history, and they do not change the URL of the page. That means if the user clicks the Back button, they will likely jump all the way out of the web application, losing any state that they were in. It is also hard to allow a user to bookmark the web application at a certain state.
Dojo's dojo.undo.browser module will introduce browser history so that it is possible for the user to click Back and Forward without leaving the web application, and the developer can get notification of these Back and Forward events and update the web application appropriately. Browser history is generated by using a hidden IFRAME and/or adding a unique value to the fragment identifier portion of the page URL. The fragment identifier is the #value thing in a URL. For example:
http://some.domain.com/my/path/to/page.html#fragmentIdentifier
Since changing the fragment identifier does not cause the page to refresh, it is ideal for maintaining the state of the application. The developer can specify a more meaningful value for the fragment identifier to allow bookmarking.
dojo.undo.browser allows setting a state object that represents the state of the page. This state object will get callbacks when the Back or Forward button is pressed. In addition to registering state objects with dojo.undo.browser directly, the state object can be passed to dojo.io.bind() and it will be added to dojo.undo.browser for you.
The following prerequisites are needed to use dojo.undo.browser:
Register the initial state of the page by calling:
dojo.undo.browser.setInitialState(state);
This state object will be called when the user clicks Back all the way back to the start of the web application. If the user clicks Back once more, they will go back in the browser to wherever they were before loading the web application.
The state object should have the following functions defined:
Example of the a very simple state object:
var state = {
back: function() { alert("Back was clicked!"); },
forward: function() { alert("Forward was clicked!"); }
}; To register a state object that represents the result of a user action, use the following call:
dojo.undo.browser.addToHistory(state);
or, if you are using dojo.io.bind(), if the object contains the function back() or backButton() or the property changeUrl, then dojo.io.bind() will call dojo.undo.browser for you (only works with XMLHTTPTransport and ScriptSrcTransport).
To change the URL in the browser's location bar, include a changeUrl property on the state object. If this property is set to true, dojo.undo.browser will generate a unique value for the fragment identifier. If it is set to any other value (except undefined, null, 0 or empty string), then that value will be used as the fragment identifier. This will allow users to bookmark the page.
There are test pages that shows how to use all of the pieces together:
http://archive.dojotoolkit.org/dojo-2007-05-15/ajax/tests/undo/test_brow...
http://archive.dojotoolkit.org/dojo-2007-05-15/ajax/tests/undo/test_brow...
What usability problems?
Consider that most painful of topics for web application developers: the back button. Web developers armed with some sample code, a decent DOM reference, and a lot of perseverance can build a pretty decent dynamic UI in modern browsers. These UIs doesn't jarringly destroy the user's in-page experience for the most trivial of tasks, like adding an item to a list. When larger portions of an application are mediated in this way, the user naturally has more desire to "go back" to some earlier state if things aren't working out the way they had planned or if the action isn't what they expected. An example might be switching between a view and edit mode in a content editing application.
As high-gloss web applications become the norm, many interactions become intra-page and not inter-page. Programmers looking for creative solutions have chosen XMLHTTP for these scenarios, but unfortunately, XMLHTTP breaks the back button, impairing the user experience. If the back button doesn't function in a way that meets user expectations, it becomes ever easier for the user to lose work or become confused about the state of an application. To assist the user, programmer need a way to capture back-button presses and do something intelligent with them.
If you've used Google Maps and you've tried to send your directions to a friend, you know that not being able to simply copy the URL out of the address bar is significantly confusing at first. Applications that dynamically construct large sections of the UI (like Google Maps) today resort to a link in an intermediate screen that the user can click to return to their current state and then, perhaps, book mark or send to someone else. And this is if and when they consider the "bookmarkability" problem at all. More common is an application that simply refuses to acknowledge that the user might want to pass around a URL to a friend and instead builds some heavyweight and non-standard state serialization mechanism that is more akin to a desktop application's "save as" feature. "Save-as" on the web is book-marking, and usable applications recognize this (even if they don't have great solutions for it today). Regardless of what serialization mechanism is in use, being able to represent the state of the application in a URL (or a marker for serialized state) is a must. This is a hard problem to be sure, and none of the currently available tools provide simple answers.
movies.csv ==> CsvStore ==> movies.html
As of January 2007, we have five simple datastores, which are included in dojo as example datastore implementations. The four datastores are:
| dojo.data.CsvStore | a read-only store that reads tabular data from .csv format files |
| dojo.data.OpmlStore | a read-only store that reads hierarchical data from .ompl format files |
| dojo.data.YahooStore | a read-only store that fetches search results from the Yahoo search engine web service |
| dojo.data.DeliciousStore | a read-only store that fetches bookmarks from the del.icio.us web service |
| dojo.data.RdfStore | a read-write store that uses SPARQL to talk to RDF data servers including, for example, the Rhizome RDF application server |
http://dojotoolkit.org/api/#dojo.data
You can also look at the API definition files themselves to see the documentation about individual API methods:
In the dojo.data unit-tests, there's an example page that allows you to read data from any of a variety of different data sources and then display data in a few different widgets:
We also have a number of dojo.data unit-test pages, which may serve as simple examples of how to use the APIs:
The widget code itself can be independent of the dojo.data APIs, and know nothing about how data access is done. The widget binding code depends on both dojo.data APIs and on the widget itself, but the dojo.data APIs are independent of both the widget itself and the widget binding. The widget and the binding are both independent of any particular datastore implementation, which allows a single widget binding to be used with a variety of datastores implementations.
Once one person has written one binding to display dojo.data items in a FilteringTable, then data items from any datastore can be displayed in a FilteringTable. If Dojo someday has 15 datastore implementations, and has 20 data display widgets, then dojo authors will only need to write 20 bindings in order to connect all the widgets to all the datastores. Without a standard dojo.data API, we would need a different bit of intermediary code for each possible connection between a widget and a datastore -- with 15 datastores and 20 widgets, that would require 300 different pieces of intermediary data translation code.
+-----------+
Widgets Bindings | dojo.data | Datastores Data Sources
| APIs |
+------------------+ | |
| Trees | | |
| (TreeV3) |-- binding --| |
+------------------+ | | +------------+ +-----------+
| |---| CsvStore |---| .csv file |
+------------------+ | | +------------+ +-----------+
| Tables & Grids | | |
| (FilteringTable) |-- binding --| | +------------+ +-------------------+
+------------------+ | |---| YahooStore |---| Yahoo web service |
| | +------------+ +-------------------+
+------------------+ | |
| Charts & Graphs | | | +------------+ +------------+
| (Chart) |-- binding --| |---| OpmlStore |---| .opml file |
+------------------+ | | +------------+ +------------+
| |
+------------------+ | | +------------+ +------------+
| Other widgets | | |---| RdfStore |---| RDF server |
| (ComboBox) |-- binding --| | +------------+ +------------+
| (SlideShow) |-- binding --| |
| (etc.) |-- binding --| | +------------+ +--------------------+
+------------------+ | |---| Other |---| other file formats |
+-----------+ | stores | | and web services |
+------------+ +--------------------+
We are designing the data-access APIs with a wide variety of use cases in mind, and we hope that the APIs will work well with many different kinds of data: RDF, XML, SQL, CSV, OMPL, etc. Here are some of the features we've been designing the APIs to support:
We hope that the dojo.data APIs will support:
We hope to eventually have different datastore implementations that read data from a wide variety of data sources. Here's a list of some of the kinds of data sources that we've had in mind while designing the APIs:
| Author: | Brad Neuberg |
|---|---|
| Version: | 0.5 |
| Copyright: | Dojo Foundation, 2006 |
| Date: | 2006/11/18 |
There are a number of modules in dojo for UI related tasks
Dojo provides three modules with basic functions on the document tree:
All of the functions above are written to either mask browser incompatibilites, or to provide functionality that wasn't provided within the standard javascript API.
All of the functions can take either a node or an id as their first argument.
One particular set of functions to note are the sizing functions.
An HTML block element has the following format:
+-------------------------+
| margin |
| +---------------------+ |
| | border | |
| | +-----------------+ | |
| | | padding | | |
| | | +-------------+ | | |
| | | | content | | | |
| | | +-------------+ | | |
| | +-|-------------|-+ | |
| +-|-|-------------|-|-+ |
+-|-|-|-------------|-|-|-+
| | | | | | | |
| | | |<- content ->| | | |
| |<------ inner ------>| |
|<-------- outer -------->|
+-------------------------+
There are three sizes associated with this element:
Depending on browser, and mode, asking for the width/height of an element will return different results; sometimes it will give you the content size and sometimes it will give you the inner size. Therefore, it's important to always use dojo's functions for getting/setting the size;
An HTML element can have multiple classes, such as:
Dojo provides functions to handle this class string as an ordered set, so you don't need to do string manipulations to add/remove items from the class list:
There are many more DOM related functions. Please see the reference doc for details.
- dojo.html.addClass(node, className)
- dojo.html.prependClass(node, className)
- dojo.html.removeClass(node, className)
- dojo.html.replaceClass(node, className, oldClassName)
Drag and Drop (DnD)
When Microsoft and Netscape introduced Dynamic HTML (different versions of course), the drag-and-drop demos elicited much applause. You could put a shopping cart on the left and catalog items on the right, and the customer could drag items right to the shopping cart. Neato! It looked just like a client-server application.
Because of the incompabilities between DHMTL, writing a cross-platform drag-and-drop application was difficult. Dojo makes it easy by layering an easy-to-use API over the top.
To drag and drop, you need three things:
- Something to drag, called a DragSource. In Dojo, any HTML element can be one.
- Some place to drop, called a DropTarget. This too can be any HTML element.
- Something to do when the item is dropped
A Simple Example
(Under construction)
Here's a simple application of drag and drop to simulate a kitchen. You have three drop targets - Frying Pan, Pot of Boiling Water, and Oven - and two drag sources - Egg and Chicken Leg. A user can drag the word Egg to the box surrounding Pot of Boiling Water to simulate boiling the egg. You can drag the foods from their initial locations, or from one destination to another, as in dragging the Egg from the Pot of Boiling Water to the Frying Pan.
<script type="text/javascript' src="/path/to/dojo.js"></script> <script type='text/javascript"> dojo.require("dojo.dnd.*"); dojo.require("dojo.event.*"); function initKtichen() { // "pan" matches the id for the <div> tag with id="pan" below // The [] around "dest" are required, for reasons we'll see later new dojo.dnd.HtmlDropTarget(dojo.byId("pan"), ["dest"]); new dojo.dnd.HtmlDropTarget(dojo.byId("pot"), ["dest"]); new dojo.dnd.HtmlDropTarget(dojo.byId("oven"), ["dest"]); // "dest" matches the "dest" in the DropTarget's above. new dojo.dnd.HtmlDragSource(dojo.byId("egg"), "dest"); new dojo.dnd.HtmlDragSource(dojo.byId("leg"), "dest"); } dojo.addOnLoad("initKitchen"); </script> </head> <body> <H1>Food</H1> <div id="egg">Egg</div> <div id="leg">ChickenLeg</div> <H1>Destinations</H1> // This is the pan, the first drop target <div id="pan" style="border:3px soid black;width:200px"> Frying Pan </div> <div id="pot" style="border:3px solid black;width:200px"> Pot of Boiling Water </div> <div id="oven" style="border:3px soid black;width:200px"> Oven </div> <br />(Vegetarians may substitute soy products with no loss of functionality) Running this example, you notice things happen automagically:
- The item moves with your mouse pointer as you drag
- When you drop the item on a drop target, the drop target expands to hold the item
- If you try to drop an item outside of a drop target, the item snaps back to its original position.
- A horizontal line appears where the drop will occur. In this example, you'll see the top border of the drop target become "fatter".
The HTMLDragSource and HTMLDropTarget constructor calls contain two arguments:
The example does a lot with a little bit of code. In the next examples, we'll work on making the user interface discoverable, meaning the user can figure it out with visual cues.
- The component do be dragged or dropped, respectively. dojo.byId() is helpful here, and an easier-to-type shortcut for document.getElementById().
- A destination code to specify which drag sources can do to which drop targets. We'll see that work in LimitingDragAndDropOptions.
Beautification
Let's put those events to good use. When a user drags an object, they'd like to know where it's legal to drop. We can help out by visually indicating a legal drop target. Going forward with our kitchen example, when the user drags a food item to a utensil, we'll make the utensil border red. When they drop, or drag out of that utensil, we'll make it black again.
To accomplish this, the natural events to use are onDragOver and onDragOut. These act a lot like the onMouseOver and onMouseOut attributes of an HTML tag.
function initKitchen(){
dojo.declare("dojo.book.dnd.DestDropTarget",dojo.dnd.HtmlDropTarget,{
onDragOver: function(e) {
// domNode is the drop target we're over
this.domNode.style.borderColor = "red";
dojo.dnd.HtmlDropTarget.prototype.onDragOver.apply(this, arguments);
},
onDragOut: function(e) {
// this.domNode is the drop target we're leaving
this.domNode.style.borderColor = "black";
dojo.dnd.HtmlDropTarget.prototype.onDragOut.apply(this, arguments);
}
});
new dojo.book.dnd.DestDropTarget(dojo.byId("pan"), ["dest"]);
new dojo.book.dnd.DestDropTarget(dojo.byId("pot"), ["dest"]);
new dojo.book.dnd.DestDropTarget(dojo.byId("oven"), ["dest"]);
new dojo.dnd.HtmlDragSource(dojo.byId("egg"), "dest");
new dojo.dnd.HtmlDragSource(dojo.byId("leg"), "dest");
}
Drag and Drop Actions
OK, we have drag sources and drop targets. But without an action, drag and drop isn't very interesting. We want something to happen when the item is dropped.
Because Drag and Drop uses Dojo's event model, you can set up actions with very few lines of code. (If you haven't reviewed the Object Oriented Concepts section, now's a good time.) Here's a simple example, which displays an alert box when the item is dropped.
function initKitchen(){ dojo.declare(dojo.book.dnd.DestDropTarget",dojo.dnd.HtmlDropTarget,{ onDrop: function(e) { alert('Ready to cook!'); // Call the superclass method to do the actual dropping dojo.dnd.HtmlDropTarget.prototype.onDrop.apply(this, arguments); } }); new dojo.book.dnd.DestDropTarget(dojo.byId("pan"), ["dest"); new dojo.book.dnd.DestDropTarget(dojo.byId("pot"), ["dest"]); new dojo.book.dnd.DestDropTarget(dojo.byId("oven", ["dest"]); new dojo.dnd.HtmlDragSource(dojo.byId("egg"), "dest"); new dojo.dnd.HtmlDragSource(dojo.byId("leg"), "dest"); }Notice how the drop target object type is no longer HtmlDropTarget, but your new class DestDropTarget. This is simple inheritance. A DestDropTarget is a more specific kind of HtmlDropTarget. Most of the methods are delegated to the superclass, HtmlDropTarget, but our specific functionality for onDrop is added before the superclass call.
Drop Events
onDrop is the most common event to override for drop targets. But there are others. To connect an action to one of these events, simply specify the method for that event in your dojo.declare call. All events without an action will be handled by HtmlDropTarget.
- onDragOver(e) - called when the user begins dragging a source over this drop target. It is called only once when the drag source "flies over".
- onDragMove(e) - called repeatedly as the drag source is over the drop target. You shouldn't perform any long calculations here.
- onDragOut(e) - the opposite of onDragOver, this is called when the drag source leaves the drop target area without having been dropped. This is called only once.
So the events fire like this:
- For dragging across a drop target without dropping - onDragOver(), onDragMove(), onDragMove(), ... onDragMove(), onDragOut().
- For dragging into a drop target and dropping - onDragOver(), onDragMove(), onDragMove(), ... onDragMove(), onDrop().
Like all event handlers, your code must include one parameter for the event information itself. This object is of type dojo.dnd.DragEvent, because Drag events and Drop events have identical event information. DragEvent contains the following readable fields:
The onDropEvent also has onDropStart and onDropEnd events before and after, accordingly. onDropStart is a good place to verify the drop target is OK, although destinations do the job easier.
- dragObject - the object being dragged.
- dragSource - the dragSource being dragged. Note that dragSource.dragObject is the same as dragObject. (Confusing, but remember the HtmlDragSource constructor takes two parameters: the object and the possible destinations).
- target - the drop target for that drop. Null if the drag source hasn't been dropped yet.
Drag Events
Each of these events also has a DragEvent parameter with event information.
Note that not every drag ends with a drop! The user can drag a source, then leave the browser window entirely and let go of the mouse button. Nothing will be dropped, since a browser doesn't usually interact with other windows. But onDragEnd will be called nonetheless.
- onSelected(e) - called when a drag source is clicked
- onDragStart(e) - called once when dragging begins
- onDragEnd(e) - called once when dragging ends. For drops, this event is called right before onDrop.
Limiting Drag and Drop Options
Using default settings as we have, the user can drag items all over the page. They may have to try several potential drop before stumbling on the right one. You can help them out by limiting their choices.
Disabling Drop Targets for Certain Sources
Continuing from our example, suppose that chicken legs can go into all destinations: the oven, the pot, or the frying pan. But eggs can go only into the pot or pan, not in the oven. (Yes there is such a thing as baked eggs, but you're not Wolfgang Puck!)
That's where the "dest" parameter comes in. The second argument for both HtmlDragSource and HtmlDropTarget, this acts much like the name="..." parameter of radio buttons. They tie different drop sources with a common set of rules.
In the following example, we set up two destination groups: allCookingOptions, and topOfStoveOnly. The HTML portion is exactly like our first example, but we change initKitchen to:
function initKitchen(){
// A pan can be a drop target for any drag source of type topOfStoveOnly or
// allCookingOptions.
new dojo.dnd.HtmlDropTarget(dojo.byId("pan"), ["allCookingOptions","topOfStoveOnly"]);
new dojo.dnd.HtmlDropTarget(dojo.byId("pot"), ["allCookingOptions","topOfStoveOnly"]);
new dojo.dnd.HtmlDropTarget(dojo.byId("oven"), ["allCookingOptions"]);
// An egg can only be dropped on a target of type topOfStoveOnly
new dojo.dnd.HtmlDragSource(dojo.byId("egg"), "topOfStoveOnly");
new dojo.dnd.HtmlDragSource(dojo.byId("leg"), "allCookingOptions");
}You may wonder, "Why the brackets around destinations in a drop target?" Drag sources belong to one and only one group, but drop targets can belong to many groups. That's why drop targets always have an array of destinations signified by [...], even when there's only one group.
Constraining The Drag Area
To make the User Interface more discoverable, you can put a boundary around all the possible drop targets.  This prevents the drag source from leaving the area, even if the mouse is dragged outside. The technique is especially helpful if you have two or more sets of drag and drop areas. For example, if you had a drop area of domestic cities and a drop area of international cities, you could constrain domestic planes (our drag source) to the domestic cities.ÂÂ
To constrain the drag sources, you use the constrain() method. That requires first constructing the HtmlDragSource object, as we have in previous examples. Then you call constrain with the id of the bounding box. Note this can be any HTML object, and the boundaries of that object act as the constraining box. Butis a natural choice for a bounding box.ÂÂExtending our kitchen example, the user will not be able to drag the egg or chicken leg outside an imaginary box containing all three destinations.
function initKitchen(){ ... // Construct drop sources // Keep the egg from leaving the boundaries of the object kitchenDiv var eggSource = new dojo.dnd.HtmlDragSource(dojo.byId("egg"), "dest"); eggSource.constrainTo("kitchenDiv"); // Do the same with the chicken leg legSource = new dojo.dnd.HtmlDragSource(dojo.byId("leg"), "dest"); legSource.constrainTo("kitchenDiv"); } ... <H1>Destinations </H1> <div id="kitchenDiv"> <div id="pan" ...The user cannot drag either the egg or the chicken leg outside of the bounding box, although they can try to drop it (unsuccessfully) between the destinations. Later we'll see how to give more visual cues to the user as to where they can drop.
List Rearrangement
Yes, you can use Drag and Drop outside of the kitchen! One popular application is rearranging lists. You can do this by enabling list items as drag sources and the list itself as a drop target. That seems kind of wierd at first - you do not expect drag sources and drop targets to overlap, much less contain one another. Here, it helps to think of dragging and dropping as a shortcut for cut-and-paste.
In this example you can drag the elements, Jim Hendrix albums, into whatever order you wish.
<html><head> <script type="text/javascript" src="/path/to/dojo.js"></script> <script type="text/javascript"> dojo.require("dojo.dnd.*"); dojo.require("dojo.dnd.event.*"); function initList() { // Loop through all li elements of list, and make them drop targets var dl = dojo.byId("listToRearrange"); var lis = dl.getElementsByTagName("li"); for (var i=0; i<lis.length; i++) new dojo.dnd.HtmlDragSource(lis[i], "dest"); new dojo.dnd.HtmlDropTarget(dl,"dest"); } dojo.addOnLoad(initList); </script> </head> <body> <H1>Jimi Hendrix Albums</H1> <p>Arrange in order of your own preference.</p> <ul id="listToRearrange"> <li>Electric Ladyland</li> <li>Are You Experienced?</li> <li>Axis, Bold as Love</li> </ul> </body> </html>Once this is done, you can use getElementsByTagName to loop through the elements in their new order, then send them by dojo.io or a page submission.
LFX
The dojo.lfx.* module is dojo's animation system. It includes many “canned� effects:In addition, it has a powerful system for chaining together primitives:
- fadeIn, fadeShow, fadeOut, fadeHide,
- wipeIn, wipeOut
- slideTo
- explode, implode
- highlight, unhighlight
// wipe two elements out, one after
// the other, following a 300ms delayvar anim1 = dojo.lfx.wipeOut(�foo�, 300);
var anim2 = dojo.lfx.wipeOut(�bar�, 500);
var composed = dojo.lfx.chain(anim1, anim2);
composed.play(300);// fade out three nodes together, using
// accelerationdojo.lfx.fadeOut(
[�foo�, “bar�, “baz�],
300,
dojo.lfx.easeInOut
).play();