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.
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
});
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.
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.
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);
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.