Using Dojo with the Zero Resource Model
To integrate with Dojo, you should already be 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. You should also have an understanding of the Dojo Data API specification.
Integration between ZRM and Dojo is comprised of the following components:
-
zero.resource.DataStore - An implementation of the Dojo Data API.
-
zero.grid.DataGrid - An extension to the
dojox.grid.Gridwidget that simplifies CRUD (create, retrieve, update, and delete) operations on ZRM resources.
Adding zero.dojo to your application
The zero.resource.DataStore and zero.grid.DataGrid components are
found in the zero.dojo module. Add it as a dependency in
your application to integrate with Dojo. Declare a dependency on
zero.dojo by adding the following line to the config/ivy.xml file of your application:
<dependency org="dojo" name="zero.dojo" rev="[1.0.0.0, 2.0.0.0["/>
After adding a dependency to your application, resolve the dependency. If your application is already started, you must first stop, resolve, and restart.
Using zero.resource.DataStore
The zero.resource.DataStore component is an implementation of the Dojo Data API. It
supports the Read, Write, Notification, and Identity APIs. The parameters shown in the following table are
valid parameters for its constructor.
| 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 example,
to change the read only
attribute of the widget in a
JavaScript function, you could use the following example:
thegrid.readonly = true;
|
| resourceCollection | Yes | n/a | This attribute specifies the ZRM resource by name. |
| contextRoot | No | Defaults to /resources | This attribute specifies the relative path from the page URI to the parent of the resource. For example, if the page URI is /people/index.html, then contextRoot should be set to ../resources. The default value is appropriate only for pages in the application root (for example /index.html). |
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, creating new data, and persisting
changes back to the server. As you invoke these methods, the REST HTTP API
of ZRM is invoked on your behalf. Therefore, 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.
Fetching all data
The Dojo Data API is asynchronous. So when you are using
zero.resource.DataStore programmatically, most methods require callbacks. This is how the Dojo dijits access data represented by
Dojo Data implementations.
The primary method to use is fetch(). This method minimally requires
callback functions to be passed in for onComplete and onError.
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
});
Filtering data with queries
The zero.resource.DataStore component uses the HTTP and Model API of ZRM to filter
data. It can use any combination of the query operators. For more information about query operators, see the Model API information about filter conditions. You can filter your
resource collection by passing query to the fetch() method as shown in the following example:
var store = new zero.resource.DataStore({ resourceCollection: "resource" });
var gotList = function(items, request) {
// ...
}
var gotError = function(error, request){
// ...
}
var filter = { name__startswith: "so" };
//Invoke the search
dataStore.fetch({
query: filter,
onComplete: gotList,
onError: gotError
});
zero.resource.DataStore using JavaScript objects.
When the gotList function is called, the items argument is filtered
according to the filter specified. In the previous example, only
instances of namevaluepairs where values of the field name start with
"so" are returned (for example, "someone", "something", "somewhere").
Sorting data
To sort the collection by one or more fields in your data model, pass
an array to the fetch() method as shown 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 is 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 is not specified, it is 0.
If count is not specified, it returns all the items starting at
start until it reaches the end of the collection. With this mechanism, you
can implement simple paging easily, as shown in the following example:
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 example, 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, as shown in the following example:
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 in which the keys are
the field names and the values are objects with the metadata for each field. The following example shows 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, navigate the data structure using the JavaScript field or associative array lookup, as shown in the following example:
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 namevaluepairs
resource has been defined by the following model:
{
"fields": {
"name":{"type": "string"},
"value":{"type":"string"}
}
}
It is also assumed that the model sync command has run successfully
in order to create the necessary database artifacts. Optionally, initial data
has been provided through the app/models/initial_data.json file.
When this is done, you can use zero.resource.DataStore in an
HTML page. Do this by first using Dojo's package loading, as shown in the following example:
<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="namevaluepairs"></div>
The jsId value you provide is used later to bind Dojo widgets to your
declared data store.
The resourceCollection is a required attribute that
matches the server-bound resource. The contextRoot attribute must be specified if
the page is not located in the application root (for example, /people/index.html requires a
contextRoot; /index.html does not). See the table of definitions in the Using zero.resource.DataStore section for details.
Attaching to Dojo Widgets
Having declared a zero.resource.DataStore instance on the HTML page, you can
attach Dojo dijits that can be data-bound to Dojo Data implementations.
zero.resource.DataStore currently supports the following extensions:
-
zero.grid.DataGrid -
dojox.grid.Grid -
dijit.form.ComboBox -
dijit.form.FilteringSelect
Using zero.grid.DataGrid
The zero.grid.DataGrid extension of Dojo's dojox.grid.Grid builds operations for creating, inline editing, deleting, and sorting data.
zero.grid.DataGrid takes the attributes described in the following table.
| Attribute | Required | Default | Description |
|---|---|---|---|
| jsId | No | n/a | Allows JavaScript
code to reference the widget by the
value associated with this attribute
as a global variable. For example,
to refer change the readonly
attribute of the widget in a
JavaScript function, you could use the following example:
thegrid.readonly = true;
|
| resourceCollection | No | Defaults to store value | 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 | Specifies the relative path from the page URI to the parent of the resource. For example, if the page URI is /people/index.html, then contextRoot should be set to ../resources. This attribute is ignored if store has been specified. The default value is appropriate only for pages in the application root (for example, /index.html). |
| visibleFields | No | Empty string | Comma delimited list of fields that display 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 example) 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 example) 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 use Dojo's package loading. There are
also some Cascading Style Sheets that must be loaded into your HTML page 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>
When that is defined, you can declare the zero.grid.DataGrid in your HTML page, as shown in the following example:
<div dojoType="zero.grid.DataGrid" jsId="thegrid"
resourceCollection="namevaluepairs"></div>
The functional data grid is now populated from a
ZRM Collection. You can create and manipulate resources using
the attributes of zero.grid.DataGrid.
The following JavaScript example sets the value of a selected field, clears the selection and saves the resource representation and renders 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 creates a new row in the grid associated with a resource and updates 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, on which zero.grid.DataGrid is based, 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 does not contain the changes for that cell.
To lose focus and save the edit value of a cell, click Enter or Tab. Click away from the cell to just remove focus from the cell.
Using dijit.form.ComboBox
Because some assumptions are made by dijit.form.ComboBox, you must specify the resource to connect through zero.resource.DataStore.
This attribute expects a JavaScript object.
The following example shows a valid query value 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.
Using 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, as shown in the following example.
<input dojoType="dijit.form.FilteringSelect"
store="thestore"
searchAttr="name"
name="nv2"
autocomplete="true"
pageSize="20" />
zero.form.widget.Form
zero.form.widget.Form is a widget provided by the
zero.dojo module. This widget can simplify the creation of
forms for ZRM-based resources by rendering a form based on a simple (JSON)
description of the form.
First create a .json file in /public/zero/forms/ with a description
of the form following this example:
{
"model": "modelExample",
"title": "Form Title",
"description": "This is the description of the form",
"fields": [
{
"title": "String short",
"dataId": "stringField1",
"shortName":"stringField_id",
"hint":"string hint",
"required": true,
"type": "Text",
"lengthProperty":"short",
"numOfChar":3,
"description": "Text field1",
"default_value": "default value1",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "String medium",
"dataId": "stringField2",
"shortName":"stringField_id",
"hint":"string hint",
"required": true,
"type": "Text",
"lengthProperty":"medium",
"numOfChar":5,
"description": "Text field2",
"default_value": "default value2",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "String long",
"dataId": "stringField3",
"shortName":"stringField_id",
"hint":"string hint",
"required": false,
"type": "Text",
"lengthProperty":"medium",
"numOfChar":7,
"description": "Text field3",
"default_value": "default value3",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Booolean Field Title",
"dataId": "booleanField",
"shortName":"booleanField_id",
"required": false,
"type": "Checkbox",
"description": "this is checkbox",
"default_value": "true",
"immutable": false,
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Date Field1",
"dataId": "dateField1",
"shortName":"dateField_id1",
"required": false,
"type": "Date",
"max": "2008-08-29",
"min": "2008-08-20",
"description": "Date Field1",
"default_value": "08/22/2008",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Date Field2",
"dataId": "dateField2",
"shortName":"dateField_id2",
"required": true,
"type": "Date",
"max": "2008-08-29",
"min": "2008-08-20",
"description": "Date Field2",
"default_value": "08/30/2008",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Date Field3",
"dataId": "dateField3",
"shortName":"dateField_id3",
"required": true,
"type": "Date",
"min": "2008-08-20",
"description": "Date Field3",
"default_value": "08/15/2008",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Date Field4",
"dataId": "dateField4",
"shortName":"dateField_id4",
"required": true,
"type": "Date",
"min": "2008-08-20",
"description": "Date Field4",
"default_value": "08/25/2008",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Time Field1",
"dataId": "timeField1",
"shortName":"timeField_id1",
"required": false,
"type": "Time",
"description": "Time Field1",
"max": "10:00:00",
"min": "07:00:00",
"default_value": "09:45 AM",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Time Field2",
"dataId": "timeField2",
"shortName":"timeField_id2",
"required": true,
"type": "Time",
"description": "Time Field1",
"max": "10:00:00",
"min": "07:00:00",
"default_value": "11:45 AM",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Decimal Field",
"dataId": "decimalField",
"shortName":"decimalField_id",
"required": false,
"type": "Number",
"description": "Decimal Field",
"max": "5",
"min": "-4",
"digitPlaces": "3",
"default_value": "2.16768",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Integer Field",
"dataId": "integerField",
"shortName":"integerField_id",
"required": false,
"type": "Number",
"description": "Integer Field",
"default_value": "123",
"immutable": false,
"onValid":"codeToExecute",
"onInvalid":"codeToExecute",
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute"
},
{
"title": "Select Field",
"dataId": "someDataField",
"shortName":"selectField_id",
"required": false,
"type": "Select",
"description": "Select Description",
"immutable": false,
"onMouseOut":"codeToExecute",
"onChange":"codeToExecute",
"onMouseOver":"codeToExecute",
"onClick":"codeToExecute",
"options":
[
{opt:"opt1",val:"val1"},
{opt:"opt2",val:"val2"},
{opt:"opt3",val:"val3"},
{opt:"opt4"}
],
"selected_option": "opt4"
}
]
}
Declare the zero.form.widget.Form in your HTML
page, as shown in the following example:
<html>
<head>
<title>Using the zero form widget</title>
<script type="text/javascript"
src="/dojo/dojo.js"
djConfig="parseOnLoad: true"></script>
<script type="text/javascript">
dojo.require("dojo.parser");
dojo.require("zero.form.widget.Form");
</script>
</head>
<body>
<div dojoType="zero.form.widget.Form" formModel="/zero/forms/myform.json">
</div>
</body>
</html>
The following table describes the attributes for the form widget.
| Attribute | Required | Default | Description |
|---|---|---|---|
| jsId | No | n/a | Allows JavaScript code to reference the widget by the value associated with this attribute as a global variable. |
| formModel | Yes | n/a | Specifies the location
of the form JSON file. For example,
/forms/studentForm3.json. |
| dataGridId | No | n/a | Page-relative URI to the form JSON file.
For example, if the page URI is /people/index.html,
then formModel should be set to
../zero/forms/myform.json. |
Instead of creating a JSON file with the form description,
you can use the setContent() API and pass it a
JavaScript object containing the form description.
You can also set a dataGridId in the instantiation
of the zero.form.widget.Form by performing
theFrom.dataGridId = theGridId; prior to the call
to the setContent method. This creates a
linkage between the form and the grid to interact in a syncronized fashion.
You can add a field to the form by declaring
var newField = {...}; where the content is
exactly like one of the objects in the fields array of the form description. Then call theForm.addField(newField);.
You can add multiple fields by declaring
var newFields = [{...}, {...}]; and calling theForm.addField(newFields);.
The codeToExecute value in each of the events can use the following code to inspect the following form
.
/* the this keyword points to the field widget */
var theFieldWidget = this;
/* each form widget has a field called containingForm which is the form*/
var theForm = theFieldWidget.containingForm;
/* the form has a property called formFields, which is an object, which maps between every field's shortName to the field widget itself*/
var theFieldsInTheFrom = theForm.formFields;
/* assuming that a field has a shortName 'theShortNameOfAnotherField', then the field itself can be extracted from the formFields object*/
var anotherField = theFieldsInTheFrom.theShortNameOfAnotherField;