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.

Resources