Using Dojo with the Zero Resource Model
Contents
Integration with Dojo assumes you have already familiar with the Zero Resource Model (ZRM). This includes declaring a dependency on ZRM, declaring models, initializing the database, and enabling the HTTP REST API.
Overview
The integration between ZRM and Dojo comprises of zero.resource.DataStore,
an implementation of the Dojo Data API, and zero.grid.DataGrid, an extension
to the dojox.grid.Grid widget that makes performing CRUD (create, retrieve,
update, and delete) operations on ZRM resources quite simple.
Adding zero.dojo to your application
The zero.resource.DataStore and zero.grid.DataGrid components are
found in the zero.dojo module. You will need to add it as a dependency in
your application in order to integrate with Dojo. Declare a dependency on
zero.dojo by adding the following line to your application's
config/ivy.xml file:
<dependency org="dojo" name="zero.dojo" rev="[1.0.0.0, 2.0.0.0["/>
After adding a dependency to your application, you must always resolve. If your application is already started, you must first stop, resolve, and restart.
Using zero.resource.DataStore programmatically
Because zero.resource.DataStore implements the Dojo Data API, you can use
it programmatically in your own JavaScript. This includes creating an instance
and invoking methods to read data from the server, create new data, and persist
changes back to the server. As you invoke these methods, ZRM's REST HTTP API
is invoked on your behalf. Thus, not only can zero.resource.DataStore be
used with Dojo dijits that can be data-bound using Dojo Data implementations,
but you can also use it to act as a proxy to data on the server.
Fetch all data
The Dojo Data API is asynchronous. This means when using
zero.resource.DataStore programmatically, most methods require callbacks. In
fact, this is exactly how the above Dojo dijits access data represented by
Dojo Data implementations.
The primary method you'll use is fetch(). This method minimally requires
callback functions to be passed in for onComplete and onError.
For instance, the following example shows how to get a ZRM collection, loop over the members in the collection and do something useful:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
// Define a callback that fires when all the items are returned.
var gotList = function(items, request) {
var itemsList = "";
dojo.forEach(items, function(i) {
itemsList += dataStore.getValue(i, "name") + " ";
});
console.debug("All items are: " + itemsList);
}
var gotError = function(error, request){
alert("The request to the store failed. " + error);
}
//Invoke the search
dataStore.fetch({
onComplete: gotList,
onError: gotError
});
Filter data with queries
zero.resource.DataStore uses ZRM's HTTP and Model API to filter
data and can use any combination of the query operators. You can filter your
resource collection by passing query to the fetch() method as in the
following example:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var gotList = function(items, request) {
// ...
}
var gotError = function(error, request){
// ...
}
var filter = "/resources/namevaluepairs?name__startswith=so";
//Invoke the search
dataStore.fetch({
query: filter,
onComplete: gotList,
onError: gotError
});
When the gotList function is called, the items argument will be filtered
according to the filter specified. For instance, in the above example, only
instances of namevaluepairs where values of the field name start with
"so" will be returned (i.e. "someone", "something", "somewhere").
Sorting data
To sort the collection by one or more fields in your data model, you must pass
an array to the fetch() method as in the following example:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var gotList = function(items, request) {
// ...
}
var gotError = function(error, request){
// ...
}
var sortKeys = [
{attribute: 'name', descending: true},
{attribute: 'value', descending: false}
];
//Invoke the search
dataStore.fetch({
sort: sortKeys,
onComplete: gotList,
onError: gotError
});
When the gotList function is called, the items argument will be sorted
according to the sortKeys array.
Paging data
The paging mechanism is used by specifying a start parameter in the
fetch() arguments. The start parameter says where, in the full list of
items, to start returning items. The index 0 is the first item in the
collection. The second argument you specify is the count argument. This
option tells zero.resource.DataStore how many items, starting at start,
to return in a request. If start isn't specified, it is assumed to be 0.
If count isn't specified, it is assumed to return all the items starting at
start until it reaches the end of the collection. With this mechanism, you
can implement simple paging easily:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var pageSize = 10;
var request = null;
var outOfItems = false;
// Define a function that will be connected to a 'next' button
var onNext = function() {
if(!outOfItems) {
request.start += pageSize;
store.fetch(request);
}
};
// Connect this function to the onClick event of the 'previous' button
// Done through dojo.connect() generally.
// Define a function will be connected to a 'previous' button.
var onPrevious = function() {
if (request.start > 0) {
request.start -= pageSize;
store.fetch(request);
}
}
// Connect this function to the onClick event of the 'previous' button
// Done through dojo.connect() generally.
// Define how we handle the items when we get it
var itemsLoaded = function(items, request) {
if (items.length < pageSize) {
// We have found all the items and are at the end of our set.
outOfItems = true;
} else {
outOfItems = false;
}
// Do something useful with a page of items
// ...
}
var args = {
onComplete: itemsLoaded,
start: 0,
count: pageSize
};
// Do the initial request.
request = store.fetch(args);
Retrieving resource metadata
The resource that zero.resource.DataStore is bound to on the server has
metadata that might be useful to your application. For instance, the
zero.grid.DataGrid uses it to automatically provide column labels and
format the data using reasonable defaults. You can access the metadata by
calling store.getMetadata() on the store instance:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var metadata = store.getMetadata();
The metadata is the same declarative JSON model that you specified in
your /app/models folder. It has some additional default attributes that
round out the model declaration if you did not provide it in the model file.
The data structure that is returned is a JavaScript object where the keys are
the field names and the values are objects with the metadata for each field.
For instance, the following is a possible JSON representation of such a data structure:
{
"first_name": {"type": "string", "max_length": 30, "label": "First name", "required": true},
"birth_date": {"type": "date", "label": "Birth date", "required": true},
"is_child": {"type": "boolean", "label": "Is child", "required": false}
}
To access the metadata, simply navigate the data structure using JavaScript field or associative array lookup:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var metadata = store.getMetadata();
var isFirstNameRequired = metadata['first_name']['required'];
var birthDateLabel = metadata.birth_date.label;
Using zero.resource.DataStore declaratively
An instance of zero.resource.DataStore can be declared in your HTML page
for reuse by data bound Dojo dijit widgets.
The following examples assume that the following namevaluepairs resource
model declaration has already been as in the following simple example:
{
"fields": {
"name":{"type": "string"},
"value":{"type":"string"}
}
}
Further, it is assumed that the model sync CLI task has executed successfully
in order to create the necessary database artifacts. Optionally, initial data
has been provided through the app/models/initial_data.json file.
Once this is done, you are now ready to use zero.resource.DataStore in an
HTML page. Do this by first using Dojo's package loading:
<script type="text/javascript">
dojo.require("dojo.parser");
dojo.require("zero.resource.DataStore");
</script>
Next declare it in an HTML tag. Many Dojo developers choose to use a div tag
as in the following example:
<div dojoType="zero.resource.DataStore" jsId="thestore"
resourceCollection="resource"></div>
Note the jsId value you provide is used later to bind Dojo widgets your
declared data store. The resourceCollection is a required attribute that
matches the server-bound resource. The contextRoot is an optional attribute.
Attaching to Dojo Widgets
Having declared a zero.resource.DataStore instance on the HTML page, you can
now attach Dojo dijits that can be data-bound to Dojo Data implementations.
zero.resource.DataStore currently supports zero.grid.DataGrid,
dojox.grid.Grid, dijit.form.ComboBox, and
dijit.form.FilteringSelect.
zero.grid.DataGrid
The zero.grid.DataGrid is an extension of Dojo's dojox.grid.Grid that
builds in operations for creating, inline editing, deleting, and sorting data.
zero.grid.DataGrid takes the following attributes:
| Attribute | Required | Default | Description |
|---|---|---|---|
| jsId | No | n/a | This attribute allows javascript
code to reference the widget by the
value associated with this attribute
as a global variable. For instance
to refer change the readonly
attribute of the widget in a
javascript function, you could do
thegrid.readonly = true;
|
| resourceCollection | No | Defaults to store value | This attribute specifies the ZRM
resource by name. This value is
ignored if a DataStore has been
specified in the store attribute.
Otherwise, it is required. |
| contextRoot | No | Defaults to store value | This attribute specifies the contextRoot of the application if any. This value is ignored if a DataStore has been specified. |
| visibleFields | No | Empty string | Comma delimited list of fields that will be displayed as columns in the grid. An empty string displays all fields. |
| readonly | No | false | Hides the toolbar and prevents fields from being edited inline. |
| store | No | n/a | The zero.resource.DataStore
instance to use to populate the
grid and persist changes back to the
server. This attribute is also
accessible from JavaScript and any
Dojo Data methods can be called on
it. |
| grid | n/a | n/a | The grid attribute can only be
referenced in JavaScript
(theGrid.grid, for instance) is
used to access the underlying
dojox.grid.Grid instance. |
| model | n/a | n/a | The model attribute can only be
referenced in JavaScript
(theGrid.model, for instance) is
used to retrieve and update the
values used by the grid. It is not
used from an HTML declaration but is
useful for manipulating the values
in the grid programmatically. |
To use zero.grid.DataGrid you use Dojo's package loading. Further, there are
also some Cascading Style Sheets that must be loaded into your HTML page in
order for the grid to render correctly. The following example shows the head
element of a page using zero.grid.DataGrid:
<head>
<title>Using DataStore with the Dojo Grid</title>
<script type="text/javascript"
src="/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script type="text/javascript">
dojo.require("dojo.parser");
dojo.require("zero.resource.DataStore");
dojo.require("zero.grid.DataGrid");
</script>
<style type="text/css">
@import "/dijit/themes/soria/soria.css";
@import "/dojo/resources/dojo.css";
@import "/zero/grid/DataGrid/DataGrid.css";
@import "/dojox/grid/_grid/Grid.css";
@import "/dojox/grid/_grid/soriaGrid.css";
</style>
</head>
You can now declare the zero.grid.DataGrid in your HTML page:
<div dojoType="zero.grid.DataGrid" jsId="thegrid"
resourceCollection="namevaluepairs"></div>
At this point you have a functional data grid that is populated with from a
ZRM Collection. You can create and manipulate resources using
zero.grid.DataGrid's attributes.
The following JavaScript example shows how to set the value of a selected field, clear the selection and save the resource representation and render the updated grid:
var rows = thegrid.grid.selection.getSelected();
if (rows.length != 1) {
alert("select one row!");
} else {
var item = thegrid.grid.model.getRow(rows[0]).__dojo_data_item;
thegrid.store.setValue(item, "field", 100);
thegrid.grid.selection.clear();
thegrid.store.save({onComplete:function () {thegrid.onSaved();}, onError:function () {alert("error");}});
}
The following JavaScript code illustrates how to create a new row in the grid associated with a resource and update the resource collection in the application:
// Set the appropriate fields of the resource.
newitem.field1 = "Value of field1";
newitem.field2 = "Value of field2"
// Add the row to the grid and create the resource.
thegrid.grid.addRow(newitem);
thegrid.store.save({onComplete:function () {thegrid.onSaved();}, onError:function () {alert("error");}});
Dojo's grid, which zero.grid.DataGrid is based on, requires an edited
cell to loose focus before the changed value is affected in a Dojo Data
implementation like zero.resource.DataStore. Otherwise, if the save
action is performed on the DataStore while editing a cell, the data sent
to the ZRM server will not contain the changes for that cell.
In order to do loose focus and save the edit value of a cell, simply press "Enter" or "Tab" or click away from the cell to loose focus of the cell.
dijit.form.ComboBox
Because of some assumptions made by dijit.form.ComboBox, it is necessary to
specify the resource to connect through the zero.resource.DataStore.
This attribute expects a JavaScript Object.
The following example shows a valid query value in order to connect to
server-side ZRM resources:
<div dojoType="zero.resource.DataStore" jsId="thestore"
resourceCollection="namevaluepairs"></div>
<input dojoType="dijit.form.ComboBox"
store="thestore"
searchAttr="name"
name="nv1"
pageSize="20" />
pageSize attribute is required.
dijit.form.FilteringSelect
There are functional differences between dijit.form.ComboBox and
dijit.form.FilteringSelect. However there are not any differences in
connecting to zero.resource.DataStore.
<input dojoType="dijit.form.FilteringSelect"
store="thestore"
searchAttr="name"
name="nv2"
autocomplete="true"
pageSize="20" />