Must read!
http://gridsearch.extjs.eu/
http://www.sencha.com/forum/showthread.php?23615-Grid-Search-Plugin&highlight=page bar
http://www.sencha.com/forum/showthread.php?14503-Grid-Filter-(Plugin)&highlight=page bar
http://www.sencha.com/forum/showthread.php?41658-Grid-header-filters
http://www.vinylfox.com/grid-filter-php-backend-code/
Even better!!
http://triin.net/temp/filter-row/
http://github.com/nene/filter-row
/*!
* Ext JS FilterRow plugin v0.5
* http://github.com/nene/filter-row
*
* Copyright 2010 Rene Saarsoo
* Licensed under GNU General Public License v3.
* http://www.gnu.org/licenses/
*/
Ext.namespace('Ext.ux.grid');
/
* @class Ext.ux.grid.FilterRow
* @extends Ext.util.Observable
*
* Grid plugin that adds filtering row below grid header.
*
*
To add filtering to column, define "filter" property in column
* to be FilterRowFilter configuration object or an instance of it.
*
*
Example:
*
* pre/code
var grid = new Ext.grid.GridPanel({
columns: [
{
header: 'Name',
dataIndex: 'name',
// Filter by regular expression
// {0} will be substituted with current field value
filter: {
test: "/{0}/i"
}
},
{
header: 'Age',
dataIndex: 'age',
filter: {
// Show larger ages than the one entered to field
test: function(filterValue, value) {
return value > filterValue;
}
}
}
],
plugins: ["filterrow"],
...
});
* /code/pre
*/
Ext.ux.grid.FilterRow = Ext.extend(Ext.util.Observable, {
/**
* @cfg {Boolean} autoFilter
* false, to turn automatic filtering off. (default true)
*/
autoFilter: true,
/
* @cfg {Boolean} refilterOnStoreUpdate
* true to refilter store when records added/removed. (default false)
*/
refilterOnStoreUpdate: false,
constructor: function(conf) {
Ext.apply(this, conf || {});
this.addEvents(
/
* @event change
* Fired when any one of the fields is changed.
* @param {Object} filterValues object containing values of all
* filter-fields. When column has "id" defined, then property
* with that ID will hold filter value. When no "id" defined,
* then dataIndexes are used. That is, you only need to specify
* ID-s for columns, when two filters use the same dataIndex.
*/
"change"
);
if (this.listeners) {
this.on(this.listeners);
}
},
init: function(grid) {
this.grid = grid;
var cm = grid.getColumnModel();
var view = grid.getView();
// For some reason GridView was changed in Ext 3.3 to completely
// re-render grid header on store "datachanged" event (which is
// fired after each loading/filtering/sorting). Because this
// re-rendering seems completely unnecessary and coding around it
// quite hard (each time user types a character into field we have
// to re-insert fields to the header and recover the lost focus,
// which I couldn't get working with IE), I've decided to just
// override the onDataChange method with Ext 3.2 version.
// See also: http://www.sencha.com/forum/showthread.php?118510
view.onDataChange = function() {
this.refresh(); // this was: this.refresh(true);
this.updateHeaderSortState();
this.syncFocusEl(0);
};
// convert all filter configs to FilterRowFilter instances
var Filter = Ext.ux.grid.FilterRowFilter;
this.eachFilterColumn(function(col) {
if (!(col.filter instanceof Filter)) {
col.filter = new Filter(col.filter);
}
col.filter.on("change", this.onFieldChange, this);
});
this.applyTemplate();
// add class for attatching plugin specific styles
grid.addClass('filter-row-grid');
// when grid initially rendered
grid.on("render", this.renderFields, this);
// when Ext grid state restored (untested)
grid.on("staterestore", this.resetFilterRow, this);
// when the width of the whole grid changed
grid.on("resize", this.resizeAllFilterFields, this);
// when column width programmatically changed
cm.on("widthchange", this.onColumnWidthChange, this);
// Monitor changes in column widths
// newWidth will contain width like "100px", so we use parseInt to get rid of "px"
view.onColumnWidthUpdated = view.onColumnWidthUpdated.createSequence(function(colIndex, newWidth) {
this.onColumnWidthChange(this.grid.getColumnModel(), colIndex, parseInt(newWidth, 10));
}, this);
// when column is moved, remove fields, after the move add them back
cm.on("columnmoved", this.resetFilterRow, this);
view.afterMove = view.afterMove.createSequence(this.renderFields, this);
// when column header is renamed, remove fields, afterwards add them back
cm.on("headerchange", this.resetFilterRow, this);
view.onHeaderChange = view.onHeaderChange.createSequence(this.renderFields, this);
// When column hidden or shown
cm.on("hiddenchange", this.onColumnHiddenChange, this);
if (this.refilterOnStoreUpdate) {
this.respectStoreFilter();
}
},
// Makes store add() and load() methods to respect filtering.
respectStoreFilter: function() {
var store = this.grid.getStore();
// re-apply filter after store load
store.on("load", this.refilter, this);
// re-apply filter after adding stuff to store
this.refilterAfter(store, "add");
this.refilterAfter(store, "addSorted");
this.refilterAfter(store, "insert");
},
// Appends refiltering action to after store method
refilterAfter: function(store, method) {
var filterRow = this;
store[method] = store[method].createSequence(function() {
if (this.isFiltered()) {
filterRow.refilter();
}
});
},
onColumnHiddenChange: function(cm, colIndex, hidden) {
var filterDiv = Ext.get(this.getFilterDivId(cm.getColumnId(colIndex)));
if (filterDiv) {
filterDiv.parent().dom.style.display = hidden ? 'none' : '';
}
this.resizeAllFilterFields();
},
applyTemplate: function() {
var colTpl = "";
this.eachColumn(function(col) {
var filterDivId = this.getFilterDivId(col.id);
var style = col.hidden ? " style='display:none'" : "";
var icon = (col.filter && col.filter.showFilterIcon) ? "filter-row-icon" : "";
colTpl = '<td' style="" =""></td'></p><div class="x-small-editor ' icon '" id="' filterDivId '"></div></pre></td>';
});
var headerTpl = new Ext.Template(
'<table border="0" cellspacing="0" cellpadding="0" style="{tstyle}">',
'<thead><tr class="x-grid3-hd-row">{cells}>/tr>>/thead>',
'<tbody><tr class="filter-row-header">',
colTpl,
'</tr></tbody>',
"</table>"
);
var view = this.grid.getView();
Ext.applyIf(view, { templates: {} });
view.templates.header = headerTpl;
},
// Removes filter fields from grid header and recreates
// template. The latter is needed in case columns have been
// reordered.
resetFilterRow: function() {
this.eachFilterColumn(function(col) {
var editor = col.filter.getField();
if (editor && editor.rendered) {
var el = col.filter.getFieldDom();
el.parentNode.removeChild(el);
}
});
this.applyTemplate();
},
renderFields: function() {
this.eachFilterColumn(function(col) {
var filterDiv = Ext.get(this.getFilterDivId(col.id));
var editor = col.filter.getField();
editor.setWidth(col.width - 2);
if (editor.rendered) {
filterDiv.appendChild(col.filter.getFieldDom());
}
else {
editor.render(filterDiv);
}
});
},
onFieldChange: function() {
if (this.hasListener("change")) {
this.fireEvent("change", this.getFilterData());
}
if (this.autoFilter) {
this.grid.getStore().filterBy(this.getFilterFunction());
}
},
// refilters the store with current filter.
refilter: function() {
this.grid.getStore().filterBy(this.getFilterFunction());
},
// collects values from all filter-fields into hash that maps column
// dataindexes (or id-s) to filter values.
getFilterData: function() {
var data = {};
this.eachFilterColumn(function(col) {
// when column id is numeric, assume it's autogenerated and use
// dataIndex. Otherwise assume id is user-defined and use it.
var name = (typeof col.id === "number") ? col.dataIndex : col.id;
data[name] = col.filter.getFieldValue();
});
return data;
},
/**
* Returns store filtering function for the current values in filter
* fields.
*
* @return {Function} function to use with store.filterBy()
*/
getFilterFunction: function() {
var tests = [];
this.eachFilterColumn(function(col) {
var p = col.filter.createPredicate(col.dataIndex);
if (p) {
tests.push(p);
}
});
return function(record) {
for (var i=0; ii)="" false;="" true;="" };="" oncolumnwidthchange:="" function(cm,="" colindex,="" newwidth)="" col="cm.getColumnById(cm.getColumnId(colIndex));" newwidth);="" forcefit:="" true,="" then="" all="" columns="" be="" when="" grid="" resized="" added="" removed.="" resizeallfilterfields:="" function()="" cm="this.grid.getColumnModel();" this.eachfiltercolumn(function(col,="" this.resizefilterfield(col,="" cm.getcolumnwidth(i));="" resizes="" according="" width="" resizefilterfield:="" function(column,="" newcolumnwidth)="" var="" editor="column.filter.getField();" editor.setwidth(newcolumnwidth="" 2);="" returns="" html="" id="" element="" containing="" div="" getfilterdivid:="" function(columnid)="" return="" this.grid.id="" -filter-="" columnid;="" that="" has="" eachfiltercolumn:="" this.eachcolumn(function(col,="" i)="" if="" (col.filter)="" func.call(this,="" col,="" i);="" },="" iterates="" over="" in="" array="" eachcolumn:="" function(func)="" ext.each(this.grid.getcolumnmodel().config,="" func,="" this);="" }="" });="" ext.preg(="" ,="" ext.ux.grid.filterrow);="" @class="" @extends="" ext.util.observable="" class="" encapsulates="" definition="" column.="" ext.ux.grid.filterrowfilter="" {="" {ext.form.field}="" instance="" some="" form="" field="" use="" for="" filtering,="" or="" just="" a="" config="" object="" -="" xtype="" .="" defaults="" textfield="" enablekeyevents="" set="" true.="" field:="" undefined,="" {[string]}="" fieldevents="" names="" listen="" from="" field.="" each="" time="" one="" of="" events="" heard,="" filterrow="" will="" filter="" grid.="" by="" it="" contains="" event="" to="" provide="" useful="" together="" with="" the="" default="" textfield.="" fieldevents:="" [="" keyup="" ],="" **="" @cfg="" {string="" function}="" test="" determines="" how="" this="" column="" is="" filtered.="" *="">
When it's a string like "/^{0}/i", a regular expression filter
* is created - substituting "{0}" with current value from field.
*
*
When it's a function, it will be called with three arguments:
*
*
*
filterValue - the current value of field,
*
value - the value from record at dataIndex,
*
record - the record object itself.
*
*
*
When function returns true, the row will be filtered in,
* otherwise excluded from grid view.
*
*
Defaults to "/{0}/i".
*/
test: "/{0}/i",
/**
* @cfg {Object} scope
* Scope for the test function.
*/
scope: undefined,
/
* @cfg {Boolean} showFilterIcon
* By default a magnifier-glass icon is shown inside filter field.
* Set this to false, to disable that behaviour. (Default is true.)
*/
showFilterIcon: true,
constructor: function(config) {
Ext.apply(this, config);
if (!this.field) {
this.field = new Ext.form.TextField({enableKeyEvents: true});
}
else if (!(this.field instanceof Ext.form.Field)) {
this.field = Ext.create(this.field, "textfield");
}
this.addEvents(
/
* @event change
* Fired when ever one of the events listed in "events" config
* option is fired by field.
*/
"change"
);
Ext.each(this.fieldEvents, function(event) {
this.field.on(event, this.fireChangeEvent, this);
}, this);
},
fireChangeEvent: function() {
this.fireEvent("change");
},
/**
* Returns the field of this filter.
*
* @return {Ext.form.Field}
*/
getField: function() {
return this.field;
},
/
* Returns DOM Element that is the root element of form field.
*
*
For most fields, this will be the "el" property, but
* TriggerField and it's descendants will wrap "el" inside another
* div called "wrap".
*
* @return {HTMLElement}
*/
getFieldDom: function() {
return this.field.wrap ? this.field.wrap.dom : this.field.el.dom;
},
/**
* Returns the value of filter field.
*
* @return {Anything}
/
getFieldValue: function() {
return this.field.getValue();
},
/**
* Creates predicate function for filtering the column associated
* with this filter.
*
* @param {String} dataIndex
* @return {Function}
/
createPredicate: function(dataIndex) {
var test = this.test;
var filterValue = this.field.getValue();
// is test a regex string?
if (typeof test === "string" && test.match(/^\\/.\\/[img]$/)) {
return this.createRegExpPredicate(test, filterValue, dataIndex);
}
else {
// otherwise assume it's a function
var scope = this.scope;
return function(r) {
return test.call(scope, filterValue, r.get(dataIndex), r);
};
}
},
createRegExpPredicate: function(reString, filterValue, dataIndex) {
// don't filter the column at all when field is empty
if (!filterValue) {
return false;
}
var regex = this.createRegExp(reString, filterValue);
return function(r) {
return regex.test(r.get(dataIndex));
};
},
// Given string "/^{0}/i" and value "foo" creates regex: /^foo/i
createRegExp: function(reString, value) {
// parse the reString into pattern and flags
var m = reString.match(/^\\/(.)\\/([img])$/);
var pattern = m[1];
var flags = m[2];
// Create new RegExp substituting value inside pattern
return new RegExp(String.format(pattern, Ext.escapeRe(value)), flags);
}
});
Ext.onReady(function() {
var store = new Ext.data.JsonStore({
url: "getdata.php",
root: "rows",
baseParams: {limit: 10},
autoLoad: true,
fields: [
{name: 'company'},
{name: 'price'},
{name: 'change'}
]
});
// Custom filtering with remote store.
// Each time filers change, send values of each filter field to
// server and reload store with the result.
var filterRow = new Ext.ux.grid.FilterRow({
autoFilter: false,
listeners: {
change: function(data) {
store.load({
params: data
});
}
}
});
var grid = new Ext.grid.GridPanel({
store: store,
columns: [
{
id: 'company',
header: 'Company',
width: 160,
sortable: true,
dataIndex: 'company',
filter: {
}
},
{
header: 'Price',
width: 75,
sortable: true,
renderer: 'usMoney',
align: "right",
dataIndex: 'price',
filter: {
}
},
{
header: 'Change',
width: 75,
sortable: true,
dataIndex: 'change',
filter: {
// hide filtering icon, just to demonstrate the possibility
showFilterIcon: false
}
}
],
plugins: [filterRow],
stripeRows: true,
autoExpandColumn: 'company',
height: 350,
width: 450,
title: 'Filtering with remote store',
renderTo: "remote-grid-container"
});
});
Ext.onReady(function() {
var Company = Ext.data.Record.create([
'company',
'price',
'change'
]);
var store = new Ext.data.JsonStore({
url: "getdata.php",
root: "rows",
autoLoad: true,
fields: Company,
sortInfo: {field: "company", direction: 'ASC'}
});
store.load();
var filterRow = new Ext.ux.grid.FilterRow({
// automatically refilter store when records are added
refilterOnStoreUpdate: true
});
var grid = new Ext.grid.GridPanel({
store: store,
columns: [
{
id: 'company',
header: 'Company',
width: 160,
sortable: true,
dataIndex: 'company',
filter: {
// Filter by string beginnings,
// the default is to filter by occurance ("/{0}/i")
test: "/^{0}/i"
}
},
{
header: 'Price',
width: 75,
sortable: true,
renderer: 'usMoney',
align: "right",
dataIndex: 'price',
filter: {
// find prices greater than filter value
test: function(filterValue, value, record) {
return value >= filterValue;
}
}
},
{
header: 'Change',
width: 75,
sortable: true,
dataIndex: 'change',
filter: {
field: {
xtype: "combo",
mode: 'local',
store: new Ext.data.ArrayStore({
id: 0,
fields: [
'value'
],
data: [['-'], ['up'], ['none'], ['down']]
}),
valueField: 'value',
displayField: 'value',
triggerAction: 'all',
value: "-"
},
fieldEvents: ["select"],
test: function(filterValue, value, record) {
return filterValue === "-" || filterValue === value;
}
}
}
],
plugins: [filterRow],
stripeRows: true,
autoExpandColumn: 'company',
height: 350,
width: 450,
title: 'Filtering with local store',
renderTo: "local-grid-container",
bbar: [
{
text: "Reload",
handler: function() {
store.load();
}
},
{
text: "Add",
handler: function() {
store.addSorted(new Company({
company: "Google Inc",
price: 150.7,
change: "up"
}));
}
}
]
});
// For testing that column header renaming works
document.getElementById("button").onclick = function() {
grid.getColumnModel().setColumnHeader(1, "Hello");
};
});