We have recently reached the alpha milestone in the development of dgrid, a new component for creating lists and grids. Built on the latest Dojo technology, dgrid is designed to be lightweight, fast, mobile-ready, and easy-to-use. This SitePen-led project brings the best innovations and techniques from extensive experience on the DataGrid, to create a brand new simple and fast architecture. Let’s explore some of the examples included in the project to demonstrate how to use it.

Download/Installation

dgrid is a package in the Dojo Foundation package repository. To install the grid, you can either download it (and its dependencies) or install it into the current working directory using CPM:

cpm install dgrid

Getting Started

The two most basic modules in the dgrid package are List and Grid. List provides the base functionality for rendering any kind of list. Grid extends List, adding functionality for defining columns and displaying tabular data with headers. Let’s take a look at how we can use the Grid module, as it is likely to be the primary topic of interest.

Simple Grid

The most basic usage of the Grid module is to simply take an array of objects and render them in tabular form. In this simple grid example, our dataset is an array of steps in a recipe that looks like:

var data = [
    {order: 1, name:"preheat", description:"Preheat your oven to 350°F"},
    {order: 2, name:"mix dry", description: 
        "In a medium bowl, combine flour, salt, and baking soda"},
    ...
];

The only real configuration needed for the grid to render this data is column definitions. dgrid allows us to define columns with an object hash (or an array) where the property names correspond to object fields by default, and the values are configuration objects where we can specify the sortability of a column, the label, and other information. The property value can alternately be a simple string, which is interpreted as the label of the column (used in the column header). In our example, we define our columns to render:

var columns = {
    order: "step", // simply defining the label
    name: {}, // reuses key (field name) as label
    description: { label: "what to do", sortable: false }
};

We have defined three columns. The first will render the order property for each object, the second will render the name property, and the third will render the description property. The first two columns will be sortable by default (you can click on the column header to sort it), and since no label is defined in the column definition for the name field, the column header defaults to the property name. The first column uses a string as the property value which defines the column name (equivalent to order: { label: "step" }).

With our column configuration we can now easily instantiate our grid. We just provide the columns and give the id of a target element to the Grid module/constructor:

require(["dgrid/Grid", "dojo/domReady!"],
    function(Grid){
        // var data = ..., columns = ..., as above
        var grid = new Grid({
            columns: columns
        }, "grid"); // id of target element
        ...

And then to render the data:

        grid.renderArray(data);

With all of these examples, feel free to check the API documentation or README for more information on specific methods and properties.

Skinning

Typically we will also want to apply a skin to our grid. It should be noted that the grid utilizes CSS best practices of structure and skin separation. The structural CSS required for the grid is automatically dynamically loaded as a dependency. However, we can optionally choose a look and feel with one of the provided themes including claro, tundra, nihilo, soria, slate, sage, cactus, and squid.

For example, we could easily apply the Claro theme by adding the following to our page:

<head>
    ...
    <link rel="stylesheet" href="css/skins/claro.css">
</head>
<body class="claro">
    ...
</body>

We can also easily create custom themes. Because the structural CSS is completely separate, there are only a few rules that need to be specified to provide a new color scheme. Additionally, dgrid follows class naming conventions from jQuery ThemeRoller, so ThemeRoller skins can be easily used on dgrid components.

Store-driven Grid

Next we will create a grid driven by an object store, an API based on the HTML5 IndexedDB API. It can be used with data providers that wrap in-memory data, JSON/REST-based data, or any other source.

Store-driven grids are critical for scaling up to large data sets, as they allow the grid to interact with the data provider and only retrieve the data needed for the visible set of rows. The data provider implements querying, sorting, and paged retrieval of data from the data source. The grid then provides virtual paging where it will request data from the data source as the rows are scrolled into view. It will request sorted data in response to header clicks. The store-driven grids can also automatically send data changes back to the data provider in response to editing cells (more on that later).

Let’s start out by using dojo/store/Memory, a simple store based on an in-memory JavaScript array. This can later be substituted by an alternate dojo/store implementation, such as the JsonRest store, which is a great choice for RESTful JSON communication with a server with integrated support for sorting and paging.

To create the Memory store, we simply instantiate it with an array of data:

define(["dojo/store/Memory"], function(Memory){
    var testStore = new Memory({data:arrayOfData});
    ...

To use the store with a grid, we will use the OnDemandGrid module/constructor. We provide the store to the constructor:

require(["dgrid/OnDemandGrid", "dojo/store/Memory"],
    function(Grid, Memory){
        var testStore = new Memory({data:arrayOfData});
        var grid = new Grid({
            store: testStore,
            columns: columns
        }, "grid");
        ...

The grid will now immediately query the store for data, retrieving limited blocks or pages of data. If we click on any of the sortable column headers, the grid will re-query the store automatically. The grid can also instantly respond to changes in the underlying data provider if the store supports observation of query results (often achieved by wrapping it with the Observable module).

We can also provide a base query to be passed to the store when queries are executed, by including a query property on the grid:

var grid = new Grid({
    store: testStore,
    query: someQuery,
    ...
});

Styling Columns

dgrid is designed to be extremely fast and follow styling best practices, achieving column styling via CSS rules. Each column is assigned class names based on the column id and field, of the form “column-” and “field-“. For example, to specify a width and background color for the “description” column we defined above, we can add CSS:

.field-description {
    width: 50em;
    background-color: blue;
}

Adding Functionality with Mixins

dgrid is designed to easily support adding functionality to the list or grid instance, via the standard mixin composition mechanisms in Dojo via dojo/_base/declare. dgrid includes several mixin modules:

  • dgrid/Keyboard: Adds keyboard navigation support
  • dgrid/Selection: Adds row selection support
  • dgrid/CellSelection: Adds cell selection support (extends dgrid/Selection)
  • dgrid/ColumnSet: Adds support for sets of column to provide column locking or independent horizontal scrolling

In addition to these “core” mixins, there is an “extensions” subfolder containing additional mixins which add functionality that is potentially useful, but less commonly desired:

  • dgrid/extensions/DnD: Adds drag’n'drop support
  • dgrid/extensions/ColumnResizer: Adds column resizing support

In this example, we will add keyboard navigation and row selection support to our grid. We can quickly do this with an anonymous inline dojo/_base/declare instantiation that creates a Grid combined with the Selection and Keyboard mixins:

require(["dgrid/OnDemandGrid","dgrid/Selection", "dgrid/Keyboard", "dojo/_base/declare"],
    function(Grid, Selection, Keyboard){
        declare([Grid, Selection, Keyboard])({
            store: testStore,
            columns: columns
        }, "grid");
        ...

View the example (see the first grid).

Column Plugins

The Keyboard and Selection mixins are grid-level plugins, but we can create and use column-level plugins as well. Column plugin modules expose functions to be applied to a column definition in our set of columns.

One such plugin is the dgrid/editor plugin, which will make cells in the target column editable. With our current sample grid, we can make certain columns editable using this plugin:

require(["dgrid/OnDemandGrid","dgrid/Selection", "dgrid/Keyboard",
    "dgrid/editor"],
    function(Grid, Selection, Keyboard, editor){
        var columns = {
            // always editable:
            col1: editor({name: "Column 1"}, "text"),
            col2: "Column 2",
            // editable on double-click:
            col3: editor({name: "Column 3"}, "text", "dblclick"),
            col4: {name: "Column 4"},
            // editable on single-click:
            "last-col": editor({name: "Column 5", field: "col5"},
                "text", "click")
        };
        ...
    });

With this you can now edit cells in the grid, similar to the first grid in this example page (however, the example page activates all its Editors on double-click).

The editor plugin can do much more than render a simple textbox, however. The editor plugin takes three arguments. The first is the standard column definition object. The second is the editor to use. The third is the event to trigger activation of the editor.

If a string is provided as the second argument, a plain HTML input will be used, and the value of this argument identifies the type of the input to use. Common types would include “radio”, “checkbox”, or “text”. The second argument can alternately be a Dijit form widget constructor, in which case an instance of the widget is used as the editor.

The third argument defines the trigger event for the editor. Common events to use would be “click” or “dblclick”, for mouse events, or the custom “dgrid-cellfocusin” event, which handles both mouse- and keyboard-driven focus events. If the third argument is omitted, the editor will always be visible.

Here is an example of creating radio and checkbox columns (with no trigger event so the radio and checkbox will always be shown):

    var columns = [
        editor({name: "CheckBox", field: "bool"}, "checkbox"),
        editor({name: "Radio", sortable: false}, "radio"),
        ...
    ];

As stated above, we can also include Dijit form widgets as editors for cells. We simply provide the widget constructor as the second argument. Here is an example of using the DateTextBox (for dates), Slider (for numbers), and NumberSpinner (for numbers) as editors for column cells:

require(["dgrid/editor", "dijit/form/DateTextBox", 
    "dijit/form/HorizontalSlider", "dijit/form/NumberSpinner"], 
    function(editor, DateTextBox, Slider, NumberSpinner){
        var columns = [
            editor({name: "A Date", field: "date"},
                DateTextBox),
            editor({name: "Real Number", field: "floatNum"}, 
                Slider),
            editor({name: "Integer", field: "integer"}, 
                NumberSpinner),
            ...
        ];
        ...
    });

The editor also supports a function property named canEdit in the column definition object. If provided, this function defines whether a cell in a particular row is editable. For each row/cell, the canEdit function is called and passed the object to be rendered. If canEdit returns a truthy value, the cell will be editable. For example:

    var columns = [
        editor({name: "Text editable if checkbox checked", field: "text", 
            canEdit: function(object){
                return object.bool;
            }
        }, "text", "dblclick"),
        ...
    ];

We can see these different editor examples together on this page.

Saving Changes

What happens when we edit a cell in an OnDemandGrid? The grid will store the change in its cache of dirty objects and be ready to save the changes on demand. To save the changes, we simply call the save() method on the grid. Any dirty data will then be sent to the store. Using the Memory store, custom handling would need to be implemented if this data is to be persisted in any way. The JsonRest store will automatically send changes back to the server through PUT requests.

Because OnDemandGrid can automatically respond to changes in data, it is easy to see the editing and saving capability in action. When an OnDemandGrid references a store instance which supports the observe method on its query results, the grid will reflect any changes made or reported at the store level. This can be seen in action in the second and third examples on this page.

When using the editor column plugin, it is possible to cause edits to a column to automatically save changes to the Grid. Set the autoSave property in the column definition to true, and any changes will be saved as soon as a cell in that column loses focus after being edited.

Tree

Another powerful column plugin is the dgrid/tree plugin, which allows for expandable rows to easily navigate hierarchical data. Like Editor and other column plugins, this module returns a function to be applied to a column definition.

To use the tree plugin, we need a store which provides a getChildren method, which implements the logic for finding the children of an object. In this example, we create a getChildren method that simply gets the children array property, then iterates through the references and retrieves each child item:

var testStore = new Memory({
    data: arrayOfData,
    getChildren: function(parent){
        var store = this;
        // Note that arrayUtil is a local reference to dojo/_base/array
        return arrayUtil.map(parent.children, function(child){
            return store.get(child._reference);
        });
    },
    ...

We can also (optionally) provide a mayHaveChildren method which will indicate whether a given object has children (and thus whether or not to display an expansion icon).

var testStore = new Memory({
    data: arrayOfData,
    mayHaveChildren: function(parent){
        return parent.children;
    },
    ...
});

And now we simply define the tree column:

var treeGrid = new Grid({
    store: testCountryStore,
    query: {type: "continent"},
    columns: {
        tree({label: "Name", field:"name", sortable: false}),
        ...
    }
});

Another thing to note about this particular example is that we are using a query that will only retrieve the top level items for the initial rendering of the grid. In our example, only objects with a type of “continent” are top level items; we expand those to see the children, which are countries and cities.

Plain Lists and Custom Row Rendering

One of the most direct and low-level ways to use the dgrid package is to use the List module. This module is the base class, and provides basic scrolling (including mobile touch scrolling), row rendering, and row access which the Grid module builds upon. We can directly use the List for situations when we don’t want the grid’s tabular layout, or we need to use our own custom row rendering. We can use the List module the same way as the Grid, simply rendering an array of values or objects, or use OnDemandList (the store-driven version of the List module) to render data from a store. To render an array, we could simply do the following:

require(["dgrid/List"], function(List){
    var list = new List({}, "list");
    list.renderArray(arrayOfStrings);
});

See our example page again (under the second heading, to the right) for an example List component with Selection and Keyboard functionality mixed in.

If the the array has strings as values (or other values with appropriate toString() methods), it can be directly rendered by the List. If we have an array of objects (or are getting data from a store), we can implement our own renderRow method to render the objects:

var list = new List({
  renderRow: function(object, options){
    return dojo.create("div", {
      innerHTML: "First name: " + object.first + 
        " last name: " + object.last
    });
  }
}, "list");

Check it Out

Check out the dgrid project page for more documentation, issue tracking, and source code. We certainly encourage you to peruse the source. It is remarkably small due to the minimalistic design principles, making it very accessible and easy to learn and extend.

In addition, the test pages and a few preliminary demos are available. (These are part of the source package, so you can tinker with them on your own server as well.)