Dojo(x) Grid Plugin — AutoFilter
The Dojo(x) Grid widget is fabulous. It makes creating incredibly fast, functional tables a breeze.
But periodically, you want to extend the Grid. For example, right now I have a need for “auto-filter”-like capability for a grid. Right click on a header row, show a dropdown of values, and let me filter the grid on that value. This isn’t a universal need, but certainly one that has been asked for on the dojo forum.
Now, aside from actually building the auto-filter, I’d like to propose creating a way of systematically extending Grid. Perhaps a concept of “plugins” would work. I could see something like this:
<div dojoType="dojox.Grid" store="store"> <div dojoType="dojox.Grid.AutoFilterPlugin"></div> </div>
Each plugin could decide how it incorporated itself into the grid. Mine, for example adds an optional attribute to each column in your structure called “autoFilter” which, if you set to true, makes autoFiltering just work if you are using a dojo.data store. And if you aren’t you simply provide a hook to your own custom “filter()” method, and it still works.
In any case, since the above declarative style plugin capability doesn’t yet exist (though I am thinking hard about how to do it), my AutoFilterPlugin is actually a mixin you use in declaring your own subclass of Grid like so:
dojo.declare("MyGrid", [dojox.grid.DataGrid, medryx.widgets._GridPlugins.AutoFilter], {});
Then you can create your layout with the autoFilter field:
var layout = [ {name: 'Alpha', field: 'col1', autoFilter:true}, {name: 'Beta', field: 'col2'}, {name: 'Gamma', field: 'col3'}, {name: 'Delta', field: 'col4', width: "150px", autoFilter:true}, {name: 'Epsilon', field: 'col5'}, {name: 'Nexilon', field: 'col6'}, {name: 'Zeta', field: 'col7'}, {name: 'Eta', field: 'col1'}, {name: 'Omega', field: 'col8'} ];
The rest of the usage is the same, and you end up with a Grid that looks like this:

A little explanation on how this works: The plugin over-rides postCreate and connects the onHeaderContextMenu event to a showAutoFilterDialog method in the plugin. That method opens a TooltipDialog with the select list of values. Chaning the value on the select list fires updateFilterValue which in turn first this.filter. The DataGrid already has a filter() method. If you are not using DataGrid, you need to mix in a method called filter() as well (in the dojo.declare of your subclass is where this would go).
How does the tooltip know what the values of the column are? This was the trickiest bit to figure out. Because the get() method is called from the context of a cell, not from the context of the grid, you couldn’t do a simple over-ride with a call to this.inherited(arguments) . That took a while to figure out. So instead, I create the modified get() method in my postCreate method. This runs the original get() and then stores the value before returning. Of course, this implies that you can only filter on values that the grid itself knows about and has rendered. For me that is fine. You could always create a different way of getting your filter values (for example by querying you store) if you wanted to.
So, if you understand any of that mess, I applaud you. And without further ado… here is the code:
/** * This is an aspect to your grid that will add in AutoFilter ability to the grid * You must provide the filter function for your grid. If will be called with an object * { * fieldName:filterValue, * fieldName2:filterValue2 * * } * to attach this plugin to your grid you mix this in to your custom grid (last) * and then define a filter() method * * TODO: [Blank] and multiple filters is still a little buggy * * @author maulin */ dojo.provide("medryx.widgets._GridPlugins.AutoFilter"); dojo.require("dojox.lang.aspect"); dojo.require("medryx.widgets._GridPlugins._AutoFilterTooltip"); dojo.declare("medryx.widgets._GridPlugins.AutoFilter", null, { constructor:function() { this._filterableValues = []; }, /** * return a list of values that this column can be filter by, * sorted alphabetically * @param {Object} column */ getFilterableValues: function(column) { var values = this._filterableValues[column.index]; return this.cleanFilterValues(values); }, cleanFilterValues:function(values) { var unique = {}; //get rid of duplicates return dojo.filter(values, function(value) { if (!unique[value]) { unique[value] = true; return true; } return false; }).sort(); }, /** * add a string value to the list of all filterable values * for a column * @param {Object} column * @param {Object} value */ addFilterableValue:function(column, value) { var columnIndex = column.index; if (!this._filterableValues[columnIndex]) { this._filterableValues[columnIndex] = []; } if (typeof(value) !== 'undefined' && value !== null) { this._filterableValues[columnIndex].push(value); } }, updateFilter:function(filter) { if (this.filter) { this.filter(filter, true); } }, updateFilterValue:function(column, value) { var filterField = {}; filterField[column.field] = value; if (!this.filterFields) { this.filterFields = {}; } if (filterField) { dojo.mixin(this.filterFields, filterField); //makes any other filters "sticky" } this.updateFilter(this.filterFields); }, getTooltip:function(column) { if (!this.tooltips) { this.tooltips = []; } if (!this.tooltips[column.index]) { this.tooltips[column.index] = new medryx.widgets._GridPlugins._AutoFilterTooltip({column:column.name}); dojo.connect(this.tooltips[column.index], "onValueChanged", dojo.hitch(this, "updateFilterValue", column)); } return this.tooltips[column.index]; }, /** * set "autoFilter:true" in your cell properties to get this to work * @param {Object} e */ showFilterDialog:function(e) { if (e.cell.autoFilter) { this.getTooltip(e.cell).show(e.pageX, e.pageY, this.getFilterableValues(e.cell)); } }, postCreate:function() { var originalGet = this.get; this.get = function(inRowIndex) { var value = originalGet.apply(this, arguments); this.grid.addFilterableValue(this, value); return value; }; dojo.connect(this, "onHeaderContextMenu", this, "showFilterDialog"); return this.inherited(arguments); }, toString:function() { return "AutoFilter Plugin"; }, });
And for the AutoFilterTooltip:
/** * Used by AutoFilter as a tooltip to set the filter value for a column. * Pass the values and coords and this will fire onValueSelected with * a new filter value * * @author maulin */ dojo.provide("medryx.widgets._GridPlugins._AutoFilterTooltip"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.require("dijit.Dialog"); //TODO: upgrade select to ComboBox dojo.declare("medryx.widgets._GridPlugins._AutoFilterTooltip", [dijit._Widget, dijit._Templated], { templatePath: dojo.moduleUrl("medryx","widgets/_GridPlugins/_AutoFilterTooltip.html"), widgetsInTemplate:true, column:"Column", onValueChanged:function(value) { }, _onChange:function(e) { if (this.values.selectedIndex === -1) { value = null; } else { value = this.values.options[this.values.selectedIndex].value; } this.onValueChanged(value); this.currentValue = value; this.close(); }, /** * set the values of the selector * @param {Object} values, array of strings */ updateValues:function(values) { var sel = this.values; sel.options.length = 0;//reset the options array sel.options[0] = new Option("Any", "*"); var currentValue = typeof(this.currentValue) !== 'undefined' ? this.currentValue : "*"; var selectedIndex = 0; dojo.forEach(values, function(value) { if (value === currentValue) { selectedIndex = sel.options.length; } if (value) sel.options[sel.options.length] = new Option(value, value); }) if (currentValue == "") { selectedIndex = sel.options.length; } if (currentValue === "*") { selectedIndex = 0; } sel.options[sel.options.length] = new Option("[Blank]", ""); sel.selectedIndex = selectedIndex; }, /** * show this dialog at the given coordinates, values * @param {Object} x * @param {Object} y */ show:function(x, y, values) { this.updateValues(values); dijit.popup.open({popup:this.tooltip, x:x, y:y}); }, close:function() { dijit.popup.close(this.tooltip); } })
Please post _AutoFilterTooltip.html
Comment by Don — August 9, 2008 @ 5:01 am
<div class="autoFilterTooltip" dojoAttachPoint="containerNode"> <div dojoType="dijit.TooltipDialog" title="AutoFilter" dojoAttachEvent="_onBlur:close" dojoAttachPoint="tooltip"> <span>${column}:</span> <select dojoAttachPoint="values" dojoAttachEvent="onchange:_onChange"></select> </div> </div>Comment by maulin — August 9, 2008 @ 5:26 am
[...] found an interesting chunk of code over here on a blog post that’s about a different problem being solved with [...]
Pingback by Jon Sykes » Blog Archive » Unique Array Filter in Dojo — August 16, 2008 @ 7:35 am
Have you tried doing this with the new dojo grid in 1.2?
Josh
Comment by Josh Trutwin — October 18, 2008 @ 1:49 am