Using Dojo with the Zero Resource Model

The Zero Resource Model has a Dojo client that makes it quite simple to interact with RESTful data from JavaScript and Dojo widgets.

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" />
At present, the 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" />

Version 1.1.0.0.21442