Core Implementations of Data APIs

The previous section provided a summary of the four primary data APIs available at this time. This section works through the two implementations provided by Core—the ItemFileReadStore and ItemFileWriteStore. As you'll see, the ItemFileReadStore implements the Read and Identity APIs, and the ItemFileWriteStore implements all four APIs discussed. A good understanding of these two stores equips you with enough familiarity to augment these existing stores to suit your own needs—or to implement your own.

Tip

Although not explicitly discussed in this book, the dojox.data subproject contains a powerful assortment of dojo.data implementations for common tasks such as interfacing to CSV stores, Flickr, XML, OPML, Picasa, and other handy stores. Since they all follow the same APIs as you're learning about here, picking them up should be a snap.

ItemFileReadStore

Although it is quite likely that your particular situation may benefit from a custom implementation of dojo.data.api.Read to maximize efficiency and impedance mismatching, the toolkit does include the ItemFileReadStore, which implements the Read and Identity interfaces and consumes a flexible JSON representation. For situations in which you need to quickly get something up and running, you need to do little more than have your application's business logic output data in the format that the ItemFileReadStore expects, and voilà, you may use the store as needed.

Tip

One thing to know up front is that the ItemFileReadStore consumes the entire data set that backs it into memory the first time a request is made; thus, operations like isItemLoaded and loadItem are fairly useless.

Hierarchical JSON and JSON with references

Although the ItemFileReadStore does implement the Read API, it packs a number of implementation-specific features of its own, including a specific data format, query syntax, a means of deserializing specific attribute values, specific identifiers for the identity of an item, and so on. Before getting into those specifics, however, have a look at some example data that is compliant for the ItemFileReadStore to consume; there are two basic flavors that relate to how nested data is represented: hierarchical JSON and JSON with references. The hierarchical JSON flavor consists of nested references that are concrete item instances, while the JSON with references flavor consists of data that points to actual data items.

To illustrate the difference between the two, first take a look at a short example of the two variations. First, for the hierarchical JSON:

{
    identifier : id,
    items : [
       {
            id : 1, name : "foo", children : [
                {id : 2, name : "bar"},
                {id : 3, name : "baz"}
            ]
        }
        /* more items... */
    ]
}

And now, for the JSON with references:

{
    identifier : id,
    items : [
        {
            id : 1, name : "foo", children : [
              {_reference: 2},
              {_reference: 3}
            ]
        },
        {id : 2, name : "bar"},
        {id : 3, name : "baz"}
        /* more items... */
    ]
k}

To recap, the foo item has two child items in both instances, but the hierarchical JSON explicitly nests the items, while the JSON with references uses pointers keying off of the identifier for the item. The primary advantage to JSON with references is its flexibility; it allows items to appear as the child of more than one parent, as well as the possibility for all items to appear as root-level items. Both possibilities are quite common and convenient for many real-world applications.

Tip

The Tree Dijit, introduced in Chapter 15, is a great example that highlights the flexibility and power (as well as some of the shortcomings) of the JSON with references data format.

ItemFileReadStore walkthrough

To get up close and personal with the ItemFileReadStore, consider the data collection represented as hierarchical JSON, shown in Example 9-1, where each item is identified by the name identifier. Note that the identifier, label, and items keys are the only expected values for the outermost level of the store.

Example 9-1. Sample coffee data set

{
    identifier : "name",
    label : "name",

    items : [
        {name : "Light Cinnamon", description : "Very light brown, dry , tastes
like toasted grain with distinct sour tones, baked, bready"},
        {name : "Cinnamon", description : "Light brown and dry, still toasted
grain with distinct sour acidy tones"},
        {name : "New England", description : "Moderate light brown , still sour
but not bready, the norm for cheap Eastern U.S. coffee"},
        {name : "American or Light", description : "Medium light brown, the
traditional norm for the Eastern U.S ."},
        {name : "City, or Medium", description : "Medium brown, the norm for
most of the Western US, good to taste varietal character of a bean."},
        {name : "Full City", description : "Medium dark brown may have some
slight oily drops, good for varietal character with a little bittersweet."},
        {name : "Light French", description : "Moderate dark brown with oily
drops, light surface oil, more bittersweet, caramelly flavor, acidity muted."},
        {name : "French", description : "Dark brown oily, shiny with oil,
also popular for espresso; burned undertones, acidity diminished"},
        {name : "Dark French", description : "Very dark brown very shiny, burned
tones become more distinct, acidity almost gone."},
        {name : "Spanish", description : "Very dark brown, nearly black and
very shiny, charcoal tones dominate, flat."}
    ]
}

Assuming the file was stored on disk as coffee.json, the page shown in Example 9-2 would load the store and make it available via the coffeeStore global JavaScript variable.

Example 9-2. Programmatically loading an ItemFileReadStore

<html>
    <head>
        <title>Fun with ItemFileReadStore!</title>
        <script
            type="text/javascript"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>
        <script type="text/javascript">
            dojo.require("dojo.data.ItemFileReadStore");

            dojo.addOnLoad(function(  ) {
                coffeeStore = new dojo.data.ItemFileReadStore({url:"coffee.json"});
            });
        </script>
    </head>
    <body>
    </body>
</html>

Although the parser isn't formally introduced until Chapter 11, using the parser is so common that it's worthwhile to explicitly mention that the markup variation in Example 9-3 would have achieved the very same effect.

Example 9-3. Loading an ItemFileReadStore in markup

<html>
    <head>
        <title>Fun with ItemFileReadStore!</title>
        <script
            type="text/javascript"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js"
            djConfig="parseOnLoad:true">
        </script>
        <script type="text/javascript">
            dojo.require("dojo.parser");
            dojo.require("dojo.data.ItemFileReadStore");
        </script>
    </head>
    <body>
        <div dojoType="dojo.data.ItemFileReadStore" url="./coffee.json"
          jsId="coffeeStore"></div>
    </body>
</html>

Regardless of how you declare the store, the API works the same either way. A great exercise is to spend a few minutes in the Firebug console with the existing store. The remainder of this section contains a series of commands and code snippets with the corresponding response values for most of the Read and Identity APIs that you can follow along with and use to accelerate your learning about the ItemFileReadStore.

Tip

In addition to using the url parameter to point an ItemFileReadStore at a data set represented as a file, you could also have passed it a variable referencing a JavaScript object that's already in memory via the data parameter.

Fetching an item by identity

Fetching an item from the ItemFileReadStore is generally done in one of two ways, though each way is quite similar. To fetch an item by its identifier, you should use the Identity API's fetchItemByIdentity function, which accepts a collection of named arguments including the identifier, what to do when the item is fetched, and what to do if an error occurs. For example, to query the sample coffeeStore for the Spanish coffee, you could do something like Example 9-4.

Example 9-4. Fetching an item by its identity and then inspecting it

coffeeStore.fetchItemByIdentity({
    identity: "Spanish",
    onItem : function(item, request) {
        var spanishCoffeeItem = item;

        //now do something with the results of the fetch request

        //like get its description...
        coffeeStore.getValue(spanishCoffeeItem, "description"); //Very dark brown...

        //or get its name...
        coffeeStore.getValue(spanishCoffeeItem, "name"); // Spanish

        //in this case, the name and label are the same...
        coffeeStore.getLabel(spanishCoffeeItem); // Spanish

    },
    onComplete(items, request) {
        /* You could access the entire result of a fetch request here,
           which in this case would only be an array of one item
           since a fetch by identity guarantees only one item back */
    },
    onError : function(item, request) {
        /* Handle any error here... */
    }
});

Warning

A common mistake when you're just starting out is to accidentally confuse the identity of an item with the item itself, which can be a tricky semantic bug to find because the code "looks right." Finding the Spanish coffee item via var item = coffeeStore.fetchItemByIdentity("Spanish") reads as though it makes sense, but when you take a closer look at the API, you realize that it's wrong in at least two ways: the call doesn't return an item back to you, and you have to provide a collection of named arguments to it—not an identity value.

Fetching an item by arbitrary criteria

If you want to fetch an item by an attribute other than the identity, you could use the more generic fetch function instead of fetchItemByIdentity, like so:

coffeeStore.fetch({
    query: {name : "Spanish"},
    onItem : function(item, request){console.log(item);}
});

However, in addition to accepting fully qualified values for attributes, the fetch function also accepts a small but robust collection of filter criteria that allows for basic regex-style matching. For example, to find any coffee description with the word "dark" in it without regard to case-sensitivity, you follow the process illustrated in Example 9-5.

Example 9-5. Fetching an item by arbitrary criteria

coffeeStore.fetch({
    query: {description : "*dark*"},
    queryOptions:{ignoreCase : true},
    onItem : function(item, request) {
        console.log(coffeeStore.getValue(item, "name"));
    }
    /* include other fetch callbacks here... */
});

Warning

Always use the store to access item attributes via getValue. Don't try to access them directly because the underlying implementation of the store may not allow it. For example, you would not want to access an item in the onItem callback as onItem: function(item, request) { console.log(item.name); }. A tremendous benefit from this abstraction is that it gives way to underlying caching mechanisms and other optimizations that improve the efficiency of the store.

If you're designing your own custom implementation of a store, you may find it helpful to know that dojo.data.util.filter is a short mix-in that can give you the same functionality as the regex-style matching that ItemFileReadStore uses for fetch, and dojo.data.util.simpleFetch provides the logic for its eight arguments: onBegin, onItem, onComplete, onError, start, count, sort, and scope.

Querying child items

The existing coffee store is quite primitive in that it is a flat list of items. Example 9-6 spices it up a bit by adding in a few additional items that contain children to produce a nested structure. The ItemFileReadStore expressly uses the children attribute to maintain a list of child items, and we'll use the JSON with references approach to accommodate the task of grouping coffees into different roasts. Note that the Light French roast has been deliberately placed into the Medium Roasts and the Dark Roasts to illustrate the utility of using references. Because each item needs to maintain a unique identity, it wouldn't be possible to include it as a child of two different parents any other way.

Tip

Although the remainder of this chapter uses a store that consists of only two levels, there is no reason why you couldn't use a data set with any arbitrary number of levels in it.

Example 9-6. Updated sample coffee data set to reflect hierarchy

{
    identifier : "name",
    items : [
        {
            name : "Light Roasts",
            description : "A number of delicious light roasts",
            children : [
                {_reference: "Light Cinnamon"},
                {_reference: "Cinnamon"},
                {_reference: "New England"}
            ]
        },

        {
            name : "Medium Roasts",
            description : "A number of delicious medium roasts",
            children : [
                {_reference: "American or Light"},
                {_reference: "City, or Medium"},
                {_reference: "Full City"},
                {_reference: "Light French"}
            ]
        },

        {
            name : "Dark Roasts",
            description : "A number of delicious dark roasts",
            children : [
                {_reference: "Light French"},
                {_reference: "French"},
                {_reference: "Dark French"},
                {_reference: "Spanish"}
            ]
        },

        {name : "Light Cinnamon", description : "Very light brown, dry , tastes
like toasted grain with distinct sour tones, baked, bready"},
        ...
    ]
}

A common task you might find yourself needing to accomplish is querying the children of an item. In this case, that amounts to finding the individual names associated with any given roast. Let's try it out in Example 9-7 for the Dark Roasts item to illustrate.

Example 9-7. Fetching an item and iterating over its children

coffeeStore.fetch({
    query: {name : "Dark Roasts"},
    onItem : function(item, request) {
        dojo.forEach(coffeeStore.getValues(item, "children"), function(childItem) {
          console.log(coffeeStore.getValue(childItem, "name"));
        });
    }
});

To recap, we issue a straightforward query for the parent item Dark Roasts, and then once we have the item, we use the getValues function to retrieve the multivalued children attribute and iterate over each with dojo.forEach —all the while remembering to use the getValue function to ultimately access the child item's value.

Note that the whole notion of {_reference: someIdentifier} is simply an implementation detail. There is never a time when you'll want to attempt to query based on the _reference attribute because there really isn't any such thing as a _reference attribute—again, it's just a standardized way of managing the bookkeeping. As far as the dojo.data application programmer is concerned, everything in a dojo.data store should be considered a good old item.

As you hopefully have observed by now, ItemFileReadStore is quite flexible and powerful, which makes it a suitable data format for a variety of situations—especially when you have to prototype an application and get something up and running quickly. As a simple specification, it's not difficult to have a server-side routine spit out data that a web client using dojo.data can digest. At the same time, however, remember that you can always subclass and extend as you see fit—or implement your own.

ItemFileWriteStore

There's no doubt that good abstraction eliminates a lot of cruft when it comes time to serve up data from the server and display it; however, it is quite often the case that you won't have the luxury of not writing data back to the server if it changes—and that's where the ItemFileWriteStore comes in. Just as the ItemFileReadStore provided a nice abstraction for reading a data store, ItemFileWriteStore provides the same kind of abstraction for managing write operations such as creating new items, deleting items, and modifying items. In terms of the dojo.data APIs, the ItemFileWriteStore implements them all—Read, Identity, Write, and Notification.

To get familiar with the ItemFileWriteStore, we'll work through the specifics in much the same way that we did for the ItemFileReadStore using the same coffee.json JSON data. As you'll see, there aren't any real surprises; the API pretty much speaks for itself.

Modifying an existing item

You'll frequently use the setValue function, shown in Example 9-8, to change the value of item's attribute by passing in the item, the attribute you'd like to modify, and the new value for the attribute. If the item doesn't have the named attribute, it will automatically be added.

Example 9-8. Setting an item's attribute

//Fetch an item like usual...
coffeeStore.fetchItemByIdentity({
    identity: "Spanish",
    onItem : function(item, request) {
        var spanishCoffeeItem = item;;

        coffeeStore.setValue(spanishCoffeeItem, "foo", "bar");

        coffeeStore.getValue(spanishCoffeeItem, "foo"); //bar

        //Likewise, you could have changed any other attribute except for the identity
        coffeeStore.setValue(spanishCoffeeItem, "description", "El Matador...?!?");
    }
});

Warning

Just like in most other data schemes, it doesn't usually make sense to change an item's identity, as the notion of identity is an immutable characteristic; following suit, the ItemFileWriteStore does not support this operation, nor is it recommended in any custom implementation of your own.

One peculiarity to note is that setting an attribute to be an empty string is not the same thing as removing the attribute altogether; this is especially important to internalize if you find yourself needing to use the Write API's hasAttribute function to check for the existence of an attribute. Example 9-9 illustrates the differences.

Example 9-9. Setting and unsetting attributes on items

coffeeStore.hasAttribute(spanishCoffeeItem, "foo"); //true

coffeeStore.setValue(spanishCoffeeItem, "foo", ""); //foo=""

coffeeStore.hasAttribute(spanishCoffeeItem, "foo"); //true

coffeeStore.unsetAttribute(spanishCoffeeItem, "foo"); //remove it

coffeeStore.hasAttribute(spanishCoffeeItem, "foo"); //false

While the previous examples in this section have demonstrated how to successfully modify an existing item, the changes so far have been incomplete in that an explicit save operation has not occurred. Internally, the ItemFileReadStore keeps track of changes and maintains a collection of dirty items—items that have been modified, but not yet saved. For example, after having modified the spanishCoffeeItem, you could use the isDirty function to learn that it has been modified but not saved, as shown in Example 9-10. After the item is saved, however, it is no longer dirty. For now, saving means nothing more than updating the in memory copy; we'll talk about saving back to the server in just a bit.

Example 9-10. Inspecting an item for dirty status

/* Having first modified the spanishCoffeeItem... */
coffeeStore.isDirty(spanishCoffeeItem); //true
coffeeStore.save(  ); //update in-memory copy of the store
coffeeStore.isDirty(spanishCoffeeItem); //false

Although it might not be immediately obvious, an advantage of requiring an explicit save operation to commit the changes lends the ability to revert the changes in case a later operation that is part of the same macro-level transaction produces an error or any other deal-breaking circumstance occurs. In relational databases, this is often referred to as a rollback. Example 9-11 illustrates reverting a dojo.data store and highlights a very subtle yet quite important point related to local variables that contain item references.

Example 9-11. Reverting changes to an ItemFileWriteStore

coffeeStore.fetchItemByIdentity({
    identity: "Spanish",
    onItem : function(item, request) {
        var spanishCoffeeItem = item;

        coffeeStore.getValue(spanishCoffeeItem, "description"); //Very dark...

        coffeeStore.setValue(spanishCoffeeItem, "description", "El Matador...?!?");

        //Right now, both the spanishCoffeeItem and the store reflect the
        //Udpated description. Let's do another fetch to verify... 

        coffeeStore.fetchItemByIdentity({
            identity: "Spanish",
            onItem : function(item, request) {
                coffeeStore.getValue(item, "description"); //El Matador...?!?
                coffeeStore.isDirty(item); //true

                coffeeStore.revert(  ); //revert the store.

                // Upon revert(  ), the local spanishCoffeeItem variable
                // ceased to be an item in the store
                coffeeStore.isItem(spanishCoffeeItem); //false

                //Fetch out the item again to demonstrate...

                coffeeStore.fetchItemByIdentity({
                    identity: "Spanish",
                    onItem : function(item, request) {
                        coffeeStore.isDirty(item); //false
                        coffeeStore.getValue(item, "description"); //Very dark...
                    }
                });

            }
        });

    }
});

Warning

Although it's theoretically possible to implement a custom store that prevents local item references from becoming stale via slick engineering behind the scenes with dojo.connect or pub/sub communication, the ItemFileWriteStore does not go to such lengths, and you should use the isItem function liberally if you are concerned about whether an item reference has become stale.

Creating and deleting items

Once you have a good grasp on the previous section that worked through the various nuances of modifying existing items, you'll have no problem picking up how to add and delete items from a store. All of the same principles apply with respect to saving and reverting—there's really not much to it. First, as shown in Example 9-12, let's add and delete a top-level item from our existing store. Adding an item involves providing a JSON object just like the server would have included in the original data set.

Example 9-12. Adding and deleting an item from an ItemFileWriteStore

var newItem = coffeeStore.newItem({
    name : "Really Dark",
    description : "Left brewing in the pot all day...extra octane."
});

coffeeStore.isItem(newItem); //true
coffeeStore.isDirty(newItem); //true

/* Query the item, save the store, revert the store, etc. */

//Or delete the item...
coffeeStore.deleteItem(newItem);
coffeeStore.isItem(newItem); //false

While adding and removing top-level items from a store is trivial, there is just a little bit more effort involved in adding a top-level item that also needs to a be a child item that is referenced elsewhere. Example 9-13 illustrates how it's done. The basic recipe is that you create it as a top-level item, get the children that you want it to join, and then add it to that same collection of children.

Example 9-13. Adding a child item to a JSON with references store

//Get a reference to the parent with the children
coffeeStore.fetchItemByIdentity({
    identity : "Dark Roasts",
    onItem : function(item, request) {
        var darkRoasts = item;

        //Use getValues to grab the children
        var darkRoastChildren = coffeeStore.getValues(darkRoasts, "children");

        
        //And add it to the children usingsetValues 
        coffeeStore.setValues(darkRoasts, "children", 
            darkRoastChildren.concat(newItem)
        )    
       
        //You could now iterate over those children to see for yourself...
        dojo.forEach(darkRoastChildren, function(x) {
            console.log(coffeeStore.getValue(x, "name"));
        });
    }
});

Warning

Remember to use getValues, not getValue, when fetching multivalued attributes.

Deleting items works in much the way you would expect. Deleting a top-level item removes it from the store but leaves its children, if any, in place, as shown in Example 9-14.

Example 9-14. Deleting a top-level item from an ItemFileWriteStore

coffeeStore.fetchItemByIdentity({
    identity : "Dark Roasts",
    onItem : function(item, request) {
        var darkRoasts = item;

        coffeeStore.deleteItem(darkRoasts);

        coffeeStore.fetch({
            query : {name : "*"},
            onItem : function(item, request) {
                //You won't see the "Dark Roasts" item in these results...
                console.log(coffeeStore.getValue(item, "name"));
            },
            onComplete : function(items, request) {
                /* Save the store, or revert the store, or... */
            }
        });
    }
});

Clearly, you could eliminate a top-level item and all of its children by first querying for the children, deleting them, and then deleting the top-level item itself.

Custom saves

You've probably been thinking for a while that saving in memory is great and all—but what about getting data back on the server? As it turns out, the ItemFileWrite store provides a _saveCustom extension point that you can implement to trigger a custom routine that fires anytime you call save ; thus, in addition to updating the local copy in memory and clearing any dirty flags, you can also sync up to the server—or otherwise do anything else that you'd like. You have the very same API available to you that you've been using all along, but in general, a "full save" would probably consist of iterating over the entire data set, serializing into a custom format—quite likely with the help of dojo.toJson —and shooting it off. Just as the Write API states, you provide keyword arguments consisting of optional callbacks, onComplete and onError, which are fired when success or an error occurs. An optional scope argument can be provided that supplies the execution context for either of those callbacks. Those keyword arguments, however, are passed into the save function—not to your _saveCustom extension.

Example 9-15 shows how to implement a _saveCustom handler to pass data back to the server when save() is called. As you'll see, it's pretty predictable.

Example 9-15. Wiring up a custom save handler for an ItemFileWriteStore

<html>
    <head>
        <title>Fun with ItemFileWriteStore!</title>
        <script
            type="text/javascript"
            src="http://o.aolcdn.com/dojo/1.1/dojo/dojo.xd.js">
        </script>
        <script type="text/javascript">
            dojo.require("dojo.data.ItemFileWriteStore");

            dojo.addOnLoad(function(  ) {
                coffeeStore = new dojo.data.ItemFileWriteStore({url:"coffee.json"}
    );
                coffeeStore._saveCustom = function(  ) {
                    /* Use whatever logic you need to save data back to the server.
                       This extension point gets called anytime you call an ordinary
                       save(  ). */
                }
            });
        </script>
    </head>
    <body>
    </body>
</html>

As it turns out, _saveCustom is used less frequently than you might think because it involves passing all of your data back to the server, which is not usually necessary unless you start from a blank slate and need to do that initial batch update. For many use cases—especially ones involving very large data sets—you'll want to use the interface provided by the Notification API that is introduced in the next section to take care of changes when they happen in small bite-size chunks.

Responding to notifications

To round out this section—and the rest of the chapter—we'll briefly review the Notification API that ItemFileWriteStore implements because it is incredibly useful for situations in which you need to respond to specific notifications relating to the creation of a new item, the deletion of an item, or the modification of an item via onNew, onDelete, or onSet, respectively.

As you're probably an expert reading and mapping the APIs back to specific store implementations by now, an example that adds, modifies, and deletes an item from a store is probably self-explanatory. But just in case, Example 9-16 is an adaptation of Example 9-13.

Example 9-16. Using the Notification API to hook events to ItemFileWriteStore

/* Begin notification handlers */
coffeeStore.onNew = function(item, parentItem) {
    var itemName = coffeeStore.getValue(item, "name");
    console.log("Just added", itemName, "which had parent", parentItem);
}

coffeeStore.onSet = function(item, attr, oldValue, newValue) {
    var itemName = coffeeStore.getValue(item, "name");
    console.log("Just modified the ", attr, "attribute for", itemName);

    /* Since children is a multi-valued attribute, oldValue and newValue are
       Arrays that you can iterate over and inspect though often times, you'll 
       only send newValue to the server to log the update */
}

coffeeStore.onDelete = function(item) {
    // coffeeStore.isItem(item) returns false, so don't try to access the item
    console.log("Just deleted", item);
}
/* End notification handlers */


/* Code that uses the notification handlers follows... */

//Add a top level item - triggers a notification
var newItem = coffeeStore.newItem({
    name : "Really Dark",
    description : "Left brewing in the pot all day...extra octane."
});

coffeeStore.fetchItemByIdentity({
    identity : "Dark Roasts",
    onItem : function(item, request) {
        var darkRoasts = item;

        var darkRoastChildren = coffeeStore.getValues(darkRoasts, "children");

        //Modify it - triggers a notification 
        coffeeStore.setValues(darkRoasts, 
            "children",darkRoastChildren.concat(newItem)
        )

        //And now delete it - triggers two notifications
        coffeeStore.deleteItem(newItem)
    }
});

The output you see when you run the example should be something like the following:

Just added Really Dark, which had parent null
Just modified the children attribute for Dark Roasts
Just modified the children attribute for Dark Roasts
Just deleted Object _0=13 name=[1] _RI=true description=[1]

In other words, you get the expected notification when you create the top-level item, a notification for modifying another item's children attribute when you assign the new item as a child, another notification when you remove the child item, and a final notification when you delete the item.

Tip

One subtlety to note about Example 9-16 is that the item reference you receive in the onDelete notification has already been removed from the store, so its utility is likely to be somewhat limited since you cannot legally use it in routine store operations.

Serializing and Deserializing Custom Data Types

Although not mentioned until now, you should be aware of one additional feature provided by ItemFileReadStore and ItemFileWriteStore : the ability to pack and unpack custom data types. The motivation for using a type map is that it may often be the case that you need to deal with attributes that aren't primitives, object literals, or arrays. In these circumstances, you're left with manually building up the attributes yourself—introducing cruft in your core logic—or streamlining the situation by tucking away the serialization logic elsewhere.

Implicit type-mapping

Implicit type-mapping for an ItemFileReadStore happens automatically if two special attributes, _type and _value, exist in the data; _type identifies a specific constructor function that should be invoked, which gets passed the _value. JavaScript Date objects are an incredibly common data type that can benefit from being type-mapped; a sample item from our existing data set that has been modified to make use of a date value might look like Example 9-17.

Example 9-17. Using a custom type map to deserialize a value

...
{
    name : "Light Cinnamon",
    description : "Very light brown, dry , tastes like toasted grain with
distinct sour tones, baked, bready"
    lastBrewed : {
        '_type' : "Date",
        '_value':"2008-06-15T00:00:00Z"}
    }
}
...

It almost looks too easy, but assuming that the Date constructor function is defined, that's it! Once the data is deserialized, any values for lastBrewed are honest to goodness Date objects—not just String representations:

var coffeeItem;
coffeeStore.fetchItemByIdentity({
    identity : "Light Cinnamon",
    onItem : function(item, request) {
        coffeeItem = item;
    }
});
coffeeStore.getValue(coffeeItem, "lastBrewed"); //A real Date object

Custom type maps

Alternatively, you can define a JavaScript object and provide a named deserialize function and a type parameter that could be used to construct the value. For ItemFileWriteStore, a serialize function is also available. Following along with the example of managing Date objects, a JavaScript object presenting a valid type map that could be passed in upon construction of the ItemFileWriteStore follows in Example 9-18.

Example 9-18. Passing in a custom type map to an ItemFileReadStore

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

    coffeeStore = new dojo.data.ItemFileReadStore({
        url:"coffee.json",
        typeMap : map
    });
});

Tip

Although we intentionally did not delve into dojox.data subprojects in this chapter, it would have been cheating not to at least provide a good reference for using the dojox.data.QueryReadStore, which is the canonical means of interfacing to very large server-side data sources. See http://www.oreillynet.com/onlamp/blog/2008/04/dojo_goodness_part_6_a_million.html for a concise example of using this store along with a custom server routine. This particular example illustrates how to efficiently serve up one million records in the famed DojoX Grid widget.

Get Dojo: The Definitive Guide now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.