Tutorials introduction custom stores data modelling realtime stores selects using stores store adapters store driven tree

Introduction

Separation of concerns is a fundamental aspect of organized, manageable programming, and an essential separation in web applications is that of data modeling from the user interface (where a user interface is typically defined as a view and controller in model-view-controller (MVC) architecture). The dstore architecture establishes a consistent interface for data interaction inspired by the HTML5 object store API, and builds on the Dojo object store API. This API was developed to facilitate loosely coupled development where different widgets and user interfaces could interact with data from a variety of sources in a consistent manner.

The dstore interface allows you to develop and use well-encapsulated components that can be easily connected to various data providers. The dstore project defines an API, and has multiple store implementations. Stores include an in-memory store, URL-based store, JSON/REST store, legacy object store adapters, and store mixins that provide additional functionality like parsing alternate formats, and tracking data changes.

Getting Started

The easiest store to get started with is dstore/Memory. We can simply provide an array of objects to the constructor and start interacting with it. Once the store is created, we can query it using the filter and sort methods. The query methods always return a sub-collection representing the query. The sub-collection offers the same interface as the source collection, including fetch, forEach, and fetchRange methods for fetching, iterating, and retrieving paged results. An easy way to query is to call the filter method and provide an object with name/value pairs indicating the required values of matched objects. Once we have created a filtered and/or sorted collection, we can retrieve the entire results. This can be done by calling fetch() to get a promise to the array of results or calling forEach to iterate of the results:

require(['dstore/Memory'], function(Memory){

    var employees = [
        {name:'Jim', department:'accounting'},
        {name:'Bill', department:'engineering'},
        {name:'Mike', department:'sales'},
        {name:'John', department:'sales'}
    ];
    var employeeStore = new Memory({data:employees, idProperty: 'name'});
    var salesEmployees = employeeStore.filter({department:'sales'});
    salesEmployees.forEach(function(employee){
        // this is called for each employee in the sales department
        alert(employee.name);
    });
});

This will display an alert with the name of each employee in the sales department. And again, we could alternately use fetch() to retrieve the results:

salesEmployee.fetch().then(function(results){
    // the array of results
});

View Demo

We can go on to create and delete objects in the store:

// add a new employee
employeeStore.add({name:'George', department:'accounting'});
// remove Bill
employeeStore.remove('Bill');

We can retrieve objects and update them. By default, objects in the store are simple, plain JavaScript objects (although we can configure stores for alternate model classes), so we can directly access and modify the properties (when you modify properties, make sure you do a put() to save the changes). The dstore methods return promises, so we provide callbacks to access the results:

// retrieve object with the name 'Jim'
employeeStore.get('Jim').then(function(jim){
    // show the department property
    console.log('Jim's department is ' + jim.department);
    // iterate through all the properties of jim:
    for(var i in jim){
        console.log(i, '=', jim[i]);
    }
    // update his department
    jim.department = 'engineering';
    // and store the change
    employeeStore.put(jim).then(function(){
        // confirmation that we have succesfully saved the jim object
    });    
});

Going back to querying, let's discuss the fetchRange method. Use the fetchRange method to request a specific number of objects (starting at a specific index). Limiting the result set can be critical for large-scale data sets that are used by paging-capable widgets (like the grid), where new pages of data are requested on demand. The argument to fetchRange should be an object with start and end properties denoting the starting index and ending index (the end is exclusive, the end index position is not included). Like fetch, this will return a promise to an array of objects.

In addition to the filter method, there is a also a sort method for creating a new collection with the objects sorted in a particular order. The first argument is the name of the property to sort on, and the second is whether to sort in descending order (alternately, an array of sort objects can be provided to define an order of sorts). Consistent with the filter method, sort returns a new sub-collection representing the sorted data. Here is an example of how the query methods may be used together, in conjunction with fetchRange:

employeeStore.filter({department: 'sales'})
    // the results should be sorted by department
    .sort('lastName')
    // starting at an offset of 0, up to 10 objects
    .fetchRange({start: 0, end: 10}).then(function(results){
        // the results will be the first 10 people from the sales
        // department, sorted by last name.
    });

The Memory store performs all of its actions synchronously, without delay, but it still conforms to the standard dstore API, so get, put, add, and remove all return promises, which are resolved on return.

dojo/store/JsonRest

Another useful store is the Rest store, which delegates the various store actions to your server using standards-based HTTP/REST with JSON. The store actions map intuitively to HTTP GET, PUT, POST, and DELETE methods.

Like all stores, Rest returns promises from its methods. We can use a promise by providing a callback to the returned promise:

require(['dstore/Rest'], function(Rest){
    employeeStore = new Rest({target: '/Employee/'});
    employeeStore.get('Bill').then(function(bill){
        // called once Bill was retrieved
    });
});

These examples demonstrate how to interact with stores. We can now build widgets and components that interact with stores in a generic way, free from dependence on a particular implementation. We can also plug our stores into existing components that use Dojo object stores.

For example, dstore's StoreSeries adapter allows us to use a store as the data source for a chart. Most components that use a store require that you provide the query that the component should use to query the store:

// Note that while the Default plot2d module is not used explicitly, it needs to
// be loaded to be able to create a Chart when no other plot is specified.
require([
    'dojox/charting/Chart',
    'dstore/charting/StoreSeries'
    /*, other deps */,
    'dojox/charting/plot2d/Default',
    'dojo/domReady!'
], function(Chart, StoreSeries /*, other deps */){
    /* create stockStore here... */

    new Chart('lines').
    /* any other config of chart */
    // now use a data series from my store
    addSeries('Price', new StoreSeries(
    stockStore.filter({sector: 'technology'}), 'price'))
        .render();
});

Another important concept in the dstore architecture is composing functionality by adding store mixins. dstore comes with several store mixins to add functionality, including a Cache mixin, Csv-parsing mixin, and a Trackable mixin for tracking the position and sequence of data changes.

URL-Driven Memory Store

A frequent need for developers is to pull in a set of data (typically JSON) from a URL and then have subsequent queries and data retrievals (and potentially even data modifications) taking place in-memory. The RequestMemory provides this type of store behavior. We can configure this store with a URL that will be requested to get the initial data, and then all operations will be performed on the retrieved data (and wait for the data to be retrieved if it is in transit). When we instantiate the store, we specify the target URL to retrieve the data from:

var store = new RequestMemory({
    target: 'path/to/data.json'
});

The store will send a request when it is instantiated, and we can immediately begin querying and interacting with the store (again the results will be deferred through a promise until a response has been received from the server);

store.filter({priority: 'high'}).forEach(function (object) {
    // for each object that matches the filter
});
// returns promise for the object with the given id, once the data set is available
var promiseForObject = store.get(someId); 

Conclusion

The new dstore package provides a critical foundation for building applications with a clean separation of concerns between our data and our user interfaces. It provides a straight-forward API, allowing for easy development of custom stores. For more information, check out the reference documentation in the dstore project on GitHub.