Using dojo.data

What is dojo.data?

Dojo.data is a uniform data access layer that removes the concepts of database drivers and unique data formats. All data is represented as an item or as an attribute of an item. With such a representation, data can be accessed in a standard fashion. Out of the box, dojo.data provides a basic ItemFileReadStore for reading a particular format of JSON data. The DojoX project provides more stores (for example, a simple XmlStore, a CsvStore, and an OpmlStore) for working with servers that can output data in such a format. In addition, dojo.data is an API that other users can write to, so you can write one for a custom data format, a specific subset of all the dojo.data APIs, or any other sort of customized data handling service you want to work with. After you have your custom format accessible using a datastore, widgets that are aware of datastores, and other such code, can then access your data without having to learn new APIs specific to your data.

Ultimately, the goal of dojo.data is to provide a flexible set of APIs as interfaces that datastores can be written to conform to. Stores that conform to the standard interfaces should then be able to be used in a wide variety of applications, widgets, and so on interchangeably. In essence, the API hides the specific structure of the data, be it in JSON, XML, CSV, or some other data format, and provides one way to access items and attributes of items consistently. This also allows optimizations on data access to be placed where they are most appropriate -- the client or the server -- depending on the type, number, and structure the data sets being manipulated.

You can think of dojo.data as one layer above dojo.xhrGet(). Both operate asynchronously, and without refreshing the page. But, xhrGet will get almost any MIME type and return the data in a glob. It's your job to interpret it. With dojo.data, you call one set of APIs to access the data items and the attributes of the items, and it's up to the store to handle the interpretation of the native formats into a common access model.

dojo.data Terminology

dojo.data terminology is similar to relational database terminology. The following table compares and contrasts dojo.data terminology and relational database terminology:

dojo.data APIs
Term Equiv. Database Term Description
datastore cursor A JavaScript object that reads data from a data source and makes that data available as data items using the dojo.data APIs.
data source database The place that the raw data comes from. For example, in a CsvStore, the data source would be the .csv formatted file that the store loaded. In general, the data source could be a file, a database server, a Web service, or something else completely. They can be as simple as flat, table-like rows, or as complex as a full heirarchical database with nested details.
item row A data item that has attributes with attribute values.
attribute column One of the fields or properties of an item.
value - The contents of an attribute for a given item.
reference -- A value in an item that points to another item.
identity primary key An identifier that can be used to uniquely identify an item within the context of a single datastore.
query WHERE clause of SQL Select A specification or request that asks a datastore for some subset of the items it knows about. A query is often an object with a set of attribute/value pairs that define what attributes should be matched. It is possible, however, that the query could be a string or a number.

Note: It is highly recommended that all stores use an object structure of attribute name/value pairs as the query format for consistency between stores.

JDBC or ODBC The standard set of functions that datastore implements. The dojo.data.api module includes a set of APIs (such as Read and Write) and a datastore can implement one or more of the APIs.
internal data representation - The private data structures that a datastore uses to cache data in local memory (for example XML DOM nodes, anonymous JSON objects, or arrays of arrays).
request SQL Select The parameters used to limit and sort a set of items. This includes the query, sorting attributes, upper and lower limits, and callbacks.

dojo.data Design and APIs

Before diving directly into the APIs of dojo.data, the basic concepts behind the APIs need to be explored because some design descisions that were made might seem odd without an explanation as to why they were chosen. Therefore, read this page in its entirety before moving onto the individual APIs.

Concept 1: Data access is broken down into separate APIs that stores can choose to implement

Data access is broken down into separate APIs because not every service or data backend is able to provide complete access and functions. So not all datastores could possibly implement functions such as read, write, identify, or notifications. To make it simple to see what features a store provides, each store must provide the 'getFeatures()' function. This function reports which APIs the store implements. The following list of basic APIs are defined:

dojo.data.api.Read
The ability to read data items and attributes of those data items. This also includes the ability to search, sort, and filter data items.
dojo.data.api.Write
The ability to create, delete, and update data items and attributes of those data items. Not all back end services allow for modification of data items. In fact, most public services like Flikr, Delicious, GoogleMaps, for example are primarily read-based data providers.
dojo.data.api.Identity
The ability to locate and look up an item based on its unique identifier, if it has one. Not all data formats have unique identifiers that can be used to look up data items.
dojo.data.api.Notification
The ability to notify listeners for change events on data items in a store. The basic change events for an item are create, delete, and update. These are particularly useful for cases such as a datastore that periodically polls a back end service for data refresh.

Future Features

There are some further functions that the Dojo development community would like to define as additional features stores which might be implemented. However, they have not been completely specified yet and are a work in progress. As such, they are not currently provided in the Dojo Toolkit. Note that the list can change at any time as decisions evolve about what capabilities the dojo.data APIs should provide. The following features are functions that the Dojo development community would like to define as additional features stores to implement:
dojo.data.api.Schema
dojo.data.api.Attribution
Creation and modification of timestamps, author, and other functions of data items.
dojo.data.api.Versioning
Tracking and accessing old versions of data items.
dojo.data.api.Derivation
Attributes derived from other attributes and calculated values

Concept 2: Items and item attributes are always accessed, modified, created, and deleted through store functions. Attributes are never directly accessed from the item object.

This concept is likely one of the aspects of dojo.data that might seem confusing at first. The following code snippet shows this concept:

var store = new some.data.Store();
  var items;
   ... //Assume in this time items is now an array populated by a call to store.fetch();
  //To iterate over the items and print out the value of a 'foo' attribute, you would do the following:
  for (var i = 0; i < items.length; i++){
      var item = items[i];
      console.log("For attribute 'foo' value was: [" + store.getValue(item, "foo") + "]");
  }

This example might make you wonder why attributes are not accessed as shown in one of the following examples:

  •         var value = item["foo"]; 
    
  •        var value = item.foo;
    
  •        var value = item.getValue("foo");
    

Why is this a requirement of dojo.data?

The following list presents the reasons for this requirement:

  • Efficiency in accessing the values on the items: By requiring access to go through store functions, the store can hide the internal structure of the item. This allows the item to remain in a format that is most efficient for representing the datatype for a particular situation. For example, the items could be XML DOM elements and, in that case, the store would access the values using DOM APIs when store.getValue() is called.

    As a second example, the item might be a simple JavaScript structure and the store can then access the values through normal JavaScript accessor notation. From the end-users perspective, the access is exactly the same: store.getValue(item, "attribute"). This provides a consistent look and feel to accessing a variety of data types. This also provides efficiency in accessing items by reducing item load times by avoiding conversion to a defined internal format that all stores would have to use.

  • The store could use a very compact internal structure: This lessens the amount of memory required by a particular store to represent some item and its attribute values.
  • Going through store accessor function provides the possibility of lazy-loading in of values as well as lazy reference resolution.

The Read API

The most fundamental API of dojo.data is the Read API. All stores will implement this API because all stores need the ability to retrieve and process data items. The Read API is designed to be extremely flexible in how items are handled. The Read API provides the ability to:

  • Introspect any datastore to determine the APIs the datastore implements through the getFeatures() call.
  • Instrospect, On an item by item basis, what attributes each item has in a way that is agnostic to the data format.
  • Get values of attributes in a way that is agnostic to the data format.
  • Test attributes of items to see if they contain a specific value.
  • Test any JavaScript object to see if it is an item from the store.
  • Test to see if an item has been fully loaded from its source or if it is just the stub of an item that needs to be fully loaded.
  • Load stub items (lazy-loading).
  • Search for items that match a query.
  • Sort items in a search.
  • Page across items in a search.
  • Filter items by the query and wildcard matching.

The following examples, guidelines, and complete API documentation provide further information on the Read API. For more complete examples, review the Using Datastores section.

Example Usage

The following sections provide examples of the Read API in use, as described by each example heading:

Example 1: Listing the APIs supported by a datastore

var store = new some.Datastore();
var features = store.getFeatures();
for(var i in features){
    console.log("Store supports feature: " + i);
}

Example 2: Testing if an object is a store item

var store = new some.Datastore();
if(store.isItem(someObject)){
    console.log("Object was an item.");
}else{
    console.log("Object was NOT an item.");
}

Example 3: Listing the attributes of an item

var store = new some.Datastore();
...
//Assume that someItem is an item we got from a load.
var attributes = store.getAttributes(someItem);
for(var i = 0; i < attributes.length; i++){
    console.log("Item has attribute; " + attributes[i]);
}

Example 4: Testing an item for an attribute

var store = new some.Datastore();
...
//Assume that someItem is an item we got from a load.
if(store.hasAttribute(someItem, "foo")){
    console.log("item has attribute foo.");
}else{
    console.log("item DOES NOT have attribute foo.");
}

Example 5: Getting the label of an item

var store = new some.Datastore();
...
//Assume that someItem is an item we got from a load.
var label = store.getLabel(someItem);
console.log("item has label: " + label);

Example 6: Fetching all the items from the store

var store = new some.Datastore();
var gotItems = function(items, request){
    console.log("Number of items located: " + items.length);
};
store.fetch({onComplete: gotItems});

Further examples

Further examples of the API usage are covered in the Using Datastores section. Refer to it for examples on paging, sorting, selecting, and so forth.

The complete ReadAPI

The following ReadAPI was taken directly from dojo/data/api/Read.js:

getValue: function(    /* item */ item, 
                        /* attribute-name-string */ attribute, 
                        /* value? */ defaultValue)
        //    summary:
        //        Returns a single attribute value.
        //        Returns defaultValue if and only if *item* does not have a value for *attribute*.
        //        Returns null if and only if null was explicitly set as the attribute value.
        //        Returns undefined if and only if the item does not have a value for the given 
        //        attribute (which is the same as saying the item does not have the attribute). 
        // description:
        //        Saying that an "item x does not have a value for an attribute y"
        //        is identical to saying that an "item x does not have attribute y". 
        //        It is an oxymoron to say "that attribute is present but has no values" 
        //        or "the item has that attribute but does not have any attribute values".
        //        If store.hasAttribute(item, attribute) returns false, then
        //        store.getValue(item, attribute) will return undefined.
        //
        //    item:
        //        The item to access values on.
        //    attribute:
        //        The attribute to access represented as a string.
        //    defaultValue:
        //        Optional.  A default value to use for the getValue return in the attribute does not exist or has no value.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or *attribute* is not a string
        //    examples:
        //        var darthVader = store.getValue(lukeSkywalker, "father");

    getValues: function(/* item */ item,
                        /* attribute-name-string */ attribute)
        //    summary:
        //         This getValues() method works just like the getValue() method, but getValues()
        //        always returns an array rather than a single attribute value.  The array
        //        may be empty, may contain a single attribute value, or may contain many
        //        attribute values.
        //        If the item does not have a value for the given attribute, then getValues()
        //        will return an empty array: [].  (So, if store.hasAttribute(item, attribute)
        //        returns false, then store.getValues(item, attribute) will return [].)
        //
        //    item:
        //        The item to access values on.
        //    attribute:
        //        The attribute to access represented as a string.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or *attribute* is not a string

    getAttributes: function(/* item */ item)
        //    summary:
        //        Returns an array with all the attributes that this item has.  This
        //        method will always return an array; if the item has no attributes
        //        at all, getAttributes() will return an empty array: [].
        //
        //    item:
        //        The item to access attributes on.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or *attribute* is not a string

    hasAttribute: function(    /* item */ item,
                            /* attribute-name-string */ attribute)
        //    summary:
        //        Returns true if the given *item* has a value for the given *attribute*.
        //
        //    item:
        //        The item to access attributes on.
        //    attribute:
        //        The attribute to access represented as a string.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or *attribute* is not a string

    containsValue: function(/* item */ item,
                            /* attribute-name-string */ attribute, 
                            /* anything */ value)
        //    summary:
        //        Returns true if the given *value* is one of the values that getValues()
        //        would return.
        //
        //    item:
        //        The item to access values on.
        //    attribute:
        //        The attribute to access represented as a string.
        //    value:
        //        The value to match as a value for the attribute.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or *attribute* is not a string

    isItem: function(/* anything */ something)
        //    summary:
        //        Returns true if *something* is an item and came from the store instance.  
        //        Returns false if *something* is a literal, an item from another store instance, 
        //        or is any object other than an item.
        //
        //    something:
        //        Can be anything.
        //

    isItemLoaded: function(/* anything */ something) 
        //    summary:
        //        Returns false if isItem(something) is false.  Returns false if
        //        if isItem(something) is true but the the item is not yet loaded
        //        in local memory (for example, if the item has not yet been read
        //        from the server).
        //
        //    something:
        //        Can be anything.
        //
        
    loadItem: function(/* object */ keywordArgs)
        //    summary:
        //        Given an item, this method loads the item so that a subsequent call
        //        to store.isItemLoaded(item) will return true.  If a call to
        //        isItemLoaded() returns true before loadItem() is even called,
        //        then loadItem() need not do any work at all and will not even invoke
        //        the callback handlers.  So, before invoking this method, check that
        //        the item has not already been loaded.  
        //     keywordArgs:
        //        An anonymous object that defines the item to load and callbacks to invoke when the 
        //        load has completed.  The format of the object is as follows:
        //        {
        //            item: object,
        //            onItem: Function,
        //            onError: Function,
        //            scope: object
        //        }
        //    The *item* parameter.
        //        The item parameter is an object that represents the item in question that should be
        //        contained by the store.  This attribute is required.
        
        //    The *onItem* parameter.
        //        Function(item)
        //        The onItem parameter is the callback to invoke when the item has been loaded.  It takes only one
        //        parameter, the fully loaded item.
        //
        //    The *onError* parameter.
        //        Function(error)
        //        The onError parameter is the callback to invoke when the item load encountered an error.  It takes only one
        //        parameter, the error object
        //
        //    The *scope* parameter.
        //        If a scope object is provided, all of the callback functions (onItem, 
        //        onError, etc) will be invoked in the context of the scope object.
        //        In the body of the callback function, the value of the "this"
        //        keyword will be the scope object.   If no scope object is provided,
        //        the callback functions will be called in the context of dojo.global().
        //        For example, onItem.call(scope, item, request) vs. 
        //        onItem.call(dojo.global(), item, request)

    fetch: function(/* Object */ keywordArgs)
        //    summary:
        //        Given a query and set of defined options, such as a start and count of items to return,
        //        this method executes the query and makes the results available as data items.
        //        The format and expectations of stores is that they operate in a generally asynchronous 
        //        manner, therefore callbacks are always used to return items located by the fetch parameters.
        //
        //    description:
        //        A Request object will always be returned and is returned immediately.
        //        The basic request is nothing more than the keyword args passed to fetch and 
        //        an additional function attached, abort().  The returned request object may then be used 
        //        to cancel a fetch.  All data items returns are passed through the callbacks defined in the 
        //        fetch parameters and are not present on the 'request' object.
        //
        //        This does not mean that custom stores can not add methods and properties to the request object
        //        returned, only that the API does not require it.  For more info about the Request API, 
        //        see dojo.data.api.Request
        //
        //    keywordArgs:
        //        The keywordArgs parameter may either be an instance of 
        //        conforming to dojo.data.api.Request or may be a simple anonymous object
        //        that may contain any of the following:
        //        { 
        //            query: query-string or query-object,
        //            queryOptions: object,
        //            onBegin: Function,
        //            onItem: Function,
        //            onComplete: Function,
        //            onError: Function,
        //            scope: object,
        //            start: int
        //            count: int
        //            sort: array
        //        }
        //        All implementations should accept keywordArgs objects with any of
        //        the 9 standard properties: query, onBegin, onItem, onComplete, onError 
        //        scope, sort, start, and count.  Some implementations may accept additional 
        //        properties in the keywordArgs object as valid parameters, such as 
        //        {includeOutliers:true}.         
        //
	//	The *query* parameter.
	//        The query may be optional in some data store implementations.
	//        The dojo.data.api.Read API does not specify the syntax or semantics
	//        of the query itself -- each different data store implementation
	//        may have its own notion of what a query should look like.
	//        However, as of dojo 0.9, 1.0, and 1.1, all the provided datastores in dojo.data
	//        and dojox.data support an object structure query, where the object is a set of 
	//        name/value parameters such as { attrFoo: valueBar, attrFoo1: valueBar1}.  Most of the
	//        dijit widgets, such as ComboBox assume this to be the case when working with a datastore 
	//        when they dynamically update the query.  Therefore, for maximum compatibility with dijit 
	//        widgets the recommended query parameter is a key/value object.  That does not mean that the
	//        the datastore may not take alternative query forms, such as a simple string, a Date, a number, 
	//        or a mix of such.  Ultimately, The dojo.data.api.Read API is agnostic about what the query 
	//        format.  
	//        Further note:  In general for query objects that accept strings as attribute 
	//        value matches, the store should also support basic filtering capability, such as * 
	//        (match any character) and ? (match single character).  An example query that is a query object
	//        would be like: { attrFoo: "value*"}.  Which generally means match all items where they have 
	//        an attribute named attrFoo, with a value that starts with 'value'.
        //
        //    The *queryOptions* parameter
        //        The queryOptions parameter is an optional parameter used to specify optiosn that may modify
        //        the query in some fashion, such as doing a case insensitive search, or doing a deep search
        //        where all items in a hierarchical representation of data are scanned instead of just the root 
        //        items.  It currently defines two options that all datastores should attempt to honor if possible:
        //        {
        //            ignoreCase: boolean, //Whether or not the query should match case sensitively or not.  Default behaviour is false.
        //            deep: boolean     //Whether or not a fetch should do a deep search of items and all child 
        //                            //items instead of just root-level items in a datastore.  Default is false.
        //        }
        //
        //    The *onBegin* parameter.
        //        function(size, request);
        //        If an onBegin callback function is provided, the callback function
        //        will be called just once, before the first onItem callback is called.
        //        The onBegin callback function will be passed two arguments, the
        //        the total number of items identified and the Request object.  If the total number is
        //        unknown, then size will be -1.  Note that size is not necessarily the size of the 
        //        collection of items returned from the query, as the request may have specified to return only a 
        //        subset of the total set of items through the use of the start and count parameters.
        //
        //    The *onItem* parameter.
        //        function(item, request);
        //        If an onItem callback function is provided, the callback function
        //        will be called as each item in the result is received. The callback 
        //        function will be passed two arguments: the item itself, and the
        //        Request object.
        //
        //    The *onComplete* parameter.
        //        function(items, request);
        //
        //        If an onComplete callback function is provided, the callback function
        //        will be called just once, after the last onItem callback is called.
        //        Note that if the onItem callback is not present, then onComplete will be passed
        //        an array containing all items which matched the query and the request object.  
        //        If the onItem callback is present, then onComplete is called as: 
        //        onComplete(null, request).
        //
        //    The *onError* parameter.
        //        function(errorData, request); 
        //        If an onError callback function is provided, the callback function
        //        will be called if there is any sort of error while attempting to
        //        execute the query.
        //        The onError callback function will be passed two arguments:
        //        an Error object and the Request object.
        //
        //    The *scope* parameter.
        //        If a scope object is provided, all of the callback functions (onItem, 
        //        onComplete, onError, etc) will be invoked in the context of the scope
        //        object.  In the body of the callback function, the value of the "this"
        //        keyword will be the scope object.   If no scope object is provided,
        //        the callback functions will be called in the context of dojo.global().  
        //        For example, onItem.call(scope, item, request) vs. 
        //        onItem.call(dojo.global(), item, request)
        //
        //    The *start* parameter.
        //        If a start parameter is specified, this is a indication to the datastore to 
        //        only start returning items once the start number of items have been located and
        //        skipped.  When this parameter is paired withh 'count', the store should be able
        //        to page across queries with millions of hits by only returning subsets of the 
        //        hits for each query
        //
        //    The *count* parameter.
        //        If a count parameter is specified, this is a indication to the datastore to 
        //        only return up to that many items.  This allows a fetch call that may have 
        //        millions of item matches to be paired down to something reasonable.  
        //
        //    The *sort* parameter.
        //        If a sort parameter is specified, this is a indication to the datastore to 
        //        sort the items in some manner before returning the items.  The array is an array of 
        //        javascript objects that must conform to the following format to be applied to the
        //        fetching of items:
        //        {
        //            attribute: attribute || attribute-name-string,
        //            descending: true|false;   // Optional.  Default is false.
        //        }
        //        Note that when comparing attributes, if an item contains no value for the attribute
        //        (undefined), then it the default ascending sort logic should push it to the bottom 
        //        of the list.  In the descending order case, it such items should appear at the top of the list.
        // 
        //    returns:
        //        The fetch() method will return a javascript object conforming to the API
        //        defined in dojo.data.api.Request.  In general, it will be the keywordArgs
        //        object returned with the required functions in Request.js attached.
        //        Its general purpose is to provide a convenient way for a caller to abort an
        //        ongoing fetch.  
        // 
        //        The Request object may also have additional properties when it is returned
        //        such as request.store property, which is a pointer to the datastore object that 
        //        fetch() is a method of.
        //
        //    exceptions:
        //        Throws an exception if the query is not valid, or if the query
        //        is required but was not supplied.

    getFeatures: function()
        //    summary:
        //        The getFeatures() method returns an simple keyword values object 
        //        that specifies what interface features the datastore implements.  
        //        A simple CsvStore may be read-only, and the only feature it 
        //        implements will be the 'dojo.data.api.Read' interface, so the
        //        getFeatures() method will return an object like this one:
        //        {'dojo.data.api.Read': true}.
        //        A more sophisticated datastore might implement a variety of
        //        interface features, like 'dojo.data.api.Read', 'dojo.data.api.Write', 
        //        'dojo.data.api.Identity', and 'dojo.data.api.Attribution'.

    close: function(/*dojo.data.api.Request || keywordArgs || null */ request)
        //    summary:
        //        The close() method is intended for instructing the store to 'close' out 
        //        any information associated with a particular request.
        //
        //    description:
        //        The close() method is intended for instructing the store to 'close' out 
        //        any information associated with a particular request.  In general, this API
        //        expects to recieve as a parameter a request object returned from a fetch.  
        //        It will then close out anything associated with that request, such as 
        //        clearing any internal datastore caches and closing any 'open' connections.
        //        For some store implementations, this call may be a no-op.
        //
        //    request:
        //        An instance of a request for the store to use to identify what to close out.
        //        If no request is passed, then the store should clear all internal caches (if any)
        //        and close out all 'open' connections.  It does not render the store unusable from
        //        there on, it merely cleans out any current data and resets the store to initial 
        //        state.

    getLabel: function(/* item */ item)
        //    summary:
        //        Method to inspect the item and return a user-readable 'label' for the item
        //        that provides a general/adequate description of what the item is. 
        //
        //    description:
        //        Method to inspect the item and return a user-readable 'label' for the item
        //        that provides a general/adequate description of what the item is.  In general
        //        most labels will be a specific attribute value or collection of the attribute
        //        values that combine to label the item in some manner.  For example for an item
        //        that represents a person it may return the label as:  "firstname lastlame" where
        //        the firstname and lastname are attributes on the item.  If the store is unable 
        //        to determine an adequate human readable label, it should return undefined.  Users that wish
        //        to customize how a store instance labels items should replace the getLabel() function on 
        //        their instance of the store, or extend the store and replace the function in 
        //        the extension class.
        //
        //    item:
        //        The item to return the label for.
        //
        //    returns: 
        //        A user-readable string representing the item or undefined if no user-readable label can 
        //        be generated.

    getLabelAttributes: function(/* item */ item)
        //    summary:
        //        Method to inspect the item and return an array of what attributes of the item were used 
        //        to generate its label, if any.
        //
        //    description:
        //        Method to inspect the item and return an array of what attributes of the item were used 
        //        to generate its label, if any.  This function is to assist UI developers in knowing what
        //        attributes can be ignored out of the attributes an item has when displaying it, in cases
        //        where the UI is using the label as an overall identifer should they wish to hide 
        //        redundant information.
        //
        //    item:
        //        The item to return the list of label attributes for.
        //
        //    returns: 
        //        An array of attribute names that were used to generate the label, or null if public attributes 
        //        were not used to generate the label.

The Write API

Some datastores provide the ability to create new items and save those items back to a service, in addition to simply reading items from a service. Stores with this capability will implement the Write API, which provides standard functions for creating new items, modifing existing items, and deleting existing items. Review the following examples, guidelines, and complete API documentation for further information on the Write API.

Example Usage

The following sections provide examples of the Read API in use, as described by each example heading:

Example 1: Simple attribute modification and save

//Instantiate some write implementing store.
var store = some.DataWriteStore();

//Set our load completed hander up...
var onCompleteFetch = function(items, request) {
    //Define the save callbacks to use 
    var onSave = function(){
       alert("Save done.");
    }
    var onSaveError = function(error) {
       alert("Error occurred: " + error)
    }

    // Process the items and update attribute 'foo'
    for (var i = 0; i < items.length; i++){
       var item = items[i];
       store.setValue(item, "foo", ("bar" + 1));
    }
    
    // If the store has modified items (it should), call save with the handlers above.
    if (store.isDirty()){
       store.save({onComplete: onSave, onError: onSaveError});
    }
}

//Define a fetch error handler, just in case.
var onFetchError = function(error, request){
    alert("Fetch failed.  " + error);
}
// Fetch some data...  All items with a foo attribute, any value.
store.fetch({query: {foo:"*"}, onComplete: onCompleteFetch});

Example 2: Simple emit of all modified items (before a save has been called)

//Instantiate some write implementing store.
var store = some.DataWriteStore();
//Set our load completed hander up...
var onCompleteFetch = function(items, request) {
    // Process the items test for modification
    for (int i = 0; i < items.length(); i++){
       var item = items[i];
       if (store.isDirty(item){
          alert("Item with label: " + store.getLabel(item) + " is dirty.");
       }
    }
}

//Define a fetch error handler, just in case.
var onFetchError = function(error, request){
    alert("Fetch failed.  " + error);
}

// Fetch some data...  All items, in fact (no query should return everything)
store.fetch({onComplete: onCompleteFetch});

Write API requirements

The following list provides the requirements for the Write API:

  • Datastores that implement the Write interface act as a two-phase intermediary between the client and the ultimate provider or service that handles the data. This allows for the batching of operations, such as creating a set of new items and then saving them all back to the persistent store with one function call.
  • The save API is defined as asynchronous. This is because most datastores will be talking to a server and not all I/O methods for server communication can perform synchronous operations.
  • Datastores track all newItem, deleteItem, and setAttribute calls on items so that the store can both save the items to the persistent store in one chunk and have the ability to revert out all the current changes and return to a pristine (unmodified) data set.
  • Revert should only revert the store items on the client side back to the point the last save was called.
  • Datastores, in their Save function, account for any copying of items and generation of save format required by the back end service before it enters into the asynchronous I/O with the server. This is to avoid any contention issues with modifications that are occurring while the datastore is is waiting for the server I/O to complete.
  • The parameter to newItem is a keywordArgs object. For ease of interoperability, this parameter should be constructed as a JavaScrpt object with attribute names and values that match the conceptual structure of the attribute list the item would return. For example, if the source store is an XML backed store, a call to create a new XML Element in that store with attributes foo, bar, and bit, should look like this:
    store.newItem({foo: "fooValue", bar: "barValue", bit: "bitValue"});
    The store will then handle constructing the actual DOMElement with the appropriate DOM attributes.
  • Items returned from store.newItem() are valid items. In other words, store.isItem(item) returns true.
  • Items returned from store.newItem() are dirty items until the next save. In other words, store.isDirty(item) returns true.
  • Items deleted by store.deleteItem() are no longer valid items. In other words, store.isItem(item) returns false unless store.revert() is called and the delete is undone.

The complete Write API

The following Write API was taken directly from dojo/data/api/Write.js):

newItem: function(/* Object? */ keywordArgs, /*Object?*/ parentInfo){
        //   summary:
        //        Returns a newly created item.  Sets the attributes of the new
        //        item based on the *keywordArgs* provided.  In general, the attribute
        //        names in the keywords become the attributes in the new item and as for
        //        the attribute values in keywordArgs, they become the values of the attributes
        //        in the new item.  In addition, for stores that support hierarchical item 
        //        creation, an optional second parameter is accepted that defines what item is the parent
        //        of the new item and what attribute of that item should the new item be assigned to.
        //        In general, this will assume that the attribute targetted is multi-valued and a new item
        //        is appended onto the list of values for that attribute.  
        //        
        //    keywordArgs:
        //        A javascript object defining the initial content of the item as a set of JavaScript 'property name: value' pairs.
        //    parentInfo:
        //        An optional javascript object defining what item is the parent of this item (in a hierarchical store.  Not all stores do hierarchical items), 
        //        and what attribute of that parent to assign the new item to.  If this is present, and the attribute specified
        //        is a multi-valued attribute, it will append this item into the array of values for that attribute.  The structure
        //        of the object is as follows:
        //        {
        //            parent: someItem,
        //            attribute: "attribute-name-string"
        //        }
        //        
        //    exceptions:
        //        Throws an exception if *keywordArgs* is a string or a number or
        //        anything other than a simple anonymous object.  
        //        Throws an exception if the item in parentInfo is not an item from the store
        //        or if the attribute isn't an attribute name string.
        //    examples:
        //        var kermit = store.newItem({name: "Kermit", color:[blue, green]});

    deleteItem: function(/* item */ item)
        //    summary:
        //        Deletes an item from the store.
        //
        //    item: 
        //        The item to delete.
        //
        //    exceptions:
        //        Throws an exception if the argument *item* is not an item 
        //        (if store.isItem(item) returns false).
        //    examples:
        //        var success = store.deleteItem(kermit);

    setValue: function(    /* item */ item, 
                        /* string */ attribute,
                        /* almost anything */ value)
        //    summary:
        //        Sets the value of an attribute on an item.
        //        Replaces any previous value or values.
        //
        //    item:
        //        The item to modify.
        //    attribute:
        //        The attribute of the item to change represented as a string name.
        //    value:
        //        The value to assign to the item.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or if *attribute*
        //        is neither an attribute object or a string.
        //        Throws an exception if *value* is undefined.
        //    examples:
        //        var success = store.set(kermit, "color", "green");

    setValues: function(/* item */ item,
                        /* string */ attribute, 
                        /* array */ values)
        //    summary:
        //        Adds each value in the *values* array as a value of the given
        //        attribute on the given item.
        //        Replaces any previous value or values.
        //        Calling store.setValues(x, y, []) (with *values* as an empty array) has
        //        the same effect as calling store.unsetAttribute(x, y).
        //
        //    item:
        //        The item to modify.
        //    attribute:
        //        The attribute of the item to change represented as a string name.
        //    values:
        //        An array of values to assign to the attribute..
        //
        //    exceptions:
        //        Throws an exception if *values* is not an array, if *item* is not an
        //        item, or if *attribute* is neither an attribute object or a string.
        //    examples:
        //        var success = store.setValues(kermit, "color", ["green", "aqua"]);
        //        success = store.setValues(kermit, "color", []);
        //        if (success) {assert(!store.hasAttribute(kermit, "color"));}

    unsetAttribute: function(    /* item */ item, 
                                /* string */ attribute)
        //    summary:
        //        Deletes all the values of an attribute on an item.
        //
        //    item:
        //        The item to modify.
        //    attribute:
        //        The attribute of the item to unset represented as a string.
        //
        //    exceptions:
        //        Throws an exception if *item* is not an item, or if *attribute*
        //        is neither an attribute object or a string.
        //    examples:
        //        var success = store.unsetAttribute(kermit, "color");
        //        if (success) {assert(!store.hasAttribute(kermit, "color"));}

    save: function(/* object */ keywordArgs)
        //    summary:
        //        Saves to the server all the changes that have been made locally.
        //        The save operation may take some time and is generally performed
        //        in an asynchronous fashion.  The outcome of the save action is 
        //        is passed into the set of supported callbacks for the save.
        //   
        //    keywordArgs:
        //        {
        //            onComplete: function
        //            onError: function
        //            scope: object
        //        }
        //
        //    The *onComplete* parameter.
        //        function();
        //
        //        If an onComplete callback function is provided, the callback function
        //        will be called just once, after the save has completed.  No parameters
        //        are generally passed to the onComplete.
        //
        //    The *onError* parameter.
        //        function(errorData); 
        //
        //        If an onError callback function is provided, the callback function
        //        will be called if there is any sort of error while attempting to
        //        execute the save.  The onError function will be based one parameter, the
        //        error.
        //
        //    The *scope* parameter.
        //        If a scope object is provided, all of the callback function (
        //        onComplete, onError, etc) will be invoked in the context of the scope
        //        object.  In the body of the callback function, the value of the "this"
        //        keyword will be the scope object.   If no scope object is provided,
        //        the callback functions will be called in the context of dojo.global.  
        //        For example, onComplete.call(scope) vs. 
        //        onComplete.call(dojo.global)
        //
        //    returns:
        //        Nothing.  Since the saves are generally asynchronous, there is 
        //        no need to return anything.  All results are passed via callbacks.
        //    examples:
        //        store.save({onComplete: onSave});
        //        store.save({scope: fooObj, onComplete: onSave, onError: saveFailed});

    revert: function()
        //    summary:
        //        Discards any unsaved changes.
        //    description:
        //        Discards any unsaved changes.
        //
        //    examples:
        //        var success = store.revert();

    isDirty: function(/* item? */ item)
        //    summary:
        //        Given an item, isDirty() returns true if the item has been modified 
        //        since the last save().  If isDirty() is called with no *item* argument,  
        //        then this method returns true if any item has been modified since
        //        the last save().
        //
        //    item:
        //        The item to check.
        //
        //    exceptions:
        //        Throws an exception if isDirty() is passed an argument and the
        //        argument is not an item.
        //    examples:
        //        var trueOrFalse = store.isDirty(kermit); // true if kermit is dirty
        //        var trueOrFalse = store.isDirty();       // true if any item is dirty

The Identity API

The dojo.data.api.Identity interface defines the set of APIs that are implemented by a datastore if a data source provides a method by which to uniquely identify each item. This API then allows users of that datastore to request a specific item without searching for an item that matches specific attributes. Review the following examples, guidelines, and complete API documentation for futher information on the Identity API.

Example usage

For all of the examples in the following sections, assume that there is a simple ItemFileReadStore instantiation from the following data in the countries.json file:

{ identifier: 'abbr', 
  label: 'name',
  items: [
    { abbr:'ec', name:'Ecuador',           capital:'Quito' },
    { abbr:'eg', name:'Egypt',             capital:'Cairo' },
    { abbr:'sv', name:'El Salvador',       capital:'San Salvador' },
    { abbr:'gq', name:'Equatorial Guinea', capital:'Malabo' },
    { abbr:'er', name:'Eritrea',           capital:'Asmara' },
    { abbr:'ee', name:'Estonia',           capital:'Tallinn' },
    { abbr:'et', name:'Ethiopia',          capital:'Addis Ababa' }
]}

Example 1: Basic lookup of an item by identity

var itemStore = new dojo.data.ItemFileReadStore({url: countries.json});

function failed(error) {
         ... //Do something with the provided error.
}

function gotItem(item) {
    if (itemStore.isItem(item)){
        if(!(itemStore.getValue(item,"name") === "El Salvador")){{
           failed(new Error("The item loaded does not have the attribute value for attribute [name] expected."));
        }else{
           ... //Do something with it.
        }
    }else{
        //This should never occur.
        console.log("Unable to locate the item with identity [sv]");
    }
}
//Invoke the lookup.  This is an async call as it may have to call back to a server to get data.
itemStore.fetchItemByIdentity({identity: "sv" onItem: gotItem, onError: failed});

Example 2: Obtaining the value of an item's identity

var itemStore = new dojo.data.ItemFileReadStore({url: countries.json});
...
function onError(error, request){
    ... //Do something with the provided error.
}
function onComplete(items, request) {
    if(items.length === 1){
       var identifier = itemStore.getIdentity(items[0]);
       if(identifier !== null && identifier === "er"){
          ... //Do something with the located identity.
       }else{
          onError(new Error("The identifier returned does not match what was expected."), request);
       }
    }else{
       onError(new Error("Too many matches found."), request);
    }
}
//Search the store and find the item with the name Eritrea
itemStore.fetch({query: {name:"Eritrea"}, onComplete: onComplete, onError: onError});

Example 3: Obtaining the list of attributes that comprise the identity of an item

var itemStore = new dojo.data.ItemFileReadStore({url: countries.json});

function failed(error) {
         ... //Do something with the provided error.
}

function gotItem(item) {
    if (itemStore.isItem(item)){

        if(!(itemStore .getValue(item,"name") === "El Salvador")){{
           failed(new Error("The item loaded does not have the attribute value for attribute [name] expected."));
        }else{
           var identityAttributes = itemStore.getIdentityAttributes(item);
           if(identityAttributes !== null){
              for(var i = 0; i < identityAttributes.length; i++){
                  var identifier = identityAttributes[i];
                  ... //Do something with 'identifier'.
              }
           }else{
              failed(new Error("Unable to locate the list of attributes comprising the identity."));
           }
        }
    }else{
        //This should never occur.
        throw new Error("Unable to locate the item with identity [sv]");
        }
}
//Invoke the lookup.  This is an async call as it may have to call back to a server to get data.
itemStore.fetchItemByIdentity({identity: "sv" onItem: gotItem, onError: failed});

Futher Examples

To see more examples of the Identity API, refer to the test cases defined in the dojo/tests/data/ItemFileReadStore.js file of your dojo source tree.

Identity API Requirements

The following list provides the requirements for the Identity API:

  • The identifiers must always be an object that can be converted to a string using the toString() JavaScript API.

    Note: This does not keep identities from being compound keys; they just must be able to be represented in a string fashion.

  • Stores that implement the Identity API may expose the identity as a publicly accessible attribute on the item, or they may implement the identity as a private attribute.
  • Stores that expose the identity of the store as a public attribute (or set of attributes), must return the attribute(s) identifier(s) from the getIdentityAttributes() method. If they are not exposed as public attributes, then the getIdentityAttributes() method must return a null value.
  • All identifier values must be unique and address only one item within the store.
  • The store's getFeatures() function will return, as part of its associative map, a property with the key name of dojo.data.api.Identity. The value of the property can be anything reasonable, such as the boolean value true, the name of the attribute that represents the identity, an array of attributes, or even an object. By the mere presence of this key in the map, the store declares that it implements this API.

The Complete Identity API

The following Identity API was taken directly from dojo/data/api/Identity.js:

getIdentity: function(/* item */ item)
        //    summary:
        //        Returns a unique identifier for an item.  The return value will be
        //        either a string or something that has a toString() method.
        //    item:
        //        The item from the store from which to obtain its identifier.
        //    exceptions:
        //        Conforming implementations may throw an exception or return null if
        //        item is not an item.

    getIdentityAttributes: function(/* item */ item)
        //    summary:
        //        Returns an array of attribute names that are used to generate the identity. 
        //        For most stores, this is a single attribute, but for some complex stores
        //        such as RDB backed stores that use compound (multi-attribute) identifiers
        //        it can be more than one.  If the identity is not composed of attributes
        //        on the item, it will return null.  This function is intended to identify
        //        the attributes that comprise the identity so that so that during a render
        //        of all attributes, the UI can hide the the identity information if it 
        //        chooses.
        //    item:
        //        The item from the store from which to obtain the array of public attributes that 
        //        compose the identifier, if any.

    fetchItemByIdentity: function(/* object */ keywordArgs){
        //    summary:
        //        Given the identity of an item, this method returns the item that has 
        //        that identity through the onItem callback.  Conforming implementations 
        //        should return null if there is no item with the given identity.  
        //        Implementations of fetchItemByIdentity() may sometimes return an item 
        //        from a local cache and may sometimes fetch an item from a remote server, 
        //
        //    keywordArgs:
        //        An anonymous object that defines the item to locate and callbacks to invoke when the 
        //        item has been located and load has completed.  The format of the object is as follows:
        //        {
        //            identity: string|object,
        //            onItem: Function,
        //            onError: Function,
        //            scope: object
        //        }
        //    The *identity* parameter.
        //        The identity parameter is the identity of the item you wish to locate and load
        //        This attribute is required.  It should be a string or an object that toString() 
        //        can be called on.
        //        
        //    The *onItem* parameter.
        //        Function(item)
        //        The onItem parameter is the callback to invoke when the item has been loaded.  It takes only one
        //        parameter, the item located, or null if none found.
        //
        //    The *onError* parameter.
        //        Function(error)
        //        The onError parameter is the callback to invoke when the item load encountered an error.  It takes only one
        //        parameter, the error object
        //
        //    The *scope* parameter.
        //        If a scope object is provided, all of the callback functions (onItem, 
        //        onError, etc) will be invoked in the context of the scope object.
        //        In the body of the callback function, the value of the "this"
        //        keyword will be the scope object.   If no scope object is provided,
        //        the callback functions will be called in the context of dojo.global.
        //        For example, onItem.call(scope, item, request) vs. 
        //        onItem.call(dojo.global, item, request)

The Notification API

When working with data and items, sometimes it is useful to be notified when items are created, deleted, or modified within a given dojo.data datastore. The dojo.data.api.Notification feature is implemented by stores to expose such a capability. This set of functions defines monitoring for the main change events a store can see on an item: create, modify, and delete. Review the following examples, guidelines, and complete API documentation for further information on the Notification API.

Example Usage

There are two general patterns of listening on these functions for change events. The first pattern is to use the dojo.connect() event model to bind to the function on the store and have one of your functions called whenever the store calls the onSet, onNew, and onDelete functions. The second pattern is to replace the implementation of the notification functions on the store with custom logic to do something each time the store calls the function. Example usage of such functions are provided in the following examples.

Example 1: Basic dojo.connect to a DataStore onNew

var store = some.NotifyWriteStore();
var alertOnNew = function(item) {
    var label = store.getLabel(item);
    alert("New item was created: [" + label + "]");
};
dojo.connect(store, "onNew", alertOnNew);
//An alert should be thrown when this completes
var newItem = store.newItem({foo:"bar"});

Example 2: Replacing the onNew function of the store with a custom one

var store = some.NotifyWriteStore();
store.onNew = function(item) {
    var label = this.getLabel(item);
    alert("New item was created: [" + label + "]");
};
//An alert should be thrown when this completes
var newItem = store.newItem({foo:"bar"});

Further Information

Note that the previous examples can be applied to any of the store Notification APIs. Also note that, by doing either the connect or the function override on the store, you can implement a variety of item event handling. This includes performing actions only when desired items are effected, as needed for your particular notification scenario. As an example, you could look at a list of items your store has specifically kept track of as items of interest and only perform an operation when the item passed into the on*() function matches one in the list.

Notification API Requirements

As with all DataStores, not all stores will implement this API. For stores that implement this API, the following assumptions should be made:

  • All change events on items (create, set attribute and delete) will trigger a call to the appropriate store notification function.
  • Notifications occur at a store level and for all items. The Notifications API does not cover registering to listen for only particular items being modified. That does not mean it cannot be done in a custom store with an extended set of functions, only that such behavior is not defined by the specification. This is because, after careful analysis, it was determined by the dojo community that a per-item registration scheme could get extremely costly in terms of CPU time or object construction and was therefore avoided.
  • Most store implementations of Notification should implement these functions as simple no-op bind points for efficiency and performance. This also provides safety, should you want to replace the stub function with a more complex implementation or notification (or both) scheme.

The Complete Notification API

The following Notification API was taken directly from dojo/data/api/Notification.js:

onSet: function(/* item */ item, 
                    /* attribute-name-string */ attribute, 
                    /* object | array */ oldValue,
                    /* object | array */ newValue)
        //    summary:
        //        This function is called any time an item is modified via setValue, setValues, unsetAttribute, etc.  
        //    description:
        //        This function is called any time an item is modified via setValue, setValues, unsetAttribute, etc.  
        //        Its purpose is to provide a hook point for those who wish to monitor actions on items in the store 
        //        in a simple manner.  The general expected usage is to dojo.connect() to the store's 
        //        implementation and be called after the store function is called.
        //
        //    item:
        //        The item being modified.
        //    attribute:
        //        The attribute being changed represented as a string name.
        //    oldValue:
        //        The old value of the attribute.  In the case of single value calls, such as setValue, unsetAttribute, etc,
        //        this value will be generally be an atomic value of some sort (string, int, etc, object).  In the case of 
        //        multi-valued attributes, it will be an array.
        //    newValue:
        //        The new value of the attribute.  In the case of single value calls, such as setValue, this value will be 
        //        generally be an atomic value of some sort (string, int, etc, object).  In the case of multi-valued attributes, 
        //        it will be an array.  In the case of unsetAttribute, the new value will be 'undefined'.
        //
        //    returns:
        //        Nothing.

    onNew: function(/* item */ newItem, /*object?*/ parentInfo){
        //    summary:
        //        This function is called any time a new item is created in the store.
        //        It is called immediately after the store newItem processing has completed.
        //    description:
        //        This function is called any time a new item is created in the store.
        //        It is called immediately after the store newItem processing has completed.
        //
        //    newItem:
        //        The item created.
        //    parentInfo:
        //        An optional javascript object that is passed when the item created was placed in the store
        //        hierarchy as a value f another item's attribute, instead of a root level item.  Note that if this
        //        function is invoked with a value for parentInfo, then onSet is not invoked stating the attribute of
        //        the parent item was modified.  This is to avoid getting two notification  events occurring when a new item
        //        with a parent is created.  The structure passed in is as follows:
        //        {
        //            item: someItem,							//The parent item
        //            attribute:	"attribute-name-string",	//The attribute the new item was assigned to.
        //            oldValue: something	//Whatever was the previous value for the attribute.  
        //                                      //If it is a single-value attribute only, then this value will be a single value.
        //                                      //If it was a multi-valued attribute, then this will be an array of all the values minues the new one.
        //            newValue: something	//The new value of the attribute.  In the case of single value calls, such as setValue, this value will be
        //                                      //generally be an atomic value of some sort (string, int, etc, object).  In the case of multi-valued attributes,
        //                                      //it will be an array.  
        //        }
        //
        //    returns:
        //        Nothing.

    onDelete: function(/* item */ deletedItem)
        //    summary:
        //        This function is called any time an item is deleted from the store.
        //        It is called immediately after the store deleteItem processing has completed.
        //    description:
        //        This function is called any time an item is deleted from the store.
        //        It is called immediately after the store deleteItem processing has completed.
        //
        //    deletedItem:
        //        The item deleted.
        //
        //    returns:
        //        Nothing.

Using Datastores

This section contains documentation on the operations that are most common to data access:  Searching, Sorting, Pagination, and Lazy Loading.

A Simple Data Source

The easiest data store is a static one, so let's begin with that. The file in the following example has the /pantry_spices.json URL:

{ identifier: 'name',
   items: [
	{ name: 'Adobo', aisle: 'Mexican' },
	{ name: 'Balsamic vinegar', aisle: 'Condiments' },
	{ name: 'Basil', aisle: 'Spices' },
	{ name: 'Bay leaf', aisle: 'Spices' },
        { name: 'Beef Bouillon Granules', aisle: 'Soup' },
...
	{ name: 'Vinegar', aisle: 'Condiments' },
	{ name: 'White cooking wine', aisle: 'Condiments' },
        { name: 'Worcestershire Sauce', aisle: 'Condiments' }]}

In this example:

  • The data source is /pantry_spices.json.
  • Each item is an ingredient, and each item has two attributes, name and aisle.
  • The name attribute is an identifier. Each ingredient has a different name to prevent confusion.

This is a simple but useful technique. Because this data source is a separate file, you can share the data among many pages. But what can you do with it? The following simple sample displays a select list of pantry items:

The class for the PantryStore, dojo.data.ItemFileReadStore, tells dojo.data to expect the data in a specific format that uses JSON structure to store the data. Our static file is in pantry_items.json so this is the URL. The target could also be a dynamic, server-run script that returns the specific data in the defined JSON format. Other widgets, like Tree and Grid, also use the dojo.data objects and URL to load data into them.

Fetching Single Items and Values

For this example, we'll assume the following simple data source:

{ identifier: 'name', 
  items: [
	{ name: 'Adobo', aisle: 'Mexican' },
	{ name: 'Balsamic vinegar', aisle: 'Condiments' },
	{ name: 'Basil', aisle: 'Spices' },
	{ name: 'Bay leaf', aisle: 'Spices' },
        { name: 'Beef Bouillon Granules', aisle: 'Soup' },
// ...
	{ name: 'Vinegar', aisle: 'Condiments' },
	{ name: 'White cooking wine', aisle: 'Condiments' },
        { name: 'Worcestershire Sauce', aisle: 'Condiments' },
        { name: 'pepper', aisle: 'Spices' }
]}

Working with One Item

You might want to access items directly and work with one item at a time. Stores that implement the identity interface allow you to do this quite easily. In this example of accessing an item by its unique identifier, the following APIs are used:

Identity
fetchItemByIdentity() Fetches an item by its key value. Because the identity value of each item is unique, you are guaranteed at most one answer back.
Read
getValue() Takes an item and an attribute and returns the associated value

The following code fragment returns the aisle pepper is in:

var pantryStore = new dojo.data.ItemFileReadStore({
  url: "pantry_items.json" 
});


pantryStore.fetchItemByIdentity({
  identity: "pepper", 
  onItem: function(item){
    console.debug("Pepper is in aisle ", pantryStore.getValue(item,"aisle"));
  }
});

Note: In the previous example, the fetch is asynchronous. This is because many Datastores will need to go back to a server to actually look up the data and some I/O methods do not readily allow for a synchronous call.

Fetching Multiple Items and Values

For this example, we'll assume the simple data source detailed on the previous page

Working with Multiple Items

You will likely want to access multiple items from such a data source as in the preceding example. No problem! Dojo.data Read API provides a mechanism for loading a set of items. All you have to do is provide the following information to the fetch function of the Read API:

  • This is what I want (if I don't tell you something, get everything)
  • Do this if there is an error
  • Do that when everything is loaded

If this sounds like it might be event-driven, that's because it is. The prime method to call, dojo.data.api.Read.fetch(), is asynchronous. This is because, in a wide variety of data access cases, the datastore will have to make a request to a server service to get the data. Many I/O methods for doing server requests only behave in an asynchronous manner. Therefore, the primary search function of dojo.data is asynchronous by definition.

In this example, the Read API is used with the following values:

fetch()
Asynchronous API that fetches a set of items which match a list of attributes.
getValue()
Takes an item and an attribute and returns the associated value.

The following code fragment returns all items:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}
var pantryStore = new dojo.data.ItemFileReadStore({ url: "pantry_items.json" });
//Define a callback that fires when all the items are returned.
var gotList = function(items, request){
    var itemsList = "";
    dojo.forEach(items, function(i){
       itemsList += pantryStore.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
pantryStore.fetch({
    onComplete: gotList,
    onError: gotError
});

Working with Lots of Items

Now that we've looked at dealing with getting a list of items in one batch, what if the list is huge? It could take a long time to get all the items, push them into an array, and then call the callback with the array of items. Wouldn't it be nice if you could stream the items in, one at a time, and do something each time a new item is available? Well, with dojo.data, you can do that! There is an alternate callback you can pass to fetch() that is called on an item by item basis. It is the onItem callback.

In the following example, the code will request that all items be returned (an empty query). As each item gets returned, it will add a textnode to the document. In this example, the Read API is used with the following values:

fetch()
Asynchronous API that fetches a set of items which match a list of attributes.
getValue()
Takes an item and an attribute and returns the associated value.

The following code fragment loads all items and streams them back into the page:

/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #000066; font-weight: bold;} .geshifilter .kw2 {color: #003366; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .co1 {color: #009900; font-style: italic;} .geshifilter .coMULTI {color: #009900; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #3366CC;} .geshifilter .nu0 {color: #CC0000;} .geshifilter .me1 {color: #006600;} .geshifilter .re0 {color: #0066FF;}
var pantryStore = new dojo.data.ItemFileReadStore({url: "pantry_items.json" } );
var body = dojo.body(); // node to put output in
// Define the onComplete callback to write
// COMPLETED to the page when the fetch has
// finished returning items.
var done = function(items, request){
    body.appendChild(document.createTextNode("COMPLETED"));
}
//Define the callback that appends a textnode into the document each time an item is returned.
gotItem = function(item, request){
    body.appendChild(
        document.createTextNode(
            pantryStore.getValue(item, "name")
        )
    );
    body.appendChild(document.createElement("br"));
}
//Define a simple error handler.
var gotError = function(error, request){
    console.debug("The request to the store failed. " +  error);
}
//Invoke the search
pantryStore.fetch({
    onComplete: done,
    onItem: gotItem,
    onError: gotError
});

Note: If the onItem callback is present in the parameters to fetch, then the first parameter to the onComplete callback, the items array, will always be null. Therefore, onItem is streaming only mode and does not rely on onComplete for anything other than a signal that the streaming has ended.

Selecting Items

There are many times when you might not want an entire item list. Though you could fetch the entire list, and loop through to select elements, dojo.data's API definition has facilities to do the tough work for you.

Selecting subsets of items requires a query. A query is a JavaScript object that has attributes which look a lot like the attributes of the data store. It's a kind of query-by-example. So for our pantry, selecting the items from the Spice aisle involves this query:

{ aisle: "Spice" }

Each type of data store can have its own query syntax. With JsomItemStore, you can use wildcards: * to mean any characters. and ? to mean one character. This notation will be familiar to you if you've worked with Perl, Java, UNIX shell regular expressions, or even old BATCH scripts. And in general, the dojo.data community would highly recommend that all stores try to follow this method of specifying the query for consistency. Even datastores that are backed by an SQL database should be able to handle such character matching, because * maps to %, and ? maps to _ in SQL syntax.

The following query will pick up items in the Condiments aisle:

{ aisle: "Condiment*" }

Multiple attributes assume an "and" between the terms. So a query like the following one will match spices with the word pepper inside them, but not "green peppers" in the vegetable aisle:

{ name: "*pepper*", aisle: "Spices" }

Once we have constructed the query, we pass it to fetch() along with the other parameters as shown in the following example:

Finally, it's important to note that searches are case-sensitive by default. If you want to make a case-insensitive query, just add the ignoreCase option to the queryOptions object as shown in the following example:

itemStore .fetch({ 
        queryOptions: {ignoreCase: true},
        query: { name: "*pepper*", aisle: "Spices" },
        onComplete: 
                ...    
   });

This example will match both "Black Pepper" and "white pepper."

In general, any option that would affect the behavior of a query, such as making it case insensitive or doing a deep scan where it scans a hierarchy of items instead of just the top level items (the deep:true option), in a store belongs in the queryOptions argument.

Why isn't it just SQL for a query?

The simple and short answer to this question is that not all datastores are backed directly by a database that handles SQL. An immediate example is ItemFileReadStore, which just uses a structured JSON list for its data. Other examples would be datastores that wrap on top of services like Flickr and Delicious, because neither of those take SQL as the syntax for their services. Therefore, the dojo.data API defines basic guidelines and syntax stores that can be easily mapped to a service (for example, attribute names can map directly to parameters in a query string). The same is true for an SQL backed datastore. The attributes become substitutions in a prepared statement that the stores use (when they pass back the query to the server) and a simple common pattern matching syntax, the * and ?, which also map easily across a wide variety of datasource query syntax.

Nested Items and Lazy Loading

Items can be handled in chunks, as well as streamed in. The other articles about datastores show items as a flat list with no hierarchy. So, what if you want a datastore to represent hierarchical data? And how do you walk across the hierarchy? Walking the hierarchy is, in fact, quite easy to do. The following example shows how to do this using JsonItemStore.

Assume a datasource of:
  { identifier: 'name',
     label: 'name',
     items: [
       { name:'Africa', type:'continent',
           children:[{_reference:'Egypt'}, {_reference:'Kenya'}, {_reference:'Sudan'}] },
       { name:'Egypt', type:'country' },
       { name:'Kenya', type:'country',
           children:[{_reference:'Nairobi'}, {_reference:'Mombasa'}] },
       { name:'Nairobi', type:'city' },
       { name:'Mombasa', type:'city' },
       { name:'Sudan', type:'country',
           children:{_reference:'Khartoum'} },
       { name:'Khartoum', type:'city' },
       { name:'Asia', type:'continent',
           children:[{_reference:'China'}, {_reference:'India'}, {_reference:'Russia'},          {_reference:'Mongolia'}] },
       { name:'China', type:'country' },
       { name:'India', type:'country' },
       { name:'Russia', type:'country' },
       { name:'Mongolia', type:'country' },
       { name:'Australia', type:'continent', population:'21 million',
           children:{_reference:'Commonwealth of Australia'}},
       { name:'Commonwealth of Australia', type:'country', population:'21 million'},
       { name:'Europe', type:'continent',
           children:[{_reference:'Germany'}, {_reference:'France'}, {_reference:'Spain'}, {_reference:'Italy'}] },
       { name:'Germany', type:'country' },
       { name:'France', type:'country' },
       { name:'Spain', type:'country' },
       { name:'Italy', type:'country' },
       { name:'North America', type:'continent',
           children:[{_reference:'Mexico'}, {_reference:'Canada'}, {_reference:'United States of America'}] },
       { name:'Mexico', type:'country',  population:'108 million', area:'1,972,550 sq km',
           children:[{_reference:'Mexico City'}, {_reference:'Guadalajara'}] },
       { name:'Mexico City', type:'city', population:'19 million', timezone:'-6 UTC'},
       { name:'Guadalajara', type:'city', population:'4 million', timezone:'-6 UTC' },
       { name:'Canada', type:'country',  population:'33 million', area:'9,984,670 sq km',
           children:[{_reference:'Ottawa'}, {_reference:'Toronto'}] },
       { name:'Ottawa', type:'city', population:'0.9 million', timezone:'-5 UTC'},
       { name:'Toronto', type:'city', population:'2.5 million', timezone:'-5 UTC' },
       { name:'United States of America', type:'country' },
       { name:'South America', type:'continent',
           children:[{_reference:'Brazil'}, {_reference:'Argentina'}] },
       { name:'Brazil', type:'country', population:'186 million' },
       { name:'Argentina', type:'country', population:'40 million' }
  ]}

The above datasource for JsonItemStore uses references to other items to build the hierarchy. Other datasources and datastores might use different internal representations for hierarchy. But, in this example, notice that the continent type items have children that are countries, which in turn have children that are cities.

The following code snippet walks across this hierarchy and displays all country items contained by the continent items:

var store = new dojo.data.ItemFileReadStore({url: "countries.json"});

//Load completed function for walking across the attributes and child items of the
//located items.
var gotContinents = function(items, request){
    //Cycle over all the matches.
    for(var i = 0; i < items.length; i++){
        var item = items[i];

        //Cycle over all the attributes.
        var attributes = store.getAttributes(item);
        for (var j = 0; j < attributes.length; j++){
            //Assume all attributes are multi-valued and loop over the values ...
            var values = store.getValues(item, attributes[j]);
            for(var k = 0; k < values.length; k++){
                var value = values[k];
                
                if(store.isItem(value)){
                    //Test to see if the items data is fully loaded or needs to be demand-loaded in (the item in question is just a stub).
                    if(store.isItemLoaded(value)){
                        console.log("Located a child item with label: [" + store.getLabel(value) + "]");
                    }else{
                        //Asynchronously load in the child item using the stub data to get the real data.
                        var lazyLoadComplete = function(item){
                            console.log("Lazy-Load of item complete.  Located child item with label: [" + store.getLabel(item) + "]");
                        }
                        store.loadItem({item: value, onItem: lazyLoadComplete});
                    }
                }else{
                    console.log("Attribute: [" + attributes[j] + "] has value: [" + value + "]");
                }
            }           
        }
    }
}

//Call the fetch of the toplevel continent items.
store.fetch({query: {type: "continent"}, onComplete: gotContinents});

Note: The previous sample also demonstrates how lazy-loading (on-demand) of items can be done through the combination of the isItemLoaded() and loadItem() functions. For a demonstration of a lazy-loading approach that uses an extended version of ItemFileReadStore, see the demo in dojox/data/demos/demo_LazyLoad.html.

Pagination

As shown in the other datastore sections, the fetch method of the Read API can query across and return sets of items in a variety of ways. However, there is generally only so much space on a display to list all the data returned. Certainly, an application could implement its own custom display logic for just displaying subsets of the data, but that would be inefficient because the application would have had to load all the data in the first place. And, if the data set is huge, it could severely increase the memory usage of the browser. Therefore, dojo.data provides a mechanism by which the store itself can do the paging for you. When you use the paging options of fetch, all that is returned in the callbacks for fetch is the page of data you wanted, no more. This allows the application to deal with data in small chunks, the parts currently visible to you.

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 dojo.data 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.

To demonstrate the paging function, we'll assume an ItemFileReadStore with the following datasource:

{   
    identifier: 'name', 
    items: [
        { name: 'Adobo', aisle: 'Mexican' },
        { name: 'Balsamic vinegar', aisle: 'Condiments' },
        { name: 'Basil', aisle: 'Spices' },
        { name: 'Bay leaf', aisle: 'Spices' },
        { name: 'Beef Bouillon Granules', aisle: 'Soup' },
        ...
        { name: 'Vinegar', aisle: 'Condiments' },
        { name: 'White cooking wine', aisle: 'Condiments' },
        { name: 'Worcestershire Sauce', aisle: 'Condiments' }
        { name: 'pepper', aisle: 'Spices' }
    ]
}

The following code snippet would allow for paging over the items, 10 at a time:

var store = new dojo.data.ItemFileReadStore({url: "pantryStore.json" });

    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 'next' 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;
       }
       //Display the items in a table through the use of store.getValue() on the items and attributes desired.
       ...
    }

    //Do the initial request.  Without a query, it should just select all items.  The start and count limit the number returned.
    request = store.fetch({onComplete: itemsLoaded, start: 0, count: pageSize});

Note: The previous sample shows how the fetch() function returns a request object. According to the dojo.data specification, the request object contains the query parameters passed in plus an abort() function appended to it. In general, the abort function is intended for cases in which a request might take too much time to process or, in using the streaming callback of fetch(), a way to stop the streaming.

The request object also serves another purpose for datastores. It is a location where the store can cache hidden information about the request in process, such as a cache entry key for boosting performance through specifying exactly what internal cache might be in use for this particular query. Therefore, datastores can avoid calls to the server if possible. And, in the paging case, it becomes important to reuse the request object returned from fetch(). Also note that not all stores will append cache information to the request, but some might. Therefore, when in doubt, reuse the request object when paging.

Sorting

Items are, in general, returned in an indeterminate order. This isn't always what you want to happen; there are definite cases where sorting items based on specific attributes is important. Fortunately, you do not have to do the sorting yourself because dojo.data provides a mechanism to do it for you. The mechanism is just another option passed to fetch, the sort array.

The sort array will look something like the following example:

var sortKeys = [{attribute: "aisle", descending: true}];

Each sort key has an attribute, which should be an attribute in the data store item, and a descending boolean flag. If an attribute passed isn't an actual attribute of the item, it will generally be ignored by the sorting; it is treated as an undefined comparison.

For compound sorts, you can define as many sort keys as you want. The order in the array defines which keys take priority over other keys when sorting. The following example shows this:

var sortKeys = [
    {attribute: "aisle", descending: true},
    {attribute: "name", descending: false}
];

A Complete Example

For this example, we'll use the ItemFileReadStore data source:

{   identifier: 'name',
    items: [
        { name: 'Adobo', aisle: 'Mexican' },
        { name: 'Balsamic vinegar', aisle: 'Condiments' },
        { name: 'Basil', aisle: 'Spices' },
        { name: 'Bay leaf', aisle: 'Spices' },
        { name: 'Beef Bouillon Granules', aisle: 'Soup' },
        ...
        { name: 'Vinegar', aisle: 'Condiments' },
        { name: 'White cooking wine', aisle: 'Condiments' },
        { name: 'Worcestershire Sauce', aisle: 'Condiments' }
        { name: 'pepper', aisle: 'Spices' }
    ]
}

Now, assume you want to sort the items in the store by aisle first, then by name. The following code snippet would do this:

var store = new dojo.data.ItemFileReadStore({url: "pantryStore.json"});

var sortKeys = [
    {attribute: "aisle", descending: true},
    {attribute: "name", descending: false}
];

store.fetch({ 
       sort: sortKeys;
       onComplete: 
           ...
});
//When onComplete is called, the array of items passed into it 
//should be sorted according to the denoted sort array.

Available Stores

The following dojo.data stores are pre-packaged with Dojo:

dojo.data.ItemFileReadStore read-only store for JSON data
dojo.data.ItemFileWriteStore read/write store for JSON data
dojox.data.CsvStore read-only store for comma-separated variable (CSV) formatted data
dojox.data.OpmlStore read-only store for Outline Processor Markup Language (OPML)
dojox.data.HtmlTableStore read-only store for data kept in HTML-formatted tables
dojox.data.XmlStore read/write store for basic XML data
dojox.data.FlickrStore read store for queries on flickr.com, and a good example data store for web services
dojox.data.FlickrRestStore read store for queries on flickr.com, and a good example data store for web services. More advanced version of FlickrStore
dojox.data.QueryReadStore like ItemFileReadStore, read-only store for JSON data, but queries servers on each request
dojox.data.AtomReadStore read store for Atom XML documents.

dojo.data.ItemFileReadStore

Summary:
Dojo core provides a basic implementation of a read-only datastore, ItemFileReadStore. This store reads the JSON structured contents from an http endpoint (service or URL), or from an in-memory JavaScript object, and stores all the items in-memory for simple and quick access. ItemFileReadStore is designed to allow for flexibility in how it represents item hierarchy, references, and custom data types. It also provides options for which attribute can act as the unique identifier (for dojo.data.api.Identity), and which attribute can be used as a general label for an item. This store has an expectation that data is provided to in in a specific though very flexible, format. All of the examples on this page demonstrate the general format expected.

The following dojo.data APIs are implemented by ItemFileReadStore

  • dojo.data.api.Read
  • dojo.data.api.Identity

Format Examples: The following examples of data conform to the format the store requires for item input.


JSON with References: The following geography example uses references (items referencing another item declared in the data):

{ 'identifier': 'name',
  'label': 'name',
  'items': [
     { 'name':'Africa', 'type':'continent',
         'children':[{'_reference':'Egypt'}, {'_reference':'Kenya'}, {'_reference':'Sudan'}] },
     { 'name':'Egypt', 'type':'country' },
     { 'name':'Kenya', 'type':'country',
         'children':[{'_reference':'Nairobi'}, {'_reference':'Mombasa'}] },
     { 'name':'Nairobi', 'type':'city' },
     { 'name':'Mombasa', 'type':'city' },
     { 'name':'Sudan', 'type':'country',
         'children':{'_reference':'Khartoum'} },
     { 'name':'Khartoum', type:'city' },
     { 'name':'Asia', 'type':'continent',
         'children':[{'_reference':'China'}, {'_reference':'India'}, {'_reference':'Russia'}, {'_reference':'Mongolia'}] },
     { 'name':'China', 'type':'country' },
     { 'name':'India', 'type':'country' },
     { 'name':'Russia', 'type':'country' },
     { 'name':'Mongolia', 'type':'country' },
     { 'name':'Australia', 'type':'continent', 'population':'21 million',
         'children':{'_reference':'Commonwealth of Australia'}},
     { 'name':'Commonwealth of Australia', 'type':'country', 'population':'21 million'},
     { 'name':'Europe', 'type':'continent',
         'children':[{'_reference':'Germany'}, {'_reference':'France'}, {'_reference':'Spain'}, {'_reference':'Italy'}] },
     { 'name':'Germany', 'type':'country' },
     { 'name':'France', 'type':'country' },
     { 'name':'Spain', 'type':'country' },
     { 'name':'Italy', 'type':'country' },
     { 'name':'North America', 'type':'continent',
         'children':[{'_reference':'Mexico'}, {'_reference':'Canada'}, {'_reference':'United States of America'}] },
     { 'name':'Mexico', 'type':'country', 'population':'108 million', 'area':'1,972,550 sq km',
         'children':[{'_reference':'Mexico City'}, {'_reference':'Guadalajara'}] },
     { 'name':'Mexico City', 'type':'city', 'population':'19 million', 'timezone':'-6 UTC'},
     { 'name':'Guadalajara', 'type':'city', 'population':'4 million', 'timezone':'-6 UTC' },
     { 'name':'Canada', 'type':'country',  'population':'33 million', 'area':'9,984,670 sq km',
         'children':[{'_reference':'Ottawa'}, {'_reference':'Toronto'}] },
     { 'name':'Ottawa', 'type':'city', 'population':'0.9 million', 'timezone':'-5 UTC'},
     { 'name':'Toronto', 'type':'city', 'population':'2.5 million', 'timezone':'-5 UTC' },
     { 'name':'United States of America', 'type':'country' },
     { 'name':'South America', 'type':'continent',
         'children':[{'_reference':'Brazil'}, {'_reference':'Argentina'}] },
     { 'name':'Brazil', 'type':'country', 'population':'186 million' },
     { 'name':'Argentina', 'type':'country', 'population':'40 million' }
]}

JSON with Hierarchy: The following geography example uses hierarchical items (items that contain definitions of other items):

{ 'identifier': 'name',
  'items': [
    { 'name':'Africa', 'type':'continent', children:[
        { 'name':'Egypt', 'type':'country' }, 
        { 'name':'Kenya', 'type':'country', children:[
            { 'name':'Nairobi', 'type':'city' },
            { 'name':'Mombasa', 'type':'city' } ]
        },
        { 'name':'Sudan', 'type':'country', 'children':
            { 'name':'Khartoum', 'type':'city' } 
        } ]
    },
    { 'name':'Asia', 'type':'continent', 'children':[
        { 'name':'China', 'type':'country' },
        { 'name':'India', 'type':'country' },
        { 'name':'Russia', 'type':'country' },
        { 'name':'Mongolia', 'type':'country' } ]
    },
    { 'name':'Australia', 'type':'continent', 'population':'21 million', 'children':
        { 'name':'Commonwealth of Australia', 'type':'country', 'population':'21 million'}
    },
    { 'name':'Europe', 'type':'continent', 'children':[
        { 'name':'Germany', 'type':'country' },
        { 'name':'France', 'type':'country' },
        { 'name':'Spain', 'type':'country' },
        { 'name':'Italy', 'type':'country' } ]
    },
    { 'name':'North America', 'type':'continent', 'children':[
        { 'name':'Mexico', 'type':'country',  'population':'108 million', 'area':'1,972,550 sq km', 'children':[
            { 'name':'Mexico City', 'type':'city', 'population':'19 million', 'timezone':'-6 UTC'},
            { 'name':'Guadalajara', 'type':'city', 'population':'4 million', 'timezone':'-6 UTC' } ]
        },
        { 'name':'Canada', 'type':'country', 'population':'33 million', 'area':'9,984,670 sq km', 'children':[
            { 'name':'Ottawa', 'type':'city', 'population':'0.9 million', 'timezone':'-5 UTC'},
            { 'name':'Toronto', 'type':'city', 'population':'2.5 million', 'timezone':'-5 UTC' }]
        },
        { 'name':'United States of America', 'type':'country' } ]
    },
    { 'name':'South America', 'type':'continent', children:[
        { 'name':'Brazil', 'type':'country', 'population':'186 million' },
        { 'name':'Argentina', 'type':'country', 'population':'40 million' } ]
    } ]
}

Custom Data Types: The following example uses custom data types (items with custom data types):

{ 'identifier': 'abbr', 
  'label': 'name',
  'items': [
    { 'abbr':'ec', 'name':'Ecuador',           'capital':'Quito' },
    { 'abbr':'eg', 'name':'Egypt',             'capital':'Cairo' },
    { 'abbr':'sv', 'name':'El Salvador',       'capital':'San Salvador' },
    { 'abbr':'gq', 'name':'Equatorial Guinea', 'capital':'Malabo' },
    { 'abbr':'er',
      'name':'Eritrea',
      'capital':'Asmara',
      'independence':{'_type':'Date', '_value':"1993-05-24T00:00:00Z"} // May 24, 1993 in ISO-8601 standard  
      },
    { 'abbr':'ee',
      'name':'Estonia',
      'capital':'Tallinn',
      'independence':{'_type':'Date', '_value':"1991-08-20T00:00:00Z"} // August 20, 1991 in ISO-8601 standard
      },
    { 'abbr':'et',
      'name':'Ethiopia',
      'capital':'Addis Ababa' }
]}

Note: For custom data types, ItemFileStore looks for attributes that have an object format value and contains the following two specific attributes:

_type
Attribute used to look up in the typeMap which constructor should be used to instantiate the custom data type.
_value
Parameter that is the data to be passed to the constructor of the type. In this case, the custom type is a JavaScript Date object, and its value is the ISO-8601 string format of the date.

NOTE: The top level wrapper of the input for each of these examples is a JavaScript object. It has the following basic attributes that define the list of items that constitute the data and meta information about them:

identifier
Optional Metadata. The attribute in each item to act as the unique identifier for that item. This parameter is optional.
label
Optional Metadata. The attribute in each item to act as the human-readable label for that item. This parameter is optional.
items
An array of JavaScript objects that act as the items of the store. The attributes that are part of the items can be any valid JavaScript attribute name. Note that there is a special way for items to reference other items in the store when dealing with hierarchical data.

Constructor parameters

The constructor for ItemFileReadStore takes the following possible parameters in its keyword arguments:

url
The URL from which to load the JSON data. This is optional. If it isn't specified, then you should specify the 'data' parameter to identify the in-memory javascript object that constitutes the basis for the store content.
data
The JavaScript object that represents the stores contents, as defined by the structure displayed in the examples. This is optional.
typeMap
A JavaScript associative map of data types to deserialize them. This is optional. See the Custom Data Type Mapping for more details.

Custom Data Type Mappings

Custom data types are a way to specify how the store should interpret a value of an attribute on an 'item' when it is parsing and loading the store from the ItemFile format. The ItemFileReadStore has one built in custom data type; the 'Date' object. It, by default, maps attribute values of {_type: "Date" _value: "some ISO string"} to a new Date instance instead of treating that JS object as a child item with attributes of '_type' and '_value'. The ItemFileReadStore also allows users to define their own custom types so that they can control how the information in the ItemFile format is interpreted. There are a couple of ways to map custom data types. There is a simple mapping method and general purpose mapping method that are described in the following sections.

Simple mapping: Direct Constructor

The direct constructor approach is the simplest way to map a type. This example assumes that the value stored in _value can be used as the parameter to the constructor for an object:

var typeMap = {
                 "Color": dojo.Color,
                 ...
              };

General Purpose Mapping

General purpose mapping is intended for cases where you cannot directly pass the value of _value into a constructor. A good example of this is how Date is mapped internally in ItemFileReadStore. This is used because ItemFileReadStore reads in the _value for a date as a serialized ISO string, which is not a format the general JavaScript Date object understands.

The following example shows the Date serialization load from an ISOString format:

var typeMap = {
                  "Date": {
                             type: Date,
                             deserialize: function(value){
                                 return dojo.date.stamp.fromISOString(value);
                             }
                          }
              };

Query Syntax

The fetch method query syntax for ItemFileReadStore is simple and straightforward. It allows a list of attributes to match against in an AND fashion. For example, a query object that looks like the following example will locate all items that have attributes of those names that match both of those values:

{ foo:"bar", bit:"bite"}

Note that ItemFileReadStore supports the use of wild cards (multi-character * and single character ?) in its attribute value matching.

Examples

To find all items with attribute foo that start with bar, the query would be:

{ foo:"bar*"}

To find all items with attribute foo the value of which ends with ar and ignoring only the first character, the query would be:

{ foo:"?ar"}

NOTE: Other stores should follow the same semantics in defining queries for consistency.

Usage Examples

For these examples, we'll assume a data source as defined by the example data format in this page unless otherwise specified.

Example 1: Query for all continents

var store = new dojo.data.ItemFileReadStore({url: "geography.json"});
var gotContinents = function(items, request){
    for (var i = 0; i < items.length; i++){
       var item = items[i];
       console.log("Located continent: " + store.getLabel(item));
    }
}
var request = store.fetch({query: {type:"continent"}, onComplete: gotContinents});

Example 2: Query for names that start with A, case insensitively

var store = new dojo.data.ItemFileReadStore({url: "geography.json"});
var gotNames= function(items, request){
    for (var i = 0; i < items.length; i++){
       var item = items[i];
       console.log("Located name that started with A: " + store.getLabel(item));
    }
}
var request = store.fetch({query: {name:"A*"}, queryOptions: {ignoreCase: true}, onComplete: gotNames});

Example 3: Programmatic creation of ItemFileReadStore without a URL. Then query for names that start with J, case insensitively.

IMPORTANT NOTE: In this example the object 'dataItems' is modified and used directly by the store to generate the internal store representation of all the data. This is for efficiency of use and code size. If you want to repeatedly reuse the dataItems object you will need to clone the object and pass the clone into the store. If you do not clone the object and try to reuse dataItems, you will likely encounter errors due to the modifications the ItemFileReadStore applies to the data set so that it can perform store operations efficiently.
var dataItems = {
   identifier: 'name',
   label: 'name',
   items: [
      {name: 'John Smith'},
      {name: 'Bob Smith'},
      {name: 'Nancy Smith}',
      {name: 'John Doe'}
   ] 
};
var store = new dojo.data.ItemFileReadStore({data: dataItems});
var gotNames= function(items, request){
    for (var i = 0; i < items.length; i++){
       var item = items[i];
       console.log("Located name that started with J: " + store.getLabel(item));
    }
}
var request = store.fetch({query: {name:"J*"}, queryOptions: {ignoreCase: true}, onComplete: gotNames});

Further Examples

For further examples refer to the Using Datastores section of the Dojo book or refer to the test cases for dojo.data provided in the tests sub-directory of your dojo distribution.

dojo.data.ItemFileWriteStore

Dojo core provides the ItemFileWriteStore store as an extension to ItemFileReadStore that adds on the dojo.data.api.Write and dojo.data.api.Notification API support to ItemFileReadStore. It was specifically created as a separate class so that if you only need read capability, you do not incur the download penalty of the write and notification API support if you won't use it. If your application needs to write to the ItemFileStore instead of just Read, then ItemFileWriteStore is the store you should instantiate. The input data format is identical to ItemFileReadStore.

The following dojo.data APIs are implemented by ItemFileWriteStore

  • dojo.data.api.Read
  • dojo.data.api.Write
  • dojo.data.api.Identity
  • dojo.data.api.Notification

Constructor Parameters

The constructor for ItemFileWriteStore takes the same parameters as ItemFileReadStore these are the following possible parameters in its keyword arguments:

url
The URL from which to load the JSON data. This is optional.
data
The JavaScript object that represents the stores contents, as defined by the previous structure. This is optional.
typeMap
A JavaScript associative map of data types to deserialize them. This is optional. See the Custom Data Type Mapping for more details.

Custom Data Type Mappings

The custom type mapping for the ItemFileWriteStore follows the same conventions as the ItemFileReadStore. The only caveat is, that for general purpose mappings, you must also provide a serialize function for mapping so the data can be rendered back out appropriately. For simple mapping, object.toString() is sufficient.

Simple Mapping: Direct Constructor

The direct constructor approach is the simplest way to map a type. This one assumes that the value stored in _value can be used as the parameter to the constructor for an object. When serializing this back to the ItemFileFormat, this assumes that object.toString() is sufficient for the _value value as shown in the following example:

var typeMap = {
                 "Color": dojo.Color,
                 ...
              };

General Purpose Mapping

The general purpose mapping is intended for cases in which you cannot directly pass the value of _value into a constructor. A good example of this is how Date is mapped internally in the ItemFileReadStore. This is used because ItemFileReadStore reads in the _value for a date as a serialized ISO string, which is not a format the general JavasScript Date object understands.

The following example shows date serialization and deserialization mapping:

var typeMap = {
                  "Date": {
                             type: Date,
                             deserialize: function(value){
                                 return dojo.date.stamp.fromISOString(value);
                             },
                             serialize: function(object){
                                 return dojo.date.stamp.toISOString(object);
                             }
                          }
              };

Query Syntax

The query syntax for ItemFileWriteStore is identical to the query syntax of ItemFileReadStore so see that section for more information.

Usage Examples

For these examples, we'll assume a datasource as defined by the following example data:

{ identifier: 'abbr',
  label: 'name',
  items: [
    { abbr:'ec', name:'Ecuador',           capital:'Quito' },
    { abbr:'eg', name:'Egypt',             capital:'Cairo' },
    { abbr:'sv', name:'El Salvador',       capital:'San Salvador' },
    { abbr:'gq', name:'Equatorial Guinea', capital:'Malabo' },
    { abbr:'er', name:'Eritrea',           capital:'Asmara' },
    { abbr:'ee', name:'Estonia',           capital:'Tallinn' },
    { abbr:'et', name:'Ethiopia',          capital:'Addis Ababa' }
]}

Example 1: Add in a new country

var store = new dojo.data.ItemFileWriteStore({url: "countries.json"});
var usa = store.newItem({abbr: 'us', name: 'United States of America', capital: 'Washington DC'});

function saveDone(){
    alert("Done saving.");
}
function saveFailed(){
    alert("Save failed.");
}
store.save({onComplete: saveDone, onError: saveFailed});

Example 2: Delete a country

var store = new dojo.data.ItemFileWriteStore({url: "countries.json"}); function saveDone(){ alert("Done saving."); } function saveFailed(){ alert("Save failed."); } var gotNames= function(items, request){ for (var i = 0; i < items.length; i++){ console.log("Deleted country: " + store.getLabel(item); store.deleteItem(items[i]); } store.save({onComplete: saveDone, onError: saveFailed}); } var request = store.fetch({query: {name:"Egypt"}, queryOptions: {ignoreCase: true}, onComplete: gotNames});

Custom Saving

The save method by itself only updates the in-memory copy. To write the store back to the server, you need to override the extension point "_saveCustom". In markup language, it'd look something like:

You could also extend ItemFileWriteStore using Dojo's inheritance facilities:

dojo.declare("CustomItemFileWriteStore", ItemFileWriteStore, {
        _saveCustom: function(saveCompleteCallback, saveFailedCallback){
                //  xhrPost/xhrPut your data back to your server (convert it to the server