Using Grids and Stores

In previous dgrid tutorials, you may have noticed demos which create grids referencing dstore instances. This tutorial will delve into the topic of store communication in more detail, and demonstrate dgrid's OnDemandList and Pagination components which interact with stores in various ways.

dstore provides a common API for accessing and manipulating data. It is intended to replace dojo/store and the older dojo/data API. Starting with 0.4 dgrid interfaces directly with dstore stores. Previous releases of dgrid worked with dojo/store instances, and the older dojox/grid works with the older dojo/data API.

If this is your first time working with dstore, we recommend starting by familiarizing yourself with dstore Collections and Stores.

Before diving into specific components, it is important to understand a few fundamental details of how dgrid interacts with stores. Many of these principles are also applicable to other widgets.

Uniquely Identifying Store Items

Stores are used to represent collections of data items. It is quite common for these items to possess a field which uniquely identifies each item, making it possible to request a specific item. In the dstore API, this ability is represented by the get and getIdentity methods. Various dgrid components rely on these methods in order to access the item represented by a specific row, so stores used with dgrid need to support these methods.

In order for these methods to work, the store needs to know how to uniquely identify items. This is typically achieved by specifying idProperty in the arguments passed to the store constructor. If not specified, it usually defaults to "id".

Don't forget to configure the store to properly detect item identities. Failing to do so can lead to a variety of unpredictable behaviors.

Querying Stores

When widgets interact with stores, they do so by directly calling the store's methods. For example, dgrid components call stores' sort and fetchRange methods, since sort and range behavior are controlled through dgrid components or its user interface.

When dgrid interacts with a store, all paging, filtering, and sorting responsibilities fall upon the store, not the grid. As a result, dgrid has a tendency to bring store- or service-related issues to light. When encountering data rendering issues, always check that the store implementation (and backend service, if applicable) are performing as expected. Don't forget to consult the store documentation; links for the implementations shipped with dstore are included at the end of this tutorial.

With these general principles now behind us, let's take a look at the components dgrid offers for presenting store data.

Virtual Scrolling with OnDemandGrid

When using dgrid with stores, the most commonly-used component is OnDemandGrid, which combines the tabular display logic of Grid with the on-demand loading and rendering of OnDemandList. The result is a grid which scrolls fluidly as if all items were rendered, but only actually requests items from the store as necessary to fill the current viewport. Developers migrating to dgrid from dojox/grid/DataGrid should find this behavior familiar.

The following example creates a grid which will query an in-memory store. The store is populated with data fetched using dstore's dstore/RequestMemory module.

	require([
		'dstore/RequestMemory',
		'dgrid/OnDemandGrid'
	], function (RequestMemory, OnDemandGrid) {
		// Create an instance of OnDemandGrid referencing the store
		var grid = new OnDemandGrid({
			collection: new RequestMemory({ target: 'hof-batting.json' }),
			columns: {
				first: 'First Name',
				last: 'Last Name',
				totalG: 'Games Played'
			}
		}, 'grid');

		grid.startup();
	});
View Demo

Note that unlike previous versions of dgrid, dgrid 0.4 and later expose a collection property instead of store. In dstore, collections are a broader concept, and stores are also collections.

OnDemandList and OnDemandGrid provide a number of properties for fine-tuning the lazy-loading behavior, though in many cases, the default values will suffice. The following list outlines a few of the most commonly-altered settings:

minRowsPerPage
The minimum number of items to request from the store at a given time. This number should never be less than the number of rows expected to be visible in the grid at any given time, in order to avoid excessive queries. The default is 25.
farOffRemoval
The distance, in pixels, at which to consider rows far enough away from the current viewport to begin removing them from the DOM. This can be reduced to more aggressively remove rows from the DOM as the user scrolls away, but it should never be less than the height of the grid. The default is 2000.
pagingDelay
The minimum amount of time (in milliseconds) to wait after scrolling occurs before requesting items from the store. This prevents excessive querying as the user is scrolling, which is especially important when using a store that retrieves data from a server on every call to range or fetch. The default is 15.

A complete list of supported properties is available in the OnDemandList and OnDemandGrid documentation.

The following demo includes two instances of OnDemandGrid with different settings for minRowsPerPage and farOffRemoval, to provide an example of how changing these properties can affect user experience.

View Demo

The Pagination Extension

While the virtual scrolling provided by OnDemandGrid is often preferable, sometimes an application calls for presenting data in discrete pages, following a more traditional approach. dgrid includes a Pagination extension for this purpose, displaying a set number of results at any given time. The Pagination extension populates a footer area with controls to switch between pages, which can be customized via various properties outlined in the Pagination documentation.

Note that the Pagination extension is incompatible with OnDemandGrid, since each has its own way of dealing with collection ranges. Pagination should be mixed into Grid, not OnDemandGrid.

The following example is very similar to the first OnDemandGrid example above, but instantiates a Grid with the Pagination extension mixed in instead:

	require([
		'dojo/_base/declare',
		'dstore/RequestMemory',
		'dgrid/Grid',
		'dgrid/extensions/Pagination'
	], function (declare, RequestMemory, Grid, Pagination) {
		// Create a Grid instance using Pagination, referencing the store
		var grid = new (declare([ Grid, Pagination ]))({
			collection: new RequestMemory({ target: 'hof-batting.json' }),
			className: 'dgrid-autoheight',
			columns: {
				first: 'First Name',
				last: 'Last Name',
				totalG: 'Games Played'
			}
		}, 'grid');

		grid.startup();
	});
View Demo

For further examples, including a form which allows experimenting with various properties supported by the Pagination extension, see the test page from the dgrid repository.

Customizing Messages

Both OnDemandGrid and Pagination include functionality allowing for custom messages to be displayed while data is being loaded, and when an empty result set is received.

loadingMessage
An optional message to be displayed in the loading node which appears when a new page of results is requested.
noDataMessage
An optional message to be displayed when a query yields no results.
	var grid = new (declare([ Grid, Pagination ]))({
		collection: store,
		columns: columns,
		loadingMessage: 'Loading data...',
		noDataMessage: 'No results found.'
	}, 'grid');

	grid.startup();

These messages may be styled using the dgrid-loading and dgrid-no-data classes, respectively:

	.dgrid-no-data,
	.dgrid-loading {
		color: #aaa;
		font-size: 3em;
		padding: 3em;
		text-align: center;
	}
View Demo

Handling Errors

You may have noticed that there is no equivalent for defining an error message. This is because errors can occur for a wide variety of reasons, at different points in time, so it is difficult to provide one place for an error message that fits all situations. However, dgrid instances will emit a dgrid-error event when an error occurs during a store operation. This event can be listened to in order to react however is deemed appropriate.

In addition to the dgrid-error event, the OnDemandGrid and Pagination modules also emit a dgrid-refresh-complete event any time a refresh call completes successfully.

The following example demonstrates listening to these two events in conjunction to show or hide a node containing an error message:

	grid.on('dgrid-error', function(event) {
		// Display an error message above the grid when an error occurs.
		messageNode.className = 'errorMessage';
		messageNode.innerHTML = event.error.message;
	});

	grid.on('dgrid-refresh-complete', function(event) {
		// Hide any previous error message when a refresh
		// completes successfully.
		messageNode.className = 'errorMessage hidden';
		messageNode.innerHTML = '';
	});
View Demo

APIs for Customizing Queries

Earlier, we mentioned that dgrid issues dstore API calls for controlling sort order and fetching data ranges. Filtering restrictions, on the other hand, are not governed by dgrid components; instead, you pass a filtered collection to the grid's collection property. The collection and sort can be set when calling the constructor:

collection
Specifies the collection to fetch items from. This can be a store, or an already-processed (e.g. filtered) collection.
sort
Specifies the sort order to be used for fetch requests. The value is normally an array of objects where each object contains a property named property identifying the data field to sort, and optionally a descending property which, if true, indicates to sort descending rather than ascending. For simple cases, dgrid supports passing a string, simply referencing the name of a data field to be sorted in ascending order.

These options can also be modified later after instantiation, using the set method:

set("collection", newCollection)
Instructs the grid to reference a different collection.
set("sort", newSort)
Resets the sort order passed to fetch requests.

In order to filter the data in dgrid to a subset of the store's data (i.e. perform a custom query), set the grid's collection to a collection returned by a filter call to the store.

The following example demonstrates hooking up form controls to handlers which use the set method to update sort and provide a filtered collection:

	on(dom.byId('queryForm'), 'submit', function(event) {
		event.preventDefault();
		grid.set('collection', store.filter({
			// Pass a RegExp to Memory's filter method
			// Note: this code does not go out of its way to escape
			// characters that have special meaning in RegExps
			last: new RegExp(this.elements.last.value, 'i')
		}));
	});

	on(dom.byId('queryForm'), 'reset', function() {
		// Reset the query when the form is reset
		grid.set('collection', store);
	});

	on(dom.byId('sortLastAsc'), 'click', function() {
		// Simple case: pass a string representing field to be
		// sorted in ascending order
		grid.set('sort', 'last');
	});

	on(dom.byId('sortGamesDesc'), 'click', function() {
		// Advanced case: pass array of objects, following
		// the same format as sort in dstore
		grid.set('sort', [ { property: 'totalG', descending: true } ]);
	});
View Demo

Remember that dgrid only understands dstore APIs. In cases where you need to connect to a dojo/store instance with dgrid, wrap the store using the dstore/legacy/StoreAdapter module.

If you need to connect to a store using the older dojo/data API, you will still need to use dstore's StoreAdapter, but you'll first need to wrap the store using the dojo/store/DataStore module.

Conclusion

dgrid offers two distinct ways to efficiently retrieve data using the dstore API: the OnDemandList and OnDemandGrid modules, and the Pagination mixin. These components directly interact with a collection's fetchRange and sort APIs, which are expected to handle all logic pertaining to sorting and paging.

Resources