2 * @class Ext.ux.grid.FiltersFeature
3 * @extends Ext.grid.Feature
5 FiltersFeature is a grid {@link Ext.grid.Feature feature} that allows for a slightly more
6 robust representation of filtering than what is provided by the default store.
8 Filtering is adjusted by the user using the grid's column header menu (this menu can be
9 disabled through configuration). Through this menu users can configure, enable, and
10 disable filters for each column.
14 ##Filtering implementations:##
16 Default filtering for Strings, Numeric Ranges, Date Ranges, Lists (which can be backed by a
17 {@link Ext.data.Store}), and Boolean. Additional custom filter types and menus are easily
18 created by extending {@link Ext.ux.grid.filter.Filter}.
20 ##Graphical Indicators:##
22 Columns that are filtered have {@link #filterCls a configurable css class} applied to the column headers.
24 ##Automatic Reconfiguration:##
26 Filters automatically reconfigure when the grid 'reconfigure' event fires.
30 Filter information will be persisted across page loads by specifying a `stateId`
31 in the Grid configuration.
33 The filter collection binds to the {@link Ext.grid.Panel#beforestaterestore beforestaterestore}
34 and {@link Ext.grid.Panel#beforestatesave beforestatesave} events in order to be stateful.
36 ##GridPanel Changes:##
38 - A `filters` property is added to the GridPanel using this feature.
39 - A `filterupdate` event is added to the GridPanel and is fired upon onStateChange completion.
41 ##Server side code examples:##
43 - [PHP](http://www.vinylfox.com/extjs/grid-filter-php-backend-code.php) - (Thanks VinylFox)</li>
44 - [Ruby on Rails](http://extjs.com/forum/showthread.php?p=77326#post77326) - (Thanks Zyclops)</li>
45 - [Ruby on Rails](http://extjs.com/forum/showthread.php?p=176596#post176596) - (Thanks Rotomaul)</li>
46 - [Python](http://www.debatablybeta.com/posts/using-extjss-grid-filtering-with-django/) - (Thanks Matt)</li>
47 - [Grails](http://mcantrell.wordpress.com/2008/08/22/extjs-grids-and-grails/) - (Thanks Mike)</li>
51 var store = Ext.create('Ext.data.Store', {
58 autoReload: false, //don't reload automatically
59 local: true, //only filter locally
60 // filters may be configured through the plugin,
61 // or in the column definition within the headers configuration
73 dataIndex: 'dateAdded'
77 options: ['extra small', 'small', 'medium', 'large', 'extra large'],
85 var grid = Ext.create('Ext.grid.Panel', {
88 filters: [filtersCfg],
91 bbar: Ext.create('Ext.PagingToolbar', {
96 // a filters property is added to the GridPanel
101 Ext.define('Ext.ux.grid.FiltersFeature', {
102 extend: 'Ext.grid.feature.Feature',
103 alias: 'feature.filters',
105 'Ext.ux.grid.menu.ListMenu',
106 'Ext.ux.grid.menu.RangeMenu',
107 'Ext.ux.grid.filter.BooleanFilter',
108 'Ext.ux.grid.filter.DateFilter',
109 'Ext.ux.grid.filter.ListFilter',
110 'Ext.ux.grid.filter.NumericFilter',
111 'Ext.ux.grid.filter.StringFilter'
115 * @cfg {Boolean} autoReload
116 * Defaults to true, reloading the datasource when a filter change happens.
117 * Set this to false to prevent the datastore from being reloaded if there
118 * are changes to the filters. See <code>{@link updateBuffer}</code>.
122 * @cfg {Boolean} encode
123 * Specify true for {@link #buildQuery} to use Ext.util.JSON.encode to
124 * encode the filter query parameter sent with a remote request.
128 * @cfg {Array} filters
129 * An Array of filters config objects. Refer to each filter type class for
130 * configuration details specific to each filter type. Filters for Strings,
131 * Numeric Ranges, Date Ranges, Lists, and Boolean are the standard filters
135 * @cfg {String} filterCls
136 * The css class to be applied to column headers with active filters.
137 * Defaults to <tt>'ux-filterd-column'</tt>.
139 filterCls : 'ux-filtered-column',
141 * @cfg {Boolean} local
142 * <tt>true</tt> to use Ext.data.Store filter functions (local filtering)
143 * instead of the default (<tt>false</tt>) server side filtering.
147 * @cfg {String} menuFilterText
148 * defaults to <tt>'Filters'</tt>.
150 menuFilterText : 'Filters',
152 * @cfg {String} paramPrefix
153 * The url parameter prefix for the filters.
154 * Defaults to <tt>'filter'</tt>.
156 paramPrefix : 'filter',
158 * @cfg {Boolean} showMenu
159 * Defaults to true, including a filter submenu in the default header menu.
163 * @cfg {String} stateId
164 * Name of the value to be used to store state information.
168 * @cfg {Integer} updateBuffer
169 * Number of milliseconds to defer store updates since the last filter change.
173 // doesn't handle grid body events
174 hasFeatureEvent: false,
178 constructor : function (config) {
181 config = config || {};
182 Ext.apply(me, config);
184 me.deferredUpdate = Ext.create('Ext.util.DelayedTask', me.reload, me);
187 me.filters = Ext.create('Ext.util.MixedCollection', false, function (o) {
188 return o ? o.dataIndex : null;
190 me.filterConfigs = config.filters;
194 attachEvents: function() {
197 headerCt = view.headerCt,
198 grid = me.getGridPanel();
200 me.bindStore(view.getStore(), true);
202 // Listen for header menu being created
203 headerCt.on('menucreate', me.onMenuCreate, me);
205 view.on('refresh', me.onRefresh, me);
208 beforestaterestore: me.applyState,
209 beforestatesave: me.saveState,
210 beforedestroy: me.destroy
213 // Add event and filters shortcut on grid panel
215 grid.addEvents('filterupdate');
219 * @private Create the Filter objects for the current configuration, destroying any existing ones first.
221 createFilters: function() {
223 filterConfigs = me.filterConfigs,
224 hadFilters = me.filters.getCount(),
228 me.saveState(null, state);
231 me.addFilters(Ext.isEmpty(filterConfigs) ? me.view.headerCt.items.items : filterConfigs);
233 me.applyState(null, state);
238 * @private Handle creation of the grid's header menu. Initializes the filters and listens
239 * for the menu being shown.
241 onMenuCreate: function(headerCt, menu) {
244 menu.on('beforeshow', me.onMenuBeforeShow, me);
248 * @private Handle showing of the grid's header menu. Sets up the filter item and menu
249 * appropriate for the target column.
251 onMenuBeforeShow: function(menu) {
256 menuItem = me.menuItem;
257 if (!menuItem || menuItem.isDestroyed) {
258 me.createMenuItem(menu);
259 menuItem = me.menuItem;
262 filter = me.getMenuFilter();
265 menuItem.menu = filter.menu;
266 menuItem.setChecked(filter.active);
267 // disable the menu if filter.disabled explicitly set to true
268 menuItem.setDisabled(filter.disabled === true);
270 menuItem.setVisible(!!filter);
271 this.sep.setVisible(!!filter);
276 createMenuItem: function(menu) {
278 me.sep = menu.add('-');
279 me.menuItem = menu.add({
282 text: me.menuFilterText,
285 checkchange: me.onCheckChange,
286 beforecheckchange: me.onBeforeCheck
291 getGridPanel: function() {
292 return this.view.up('gridpanel');
297 * Handler for the grid's beforestaterestore event (fires before the state of the
299 * @param {Object} grid The grid object
300 * @param {Object} state The hash of state values returned from the StateProvider.
302 applyState : function (grid, state) {
304 this.applyingState = true;
307 for (key in state.filters) {
308 filter = this.filters.get(key);
310 filter.setValue(state.filters[key]);
311 filter.setActive(true);
315 this.deferredUpdate.cancel();
319 delete this.applyingState;
320 delete state.filters;
324 * Saves the state of all active filters
325 * @param {Object} grid
326 * @param {Object} state
329 saveState : function (grid, state) {
331 this.filters.each(function (filter) {
333 filters[filter.dataIndex] = filter.getValue();
336 return (state.filters = filters);
341 * Handler called by the grid 'beforedestroy' event
343 destroy : function () {
345 Ext.destroyMembers(me, 'menuItem', 'sep');
351 * Remove all filters, permanently destroying them.
353 removeAll : function () {
355 Ext.destroy.apply(Ext, this.filters.items);
356 // remove all items from the collection
357 this.filters.clear();
363 * Changes the data store bound to this view and refreshes it.
364 * @param {Store} store The store to bind to this view
366 bindStore : function(store, initial){
367 if(!initial && this.store){
369 store.un('load', this.onLoad, this);
371 store.un('beforeload', this.onBeforeLoad, this);
376 store.on('load', this.onLoad, this);
378 store.on('beforeload', this.onBeforeLoad, this);
387 * Get the filter menu from the filters MixedCollection based on the clicked header
389 getMenuFilter : function () {
390 var header = this.view.headerCt.getMenu().activeHeader;
391 return header ? this.filters.get(header.dataIndex) : null;
395 onCheckChange : function (item, value) {
396 this.getMenuFilter().setActive(value);
400 onBeforeCheck : function (check, value) {
401 return !value || this.getMenuFilter().isActivatable();
406 * Handler for all events on filters.
407 * @param {String} event Event name
408 * @param {Object} filter Standard signature of the event before the event is fired
410 onStateChange : function (event, filter) {
411 if (event !== 'serialize') {
413 grid = me.getGridPanel();
415 if (filter == me.getMenuFilter()) {
416 me.menuItem.setChecked(filter.active, false);
419 if ((me.autoReload || me.local) && !me.applyingState) {
420 me.deferredUpdate.delay(me.updateBuffer);
422 me.updateColumnHeadings();
424 if (!me.applyingState) {
427 grid.fireEvent('filterupdate', me, filter);
433 * Handler for store's beforeload event when configured for remote filtering
434 * @param {Object} store
435 * @param {Object} options
437 onBeforeLoad : function (store, options) {
438 options.params = options.params || {};
439 this.cleanParams(options.params);
440 var params = this.buildQuery(this.getFilterData());
441 Ext.apply(options.params, params);
446 * Handler for store's load event when configured for local filtering
447 * @param {Object} store
448 * @param {Object} options
450 onLoad : function (store, options) {
451 store.filterBy(this.getRecordFilter());
456 * Handler called when the grid's view is refreshed
458 onRefresh : function () {
459 this.updateColumnHeadings();
463 * Update the styles for the header row based on the active filters
465 updateColumnHeadings : function () {
467 headerCt = me.view.headerCt;
469 headerCt.items.each(function(header) {
470 var filter = me.getFilter(header.dataIndex);
471 header[filter && filter.active ? 'addCls' : 'removeCls'](me.filterCls);
477 reload : function () {
479 store = me.view.getStore(),
483 store.clearFilter(true);
484 store.filterBy(me.getRecordFilter());
486 me.deferredUpdate.cancel();
492 * Method factory that generates a record validator for the filters active at the time
496 getRecordFilter : function () {
498 this.filters.each(function (filter) {
505 return function (record) {
506 for (i = 0; i < len; i++) {
507 if (!f[i].validateRecord(record)) {
516 * Adds a filter to the collection and observes it for state change.
517 * @param {Object/Ext.ux.grid.filter.Filter} config A filter configuration or a filter object.
518 * @return {Ext.ux.grid.filter.Filter} The existing or newly created filter object.
520 addFilter : function (config) {
521 var Cls = this.getFilterClass(config.type),
522 filter = config.menu ? config : (new Cls(config));
523 this.filters.add(filter);
525 Ext.util.Observable.capture(filter, this.onStateChange, this);
530 * Adds filters to the collection.
531 * @param {Array} filters An Array of filter configuration objects.
533 addFilters : function (filters) {
536 for (i = 0, len = filters.length; i < len; i++) {
538 // if filter config found add filter for the column
540 this.addFilter(filter);
547 * Returns a filter for the given dataIndex, if one exists.
548 * @param {String} dataIndex The dataIndex of the desired filter object.
549 * @return {Ext.ux.grid.filter.Filter}
551 getFilter : function (dataIndex) {
552 return this.filters.get(dataIndex);
556 * Turns all filters off. This does not clear the configuration information
557 * (see {@link #removeAll}).
559 clearFilters : function () {
560 this.filters.each(function (filter) {
561 filter.setActive(false);
566 * Returns an Array of the currently active filters.
567 * @return {Array} filters Array of the currently active filters.
569 getFilterData : function () {
570 var filters = [], i, len;
572 this.filters.each(function (f) {
574 var d = [].concat(f.serialize());
575 for (i = 0, len = d.length; i < len; i++) {
587 * Function to take the active filters data and build it into a query.
588 * The format of the query depends on the <code>{@link #encode}</code>
590 * <div class="mdetail-params"><ul>
592 * <li><b><tt>false</tt></b> : <i>Default</i>
593 * <div class="sub-desc">
594 * Flatten into query string of the form (assuming <code>{@link #paramPrefix}='filters'</code>:
596 filters[0][field]="someDataIndex"&
597 filters[0][data][comparison]="someValue1"&
598 filters[0][data][type]="someValue2"&
599 filters[0][data][value]="someValue3"&
602 * <li><b><tt>true</tt></b> :
603 * <div class="sub-desc">
604 * JSON encode the filter data
606 filters[0][field]="someDataIndex"&
607 filters[0][data][comparison]="someValue1"&
608 filters[0][data][type]="someValue2"&
609 filters[0][data][value]="someValue3"&
613 * Override this method to customize the format of the filter query for remote requests.
614 * @param {Array} filters A collection of objects representing active filters and their configuration.
615 * Each element will take the form of {field: dataIndex, data: filterConf}. dataIndex is not assured
616 * to be unique as any one filter may be a composite of more basic filters for the same dataIndex.
617 * @return {Object} Query keys and values
619 buildQuery : function (filters) {
620 var p = {}, i, f, root, dataPrefix, key, tmp,
621 len = filters.length;
624 for (i = 0; i < len; i++) {
626 root = [this.paramPrefix, '[', i, ']'].join('');
627 p[root + '[field]'] = f.field;
629 dataPrefix = root + '[data]';
630 for (key in f.data) {
631 p[[dataPrefix, '[', key, ']'].join('')] = f.data[key];
636 for (i = 0; i < len; i++) {
644 // only build if there is active filter
646 p[this.paramPrefix] = Ext.JSON.encode(tmp);
653 * Removes filter related query parameters from the provided object.
654 * @param {Object} p Query parameters that may contain filter related fields.
656 cleanParams : function (p) {
657 // if encoding just delete the property
659 delete p[this.paramPrefix];
660 // otherwise scrub the object of filter data
663 regex = new RegExp('^' + this.paramPrefix + '\[[0-9]+\]');
665 if (regex.test(key)) {
673 * Function for locating filter classes, overwrite this with your favorite
674 * loader to provide dynamic filter loading.
675 * @param {String} type The type of filter to load ('Filter' is automatically
676 * appended to the passed type; eg, 'string' becomes 'StringFilter').
677 * @return {Class} The Ext.ux.grid.filter.Class
679 getFilterClass : function (type) {
680 // map the supported Ext.data.Field type values into a supported filter
693 return Ext.ClassManager.getByAlias('gridfilter.' + type);