Using Dojo with the Zero Resource Model

The Zero Resource Model has a Dojo extension that simplifies interaction with RESTful data from JavaScript™ and Dojo widgets.

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.Grid widget 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
});
The same query syntax in the model API on the server that uses maps with Groovy is used in 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" />

The 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;

Version 1.1.28346