miércoles 21 de noviembre de 2007

Adding pagination to the dojo (Virtual)Grid

dojo 1.0 was finally released a couple of weeks ago and it packs once again (it was missing in v0.9) one of the most useful widgets, the Grid (formerly FilteringTable). This new widget (actually it's a revised version of TurboGrid) features a nice set of options (in a brief summary):
  • Performant and robust
  • Backed by a model (data store)
  • Configurable (even programmatically)
  • Inline edition
Probably the synchronization between a data and view is one of the most appreciated features. The raw performance would be other (it can load thousands of rows). A virtual scrolling mechanism had to be devised to allow them. Unfortunately this solution has some cons as well
  • Not always a huge number of records is available but just little chunks
  • Pagination is required or preferred to scrolling
  • The current data stores are not meant to interact with a server
For a developer used to work with DWR it's really too much. Fortunately, dojo offers a solution in VirtualGrid. The VirtualGrid widget is a Grid that is not backed by a model. This means that it supports everything that a Grid supports but the developer will need to provide the functions that the data store would provide otherwise (fetching, loading, sorting, filtering). It seems complicated but it is not! I will start showing how the final result should look like:


So now, let's review step by step how to do it. The first thing to do is declare the widget:

<script type="text/javascript"
   src='/dojo/dojo.js'
   djConfig="isDebug:false, parseOnLoad: true">
</script>
<script type='text/javascript'>
   dojo.require("dojox.grid.VirtualGrid");
   dojo.require("dojo.parser");
</script>
<div
   id="grid"
   dojoType="dojox.VirtualGrid"
   autoHeight="true"
   autoWidth="false"
   rowCount="10"
   get="getRow">
</div>

Nothing fancy there. Just the usual declaration of a dojo widget (dojox in this case). The HTML div includes just one weird attribute named get. It refers to the javascript function that will be called to populate each cell. The table as is cannot be painted. The information about the layout is missing:

dojo.addOnLoad(init);

function init() {
   var view = {noscroll: true, cells:[]};
   var header = [];
   for (var column in headers) {
      aColumn = new Object();
      aColumn.name = headers[column];
      aColumn.formatter = formatCell;
      aColumn.width = widths[column] + "px";
      header.push(aColumn);
   }
   view.cells.push(header);
   layout.push(view);
}

The code above uses a couple global variables (headers, widths) to create a layout. This layout is used along with the data to create the widget itself. The data will be obtained using the previously declared get function.

function getRow(inRowIndex) {
   var obj = dataArray[inRowIndex];
   var field = fields[this.index];
   return eval("obj." + field)
}

function formatCell(cellData) {
   return cellData;
}

That is enough. Supposing the data is stored as an object array (typical DWR response for example) named dataArray that is globally accesible, the code above retrieves the value of specific field of the object (the field names have to be stored in another array). this.index refers to the current cell, inRowIndex refers to the current row number. The formatCell function is called to format the output, for example to parse date objects. In our case it will rely on the default javascript toString representation. To load and populate the grid call the following function once there's data in the array:

function loadTable() {
   dijit.byId("grid").setStructure(layout);
}

By now, a ten row table should be visible on the screen (quite ugly if you haven't imported the stylesheets by the way). This is the base table to work with. It's very basic and lacks all the features. Let's start with sorting. Add the following lines to the init function

var grid = dijit.byId("grid");
grid.canSort = function(column) { return true; }
grid.sort = function() {...}
grid.setSortIndex(0, false);

I cannot provide the sort function code here as is quite extensive but you can find it in the example WAR below. With sorting working it's time to add filtering. The only real interest is obtaining the cell value. I'll use a context menu to do it:

<div
   dojoType="dijit.Menu"
   id="gridMenu"
   style="display: none;">
   <div
      divdojoType="dijit.MenuItem"
      onClick="applyCellFilter(cellNode)">
         Filter by [<span id="cellFilter"></span>]
   </div>
</div>

<script>
   var selectedCellNode;

   dojo.addOnLoad(function() {
      grid = dijit.byId("grid");
      gridMenu = dijit.byId("gridMenu");
      gridMenu.bindDomNode(grid.domNode);
      grid.onCellContextMenu = function(e) {
         byId('cellFilter').innerHTML = e.cellNode.innerHTML;
         selectedCellNode = e.cellNode;
      };
   });

   function applyCellFilter(id, cell) {...}
</script>


As before, the code to filter the object array is available in the example WAR. The interesting part in the code above is the onCellContextMenu. It iwill save the cell clicked in a javascript variable for later use.

Finally, the widget is in a position to add pagination. This is done externally using a list of page numbers an a couple of buttons to enable navigation. All the work is done against the data array. Basically, you only need to take care of the current offset. There's a little trick, always ask for one record more than the showed ones so you can easily test if there are more records available. The code can get really complex (a lot of functionality is working together by now) so you're better off reading it completely.

And now to the example. Some things to take into account just before:
  • All the styles and stuff that makes everything look pretty was done by Ignacio Gros, a fellow team member. Check his site if interested.
  • The example is provided as a proof of concept, with no warranty of any kind. Don't use it in production!
  • Look here for ideas with pagination
  • The server side code is mocked by a javascript function. It should be pretty straightforward to plug in something like DWR.
  • The widget is wrapped inside a tag file to be easier to use and deploy in a page
  • It downloads dojo.js from AOL CDN, change if needed
  • Look (use Firebug) to the number of requests when trying to improve performance
So, finally, you can download the WAR from Internna's repository

2 comentarios:

Maine dijo...

While this is a nice tutorial, you might want to also try Dojo grid's virtual scrolling instead of previous/next buttons. It works nicely with server side paging too, so that when you scroll down, it fires requests and fetches data from server in chunks of "rowsPerPage".

Toniez alamo de la mite dijo...

I can't seem to find the code for your applyCellFilter() and grid.sort() functions in the war archives. Can you please tell me where to find it or can you email to toniez@hotmail.com. Thanks!