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 exposes 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
orfetch
. 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.
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 adescending
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.