Rendering a Summary Row
Grids are often used to display numeric data, in which case it can be
desirable to display an extra row containing total values for each column.
In this tutorial, we will walk through how to write a mixin which adds a
summary or totals row to a grid's footerNode
for this purpose.
The Footer Region
All dgrid instances, whether List
or Grid
, possess
a footerNode
. This node represents an area beneath the
body of the grid (the bodyNode
). By default this area contains
nothing and remains hidden, but extensions (such as Pagination
)
may add to it and show it by setting showFooter
to true
.
Therefore, to begin implementing a summary row mixin, we will want to show the
footer and add an element to the footerNode
to hold the table
we will render:
define([ 'dojo/_base/declare', 'dojo/_base/lang', 'dojo/dom-construct' ], function (declare, lang, domConstruct) { return declare(null, { showFooter: true, buildRendering: function () { this.inherited(arguments); var areaNode = this.summaryAreaNode = domConstruct.create('div', { className: 'summary-row', role: 'row', style: { overflow: 'hidden' } }, this.footerNode); } }); });
Creating a Custom Setter
Now we have an area to render the summary, but we haven't actually rendered
a table or row, because we don't have data for it yet.
Let's define a custom setter for a summary
property to
render data for the summary row:
_setSummary: function (data) { var tableNode = this.summaryTableNode; this.summary = data; // Remove any previously-rendered summary row if (tableNode) { domConstruct.destroy(tableNode); } // Render row here... this.summaryAreaNode.appendChild(tableNode); }
Note that dgrid setters follow the naming convention _setFoo
,
which is unique from Dijit's convention.
Populating the Summary Row
Now we've created a setter method, but we haven't yet added code to populate
the summary row. We can actually reuse Grid
's
createRowCells
method to help us accomplish this.
createRowCells
accepts a tag name ('th'
or
'td'
) and a function that will be applied for each column defined
in the grid's structure. Let's create a function to pass to
createRowCells
, and call it within the custom setter:
_renderSummaryCell: function (item, cell, column) { var value = item[column.field] || ''; cell.appendChild(document.createTextNode(value)); }, _setSummary: function (data) { var tableNode = this.summaryTableNode; this.summary = data; // Remove any previously-rendered summary row if (tableNode) { domConstruct.destroy(tableNode); } // Render row, calling _renderSummaryCell for each cell tableNode = this.summaryTableNode = this.createRowCells('td', lang.hitch(this, '_renderSummaryCell', data)); this.summaryAreaNode.appendChild(tableNode); }
An additional benefit of reusing createRowCells
is that it will
automatically work for ColumnSet
structures as well.
Additional Considerations
While the above code works well for set
calls after a grid is
created, it would also be useful to be able to include summary
in the initial arguments passed to the constructor. Unlike Dijit, dgrid
does not call custom setters automatically during the creation process,
so we need to do so specifically when we want it to be called:
buildRendering: function () { this.inherited(arguments); var areaNode = this.summaryAreaNode = // ... (existing code omitted) // Process any initially-passed summary data if (this.summary) { this._setSummary(this.summary); } },
We also need to take some layout considerations into account. Firstly, it is possible for a grid's columns to extend beyond its width. We want our columns to align with the grid's at all times, but the footer area itself is normally fixed.
Since the way we've rendered the summary row already matches the full width
of the rows in the body, we can account for scrolling simply by adding a
scroll
listener:
buildRendering: function () { this.inherited(arguments); var areaNode = this.summaryAreaNode = // ... (existing code omitted) // Keep horizontal alignment in sync this.on('scroll', lang.hitch(this, function () { areaNode.scrollLeft = this.getScrollPosition().x; })); // Process any initially-passed summary data // ... (existing code omitted) },
One last layout consideration we need to account for is that when the
summary area is updated, its new contents may flow differently than its
previous contents, which could cause the height of the footerNode
to change. We can account for this by making sure to call resize
after updating the summary row (taking care not to do so if the grid hasn't
been started up yet):
_setSummary: function (data) { var tableNode = this.summaryTableNode; this.summary = data; // ... (existing code omitted) this.summaryAreaNode.appendChild(tableNode); // Force resize processing, // in case summary row's height changed if (this._started) { this.resize(); } }
Putting it All Together
Here is the entire module for reference, with comments included:
define([ 'dojo/_base/declare', 'dojo/_base/lang', 'dojo/dom-construct' ], function (declare, lang, domConstruct) { return declare(null, { // summary: // A mixin for dgrid components which renders // a row with summary information (e.g. totals). // Show the footer area, which will hold the summary row showFooter: true, buildRendering: function () { this.inherited(arguments); var areaNode = this.summaryAreaNode = domConstruct.create('div', { className: 'summary-row', role: 'row', style: { overflow: 'hidden' } }, this.footerNode); // Keep horizontal alignment in sync this.on('scroll', lang.hitch(this, function () { areaNode.scrollLeft = this.getScrollPosition().x; })); // Process any initially-passed summary data if (this.summary) { this._setSummary(this.summary); } }, _updateColumns: function () { this.inherited(arguments); if (this.summary) { // Re-render summary row for existing data, // based on new structure this._setSummary(this.summary); } }, _renderSummaryCell: function (item, cell, column) { // summary: // Simple method which outputs data for each // requested column into a text node in the // passed cell element. Honors columns' // get, formatter, and renderCell functions. // renderCell is called with an extra flag, // so custom implementations can react to it. var value = item[column.field] || ''; cell.appendChild(document.createTextNode(value)); }, _setSummary: function (data) { // summary: // Given an object whose keys map to column IDs, // updates the cells in the footer row with the // provided data. var tableNode = this.summaryTableNode; this.summary = data; // Remove any previously-rendered summary row if (tableNode) { domConstruct.destroy(tableNode); } // Render summary row // Call _renderSummaryCell for each cell tableNode = this.summaryTableNode = this.createRowCells('td', lang.hitch(this, '_renderSummaryCell', data)); this.summaryAreaNode.appendChild(tableNode); // Force resize processing, // in case summary row's height changed if (this._started) { this.resize(); } } }); });View Demo
You can double-click the mixin code above to select all of it for easy copying.
Conclusion
The above example should provide a good starting point for adding and
supplying data to a summary row displayed in a grid's footer region.
This mixin could be expanded further in various ways, such as to display
multiple rows within the summary, or to account for columns'
get
, formatter
, or renderCell
methods.