Introducing the Next Grid: dgrid

By on October 26, 2011 1:20 pm
Notice: This post is now several years old and covers early versions of dgrid prior to 1.x. We recommend starting with the dgrid tutorials to leverage dgrid 1.x.

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 available on GitHub. To install the grid, you can either download it (and its dependencies) or install dgrid using npm or bower.

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 several provided themes.

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.

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 dgrid test pages and a few demos are available. (These are part of the source package, so you can tinker with them on your own server as well.)

Comments

  • otiteca

    Hi,
    I love that new grid. Really better global performance and also faster scrolling…
    But i do like to know why we can not provide simple way to:

    * provide paging (not virtual paging), paging that send a new request to the store
    * as there’s a workable tree plugin, provide declarative grouping

  • Eric

    First off, the grid looks nice. Since I have only had a quick look and might be missing something, take these ideas with that in mind.

    It would be cool to have “multi-column” sort. I have seen that done in other grids where by holding the Shift key when clicking a second column, it preserves the initial sort then sorts by the clicked column as a second (like in Excel — Sort By column, Then By column…)

    Also, for grids that are used in certain applications, it is nice to be able to highlight some rows (even with simple ‘left-click and drag’) then copy them for paste into an external application. Ideally each column/cell entry in a row is separated by a tab when pasted, with the the first column cells in each row the start of a new line. I tested this on one of the demo grids and noticed it mostly worked, but each row started with a tab (that might have been because the first column had images in them, but another test on another grid did the same.) Also, each grid row should be copy/pasted as a single row, which one demo did not do.

    Here is some output:
    http://sitepen.github.com/dgrid/dgrid/demos/multiview/
    dojo Dojo Core

    dijit Dijit

    dgrid dgrid

    xstyle xstyle

    put-selector put-selector

    http://sitepen.github.com/dgrid/dgrid/demos/dTuned/
    Nothin’ Song 5:40 1995 Alice In Chains Alice In Chains Alternative
    Frogs 8:18 1995 Alice In Chains Alice In Chains Alternative
    Over Now 7:03 1995 Alice In Chains Alice In Chains Alternative
    Man In the Box 4:44 1990 Alice In Chains Facelift Alternative

    Again, part of the first example and the new lines with one just a tab and another just a space (I know this won’t show up well in a text box) could be because of the images in the first column. Anyway, just some thoughts. Thanks for the good work.

  • Eric

    Sorry, that output on the bottom did not show too well. I have created a pastebin that expires in a month. Highlight text to see the tab and space characters:

    http://pastebin.com/tg72uKb3

  • Why is the dgrid not bundled with the 1.7 release?

  • Bas

    When using dgrid with an Editor in autoSave mode I was really missing the ability to press escape to cancel the update and revert the value to the original.

    I hacked this into dgrid/Editor.js

    function onkeypress(event) {
    if (event.keyCode == 27) {
    var target = event.target;
    if(“lastValue” in target && target.className.indexOf(“dgrid-input”) > -1){
    target[target.type == “checkbox” || target.type == “radio” ? “checked” : “value”] = target.lastValue;
    this.focus();
    }
    }
    }

    and added in the renderWidget function the following:

    grid.on(“keydown”, dojo.hitch(cell, onkeypress));

    It seems to work well for me but rather would like to see this type of functionality be officially added to the codebase so I won’t run into trouble after you update the code.

    thanks for the very promising new grid!

  • @Martin, we are pushing to move new components out of Dojo so they can be released on an independent release cycle, and have some level of autonomy. See this post for more information: http://www.sitepen.com/blog/2011/07/25/dojo-foundation-packages/

  • John

    Anyone know if it’s possible to use a combination of the onDemandGrid and the Tree? I have a small subset of root nodes, but I would like to lazy load the, say, 10000 children of each of those root nodes. anyone every do this? does the Tree column support this out of the box?

    Thanks!

  • @John: Yes, the Tree column can be used with the OnDemandGrid. This should work as long as your getChildren method accepts a second arg for query options (that will include the start and count for paging) and returns a query result set.

  • John

    @Kris,

    Yeah, that’s what I thought as well.. it’s requesting the first 25 rows successfully, I’m returning them wrapped in a QueryResults object and it’s showing them properly.. however it never seems to request the next 25. My Store could be doing something strange, but I can’t figure out what it might be.

    John

  • John

    I have reproduced what seems to be a problem with the Tree plugin.. I have updated dgrid/test/Tree.html to pass in a modified testCountryStore that contains 100 cities under ‘Argentina’. It only ever shows 25. I’m sure this isn’t the place to be posting feature requests/bugs, but please allow me this one annoying post. :)

  • Superbird

    “Multiple” selection is supported in the grid but the “dgrid-select” event doesn’t support “multiple” selection. It only has one row. Am I missing something ?

  • @John: I just fixed the paging issue with the Tree in https://github.com/kriszyp/dgrid/tree/scrolling (we will merge into master soon).

    @Superbird: You can get the cumulative set of all selected items from grid.selection (an object hash of selected objects).

  • bwa

    Looks like dgrid-datachange is not fired on IE when using a jsonrest store….
    see http://dojo-toolkit.33424.n3.nabble.com/dgrid-dgrid-datachange-not-fired-with-IE-9-tp3671607p3671607.html

  • Anthony

    I’ve put an OnDemandGrid into a mobile dojo application (1.7.1). I’m seeing an issue where the rows recycle when you scroll down (i.e. the first row re-appears, then the second, etc..). Is that a known behaviour? Could it be paging related?

  • Jochen

    I really like the grid and it works great. But what to do if I can’t tell how many results a query might have (and I really can’t – there is no way around it). I tried to extend the range with every request (like 0-24/50, next request sets header to 25-49/75 and so on), but that does not work ….is there a way to do something like this with dgrid? Thanks for any help …

  • Is there any road map to make extensions replace EnhancedGrid Plugins ?

  • E

    How does the dgrid hold up with a large data set, hundreds of rows?

  • @E: We routinely test the grid with over 10,000 rows, and it works smoothly.

  • ge

    Sounds like a great addition to dojo, however I can’t seem to get the module to install properly in my dojo 1.7.2 directory structure. At first I just downloaded dgrid, put the dgrid directory under dojoroot and “required” it in my code. I kept getting “dgrid is undefined”. I’ve posted a more detailed explanation of the problem to the discussion board on dojotoolkit.org. In the meantime, I’m trying to to use cpm, thinking that maybe I missed something with my simple download approach. However cpm doesn’t download anything due to connection timeouts. I upgraded Java on my machine hoping that would fix cpm, but to no avail.

    Still stuck at “dgrid is not defined.”

  • @ge: If you’re on Mac OS X Lion, I had to follow the instructions noted in this issue: https://github.com/kriszyp/cpm/issues/4#issuecomment-3296942. To get dgrid working, you’ll also need its two dependencies, xstyle and put-selector. cpm handles this, or you can also grab those and place them parallel to dgrid. So your directory structure would look something like this:

    /js/dgrid/
    /js/dijit/
    /js/dojo/
    /js/dojox/
    /js/put-selector/
    /js/util/
    /js/xstyle/

  • Aldo

    I get
    “RequestError: Unable to load ./data/rest.php? status: 416”
    when I run the JsonRest.html test (with today’s dgrid code).

    Tomcat 7.0.29, either Dojo 1.7.3 or Dojo 1.8.0rc2, Chrome 21.0.1180.75 m, Firefox 14.0.1, IE 9.0.8112

    In the HTTP request I see the headers:

    Range:items=0-24
    X-Range:items=0-24

    Are those ranges bothering the browser?

  • @Aldo, no, those headers are bothering your server. Tomcat can’t run the php sample. To run the PHP samples you need PHP, or you will need to write a servlet (or whatever framework you are using) that can handle the paged data requests.

  • Aldo

    Thank you Kris.
    In fact I use Java.
    Is there a sample Java Servlet that handles the paged data request I can look at?
    Besides http://dojotoolkit.org/reference-guide/1.7/dojo/store/JsonRest.html#dojo-store-jsonrest is there any other documentation about creating the server side of a JsonRest store you suggest to read?

  • We were bummed when we tried using the dGrid on the iPad because the rows were not rendering properly when on scroll. After looking around a bit, it turns out that there is an issue in iOS with relatively positioned elements located under the fold (who knew?). Here’s a post explaining the fix in case anyone else is having the same issue:

    http://cantina.co/2012/03/06/ios-5-native-scrolling-grins-and-gothcas/

    Thanks for the great work! All in all, it was a pretty painless switch.

  • IE7 is supported… which version of dgrid and Dojo were you trying?

  • Greg Prosch

    Links on this page are broken. For example, the “test pages” and “demos” links just above are broken.

  • Hi Greg, this post is indeed quite old (6+ years now!) and quite a bit has changed… I’d suggest starting with dgrid.io for information, examples/demos, tests, and documentation.

  • Greg Prosch

    Dylan, my bad! I didn’t realize how old it was and should have checked. Thank you for directing me to the up-to-date site.