Ext JS 4: Filtering a TreeStore - extjs

I originally posted this on the Sencha forums here but didn't get any responses (other than my own answer, which I will post soon), so I am going to repost it here and see if I get anymore help.
I've been racking my brain on how to filter a TreeStore in 4.0.7. I've tried the following:
The model
Ext.define('model', {
extend: 'Ext.data.Model',
fields: [
{name: 'text', type: 'string'},
{name: 'leaf', type: 'bool'},
{name: 'expanded', type: 'bool'},
{name: 'id', type: 'string'}
],
hasMany: {model: 'model', name: 'children'}
});
The store
Ext.define('myStore', {
extend: 'Ext.data.TreeStore',
model: 'model',
storeId: 'treestore',
root: {
text: 'root',
children: [{
text: 'leaf1',
id: 'leaf1',
children: [{
text: 'child1',
id: 'child1',
leaf: true
},{
text: 'child2',
id: 'child2',
leaf: true
}]
},{
text: 'leaf2',
id: 'leaf2',
leaf: true
}]
},
proxy: {
type: 'memory',
reader: {
type: 'json'
}
}
});
The tree
var myTree = Ext.create('Ext.tree.Panel', {
id: 'myTree',
selType: 'cellmodel',
selModel: Ext.create('Ext.selection.CellModel', {mode: 'MULTI'}),
rootVisible: false,
store: Ext.create('myStore'),
width: 300
});
The filter
var filter = Ext.create('Ext.util.Filter', {
filterFn: function(item) {
return item.data.text == 'leaf1';
}
});
So I think my problem is... I don't know how to use this filter due to TreeStore not actually inheriting any type of filter functions like a normal store. I've tried:
myTree.store.filters.add(filter);
myTree.store.filters.filter(filter); // This seems to work
// I can get into the filterFn when debugging, but I think item is the "this" of my filter object.
Normally, if I have a grid and I create a filter like above, I can just do myTree.store.filter(filter) and it'll grab each row's item/filter on what I return... but I'm thinking because TreeStore doesn't inherit a filtering function, that's not being passed in.
If someone could provide some clarity as to what I'm doing wrong or any insight on how to set up a filter function/my thinking process, please go ahead. I'd appreciate any help.

Thanks for catching that other one, I fixed up the answer to include the more dynamic treestore filter override that I included below to answer your Q.
It is working fine in 4.1b2, I know there were some changes to the treestore between 4.07 and 4.1 but I think 4.07 still had the tree objects I am using here.
Here's the override:
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
filter: function(filters, value) {
if (Ext.isString(filters)) {
filters = {
property: filters,
value: value
};
}
var me = this,
decoded = me.decodeFilters(filters),
i = 0,
length = decoded.length;
for (; i < length; i++) {
me.filters.replace(decoded[i]);
}
Ext.Array.each(me.filters.items, function(filter) {
Ext.Object.each(me.tree.nodeHash, function(key, node) {
if (filter.filterFn) {
if (!filter.filterFn(node)) node.remove();
} else {
if (node.data[filter.property] != filter.value) node.remove();
}
});
});
me.hasFilter = true;
console.log(me);
},
clearFilter: function() {
var me = this;
me.filters.clear();
me.hasFilter = false;
me.load();
},
isFiltered: function() {
return this.hasFilter;
}
});
It uses the store.tree.nodeHash object to iterate through all nodes against the filters rather than just the first child. It will accept a filter as a function or property/value pair. I suppose the clearFilter method could be worked over though to prevent another ajax call.

This is the answer that I came up with... it's not ideal, so I'm hoping someone can provide a better, more generic approach. Why? Well, if my tree had a parent that had a child that had a child, I'd like to filter on those, but my solution only goes one child deep.
Thanks to this thread, I figured some things out. The only problem with this thread is that it made filtering flat... so child nodes wouldn't appear under their parent nodes. I modified their implementation and came up with this (it only goes 1 child deep, so it wouldn't work if you have a parent that contains a child that has a child):
TreeStore
filterBy : function(fn, scope) {
var me = this,
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
var hash = {};
tmp = root.copy(null, true);
tmp.cascadeBy(function(node) {
if (fn.call(me, node)) {
if (node.data.parentId == 'root') {
hash[node.data.id] = node.copy(null, true);
hash[node.data.id].childNodes = [];
}
else if (hash[node.data.parentId]) {
hash[node.data.parentId].appendChild(node.data);
}
}
/* original code from mentioned thread
if (fn.call(scope || me, node)) {
node.childNodes = []; // flat structure but with folder icon
nodes.push(node);
}*/
});
delete tmp;
root.removeAll();
var par = '';
for (par in hash) {
root.appendChild(hash[par]);
}
return me;
},
clearFilter: function() {
var me = this;
if (me.isFiltered()) {
var tmp = [];
var i;
for (i = 0; i < me.snapshot.childNodes.length; i++) {
tmp.push(me.snapshot.childNodes[i].copy(null, true));
}
me.getRootNode().removeAll();
me.getRootNode().appendChild(tmp);
delete me.snapshot;
}
return me;
},
isFiltered : function() {
return !!this.snapshot;
}
So this works when I do something like this (using my tree in the first post):
Ext.getCmp('myTree').store.filterBy(function(rec) {
return rec.data.id != 'child1';
});
This code will return every record that doesn't have a child1 id, so under leaf1, it will only have child2 as the node. I can also clear the filter by doing Ext.getCmp('myTree').store.clearFilter().
Now, I realize I just answered my own question, but like I posted above, I'd really like critiquing/advice on what I can make more efficient and generic. If anyone has any tips, I'd love to hear them! Also, if you need help getting this code up and running, let me know.
Sha, I also tried filters, but no luck. Have a look at this thread.

The above override is great, and it solves some of my problems, however, I found a bug that is hard to find with the above code. After spending half a day, I figured out we need to use slice() to copy the array otherwise some nodes get deleted.
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
/**
* Filters the current tree by a function fn
* if the function returns true the node will be in the filtered tree
* a filtered tree has also a flat structure without folders
*/
filterBy: function (fn, scope) {
var me = this,
nodes = [],
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
tmp = me.snapshot.copy(null, true);
var childNodes = tmp.childNodes.slice();
root.removeAll();
for (var i = 0; i < childNodes.length; i++) {
//Recursively tranverse through the root and adds the childNodes[i] if fn returns true
this.traverseNode(childNodes[i], root, fn);
}
return me;
},
/**
* Recursively tranverse through the root and adds the childNodes[i] if fn returns true
*/
traverseNode: function (node, parentNode, fn) {
var me = this;
if (fn.call(me, node)) {
parentNode.appendChild(node);
return true;
}
if (node.hasChildNodes()) {
var t_childNodes = node.childNodes.slice();
var found = false;
for (var i = 0; i < t_childNodes.length; i++) {
if (this.traverseNode(t_childNodes[i], node, fn) == true) {
found = true;
}
}
if (found == true) {
parentNode.appendChild(node);
return true;
}
}
return false;
},
/**
* Clears all filters a shows the unfiltered tree
*/
clearFilter: function () {
var me = this;
if (me.isFiltered()) {
me.setRootNode(me.snapshot);
delete me.snapshot;
}
return me;
},
/**
* Returns true if the tree is filtered
*/
isFiltered: function () {
return !!this.snapshot;
}
});

I was looking for a way to filter a treestore so that if a filterBy function returned true for any node, I wanted to display the complete node hierarchy of that node including all the parent nodes, grand parent node, etc and child nodes, grand child node, etc. I modified it from the other solutions provided in this question. This solutions works recursively so the treestore can be of any size.
Ext.override(Ext.data.TreeStore, {
hasFilter: false,
/**
* Filters the current tree by a function fn
* if the function returns true the node will be in the filtered tree
* a filtered tree has also a flat structure without folders
*/
filterBy : function(fn, scope) {
var me = this,
nodes = [],
root = me.getRootNode(),
tmp;
// the snapshot holds a copy of the current unfiltered tree
me.snapshot = me.snapshot || root.copy(null, true);
tmp = me.snapshot.copy(null, true);
var childNodes = tmp.childNodes;
root.removeAll();
for( var i=0; i < childNodes.length; i++ ) {
//Recursively tranverse through the root and adds the childNodes[i] if fn returns true
if( this.traverseNode( childNodes[i], root, fn ) == true ) {
i--;
}
}
return me;
},
/**
* Recursively tranverse through the root and adds the childNodes[i] if fn returns true
*/
traverseNode: function( node, parentNode, fn ) {
var me = this;
if( fn.call( me, node ) ) {
parentNode.appendChild( node );
return true;
}
if( node.hasChildNodes() ) {
var childNodes = node.childNodes;
var found = false;
for( var i=0; i < childNodes.length; i++ ) {
if( this.traverseNode( childNodes[i], node, fn ) == true ) {
found = true;
}
}
if( found == true ) {
parentNode.appendChild( node );
return true;
}
}
return false;
},
/**
* Clears all filters a shows the unfiltered tree
*/
clearFilter : function() {
var me = this;
if (me.isFiltered()) {
me.setRootNode(me.snapshot);
delete me.snapshot;
}
return me;
},
/**
* Returns true if the tree is filtered
*/
isFiltered : function() {
return !!this.snapshot;
}
});
So it works with just like a regular store filterBy call.
searchText = "searchText";
store.filterBy( function(item) {
var keys = item.fields.keys;
for( var i=0; i < keys.length; i++ ) {
var value = item.get( keys[i] );
if( value != null ) {
if( value.toString().toLowerCase().indexOf( searchText ) !== -1 ) {
return true;
}
}
}
return false;
});

I was able to do some basic filtering using onbeforeappend event.
While not as well structured as the above solutions, this provides an easy and straight forward way to apply basic filtering without the need to override base class methods or use external plugins.
I implemented my filtering in the store itself.
In more advanced scenarios this can be done in controller too.
Ext.define('MyApp.store.FilteredTreeStore', {
extend: 'Ext.data.TreeStore',
....
....
listeners: {
beforeappend: function (thisStore, node, eOpts) {
var allowAppend = false;
allowAppend = --your filtering logic here
--returning false will cancel append of the entire sub tree
return allowAppend;
}
}
});

Related

ExtJS 6 Use custom filter function within memory proxy

I have a store configured with memory proxy with enablePaging: true. Store's remoteFilter and remoteSort set to true so filtering and sorting requests handled by proxy.
When I filter my store by multiple fields I want to use OR condition, not AND.
Without memory proxy paging I could use remoteFilter: false and custom filter function like:
store.filterBy(function (record)
{
for (var i = 0; i < searchFields.length; ++i) {
if (record.get(searchFields[i]).toLowerCase().indexOf(searchText) !== -1) {
return true;
}
}
return false;
});
But how I can achieve this with enabled paging? Override memory proxy?
Ext.data.proxy.Memory.read use Ext.util.Filter.createFilterFn to create a filter function based on passed Ext.util.Filter[] (with property-value configs):
// Filter the resulting array of records
if (filters && filters.length) {
// Total will be updated by setting records
resultSet.setRecords(records = Ext.Array.filter(records, Ext.util.Filter.createFilterFn(filters)));
resultSet.setTotal(records.length);
}
So, if you want to use different filter logic, you can override memroy proxy read method and use custom function to create filter function:
Ext.define('MyApp.extensions.FilterMemoryProxy', {
extend: 'Ext.data.proxy.Memory',
alias: 'proxy.filtermemory',
createCustomFilterFn: function (filters)
{
if (!filters) {
return Ext.returnTrue;
}
return function (candidate)
{
var items = filters.isCollection ? filters.items : filters,
length = items.length,
i, filter;
for (i = 0; i < length; i++) {
filter = items[i];
if (!filter.getDisabled() && candidate.get(filter.getProperty()).toLowerCase().indexOf(
filter.getValue().toLowerCase()) !== -1) {
return true;
}
}
return false;
};
},
read: function (operation)
{
...
if (operation.process(resultSet, null, null, false) !== false) {
// Filter the resulting array of records
if (filters && filters.length) {
// Total will be updated by setting records
resultSet.setRecords(records = Ext.Array.filter(records, me.createCustomFilterFn(filters)));
resultSet.setTotal(records.length);
}
...
}
}
});
You can override the memory proxy to suit your needs, or, if you only wish to find a string in one of the fields specified in searchFields, you can add a 'virtual' field to the model with the convert method specified.
For example:
// Extend Ext.data.Model or create it. Dealer's choice.
Ext.define('MyLocalModel', {
extend: 'Ext.data.model',
fields: [
...
{
name: 'searchField',
type: 'string',
convert: function(value, record) {
// Get search fields somewhere
var resultArray = [];
for (var i = 0; i < searchFields.length; ++i) {
var searchField = searchFields[i];
var fieldValue = record.get(searchField);
resultArray.push(fieldValue);
}
// Join by a string that would not normally be searched
// so that field values 'foo' and 'bar' are not valid when searching 'ooba'
var result = resultArray.join('**');
return result;
}
}
]
});
All you need to do in the store then is:
store.filter('searchField', searchText);
It's not exactly what you are searching for, but it should get the job done.

Nodejs async data duplication

I'm having some problems with one async process on nodejs.
I'm getting some data from a remote JSON and adding it in my array, this JSON have some duplicated values, and I need check if it already exists on my array before add it to avoid data duplication.
My problem is when I start the loop between the JSON values, the loop call the next value before the latest one be process be finished, so, my array is filled with duplicated data instead of maintain only one item per type.
Look my current code:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
_.each(data, function(book) {
i++;
console.log('\n\n ------------------------------------------------------------ \n\n');
console.log('BOOK: ' + book.volumeInfo.title);
self.process_author(book, function() { console.log('in author'); });
console.log('\n\n ------------------------------------------------------------');
if(i == data.length) callback();
})
}
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
if(!this.in_array(this.authors, author)) {
this.authors.push(author);
callback();
}
}
}
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i] === obj) return true; }
return false;
}
The result is:
[{name: author1 }, {name: author2}, {name: author1}]
And I need:
[{name: author1 }, {name: author2}]
UPDATED:
The solution suggested by #Zub works fine with arrays, but not with sequelize and mysql database.
When I try to save my authors list on the database, the data is duplicated, because the system started to save another array element before finish to save the last one.
What is the correct pattern on this case?
My code using database is:
BookRegistration.prototype.process_author = function(book, callback) {
if(book.volumeInfo.authors) {
var author = { name: book.volumeInfo.authors[0].toLowerCase() };
var self = this;
models.Author.count({ where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(count) {
if(count < 1) {
models.Author.create(author).success(function(author) {
console.log('SALVANDO AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
} else {
models.Author.find({where: { name: book.volumeInfo.authors[0].toLowerCase() }}).success(function(author) {
console.log('FIND AUTHOR');
self.process_publisher({ book:book, author:author }, callback);
});
}
});
// if(!this.in_array(this.authors, 'name', author)) {
// this.authors.push(author);
// console.log('AQUI NO AUTHOR');
// this.process_publisher(book, callback);
// }
}
}
How can I avoid data duplication in an async process?
This is because you are comparing different objects and result is always false.
Just for experiment type in the console:
var obj1 = {a:1};
var obj2 = {a:1};
obj1 == obj2; //false
When comparing objects (as well as arrays) it only results true when obj1 links to obj2:
var obj1 = {a:1};
var obj2 = obj1;
obj1 == obj2; //true
Since you create new author objects in each process_author call you always get false when comparing.
In your case the solution would be to compare name property for each book:
BookRegistration.prototype.in_array = function(list, obj) {
for(i in list) { if(list[i].name === obj.name) return true; }
return false;
}
EDIT (related to your comment question):
I would rewrite process_new_books_list method as follows:
BookRegistration.prototype.process_new_books_list = function(data, callback) {
var i = 0,
self = this;
(function nextBook() {
var book = data[i];
if (!book) {
callback();
return;
}
self.process_author(book, function() {
i++;
nextBook();
});
})();
}
In this case next process_author is being called not immediately (like with _.each), but after callback is executed, so you have consequence in your program.
Not sure is this works though.
Sorry for my English, I'm not a native English speaker

save values as a string array instead of comma delimited string in a custom xtype CQ5

I have created a custom xtype for multiselect, but i am not able to understand what changes i need to perform to save the values as a string array instead of comma delimited string.
Currently it is storing the values as follows
Property industry
Type String
Value government,healthcare
Instead, i want to save the information as follows
Property industry
Type String[]
Value government,healthcare
Any suggestions, pointers highly appreciated.
CQ.Ext.form.Multiselect = CQ.Ext.extend(CQ.Ext.form.Field, {
store:null,
storeUrl:'',
displayField:'text',
valueField:'value',
allowBlank:true,
minLength:0,
blankText:CQ.Ext.form.TextField.prototype.blankText,
copy:false,
allowDup:false,
allowTrash:false,
legend:null,
focusClass:undefined,
delimiter:',',
view:null,
dragGroup:null,
dropGroup:null,
tbar:null,
appendOnly:false,
sortField:null,
sortDir:'ASC',
defaultAutoCreate : {tag: "div"},
initComponent: function(){
CQ.Ext.form.Multiselect.superclass.initComponent.call(this);
this.addEvents({
'dblclick' : true,
'click' : true,
'change' : true,
'drop' : true
});
},
onRender: function(ct, position){
var fs, cls, tpl;
CQ.Ext.form.Multiselect.superclass.onRender.call(this, ct, position);
cls = 'ux-mselect';
fs = new CQ.Ext.form.FieldSet({
renderTo:this.el,
title:this.legend,
height:this.height,
width:this.width,
style:"padding:1px;",
tbar:this.tbar
});
if(!this.legend){
//fs.el.down('.'+fs.headerCls).remove();
fs.body.addClass(cls);
}
tpl = '<tpl for="."><div class="' + cls + '-item';
if(CQ.Ext.isIE || CQ.Ext.isIE7 || CQ.Ext.isOpera )tpl+='" unselectable=on';
else tpl+=' x-unselectable"';
tpl+='>{' + this.displayField + '}</div></tpl>';
this.store = new CQ.Ext.data.JsonStore({
autoload:true,
url: CQ.HTTP.externalize(this.storeUrl),
fields:['value','text']
});
this.store.load();
this.view = new CQ.Ext.ux.DDView({
multiSelect: true, store: this.store, selectedClass: cls+"-selected", tpl:tpl,
allowDup:this.allowDup, copy: this.copy, allowTrash: this.allowTrash,
dragGroup: this.dragGroup, dropGroup: this.dropGroup, itemSelector:"."+cls+"-item",
isFormField:false, applyTo:fs.body, appendOnly:this.appendOnly,
sortField:this.sortField, sortDir:this.sortDir
});
fs.add(this.view);
this.view.on('click', this.onViewClick, this);
this.view.on('beforeClick', this.onViewBeforeClick, this);
this.view.on('dblclick', this.onViewDblClick, this);
this.view.on('drop', function(ddView, n, dd, e, data){
return this.fireEvent("drop", ddView, n, dd, e, data);
}, this);
this.hiddenName = this.name;
var hiddenTag={tag: "input", type: "hidden", value: "", name:this.name};
if (this.isFormField) {
this.hiddenField = this.el.createChild(hiddenTag);
} else {
this.hiddenField = CQ.Ext.get(document.body).createChild(hiddenTag);
}
fs.doLayout();
},
initValue:CQ.Ext.emptyFn,
onViewClick: function(vw, index, node, e) {
var arrayIndex = this.preClickSelections.indexOf(index);
if (arrayIndex != -1)
{
this.preClickSelections.splice(arrayIndex, 1);
this.view.clearSelections(true);
this.view.select(this.preClickSelections);
}
this.fireEvent('change', this, this.getValue(), this.hiddenField.dom.value);
this.hiddenField.dom.value = this.getValue();
this.fireEvent('click', this, e);
this.validate();
},
onViewBeforeClick: function(vw, index, node, e) {
this.preClickSelections = this.view.getSelectedIndexes();
if (this.disabled) {return false;}
},
onViewDblClick : function(vw, index, node, e) {
return this.fireEvent('dblclick', vw, index, node, e);
},
getValue: function(valueField){
var returnArray = [];
var selectionsArray = this.view.getSelectedIndexes();
if (selectionsArray.length == 0) {return '';}
for (var i=0; i<selectionsArray.length; i++) {
returnArray.push(this.store.getAt(selectionsArray[i]).get(((valueField != null)? valueField : this.valueField)));
}
return returnArray;
},
setValue: function(values) {
var index;
var selections = [];
this.view.clearSelections();
this.hiddenField.dom.value = '';
if (!values || (values == '')) { return; }
if (!(values instanceof Array)) { values = values.split(this.delimiter); }
for (var i=0; i<values.length; i++) {
index = this.view.store.indexOf(this.view.store.query(this.valueField,
new RegExp('^' + values[i] + '$', "i")).itemAt(0));
selections.push(index);
}
this.view.select(selections);
this.hiddenField.dom.value = values;
for (var i=0; i<values.length; i++) {
this.listOfIndustries=values[i];
alert(values[i]);
}
this.validate();
},
getRawValue: function(valueField) {
var tmp = this.getValue(valueField);
if (!tmp) {
tmp = [];
}
return tmp;
},
setRawValue: function(values){
setValue(values);
},
validateValue : function(value){
if (value.length < 1) { // if it has no value
if (this.allowBlank) {
this.clearInvalid();
return true;
} else {
this.markInvalid(this.blankText);
return false;
}
}
if (value.length < this.minLength) {
this.markInvalid(String.format(this.minLengthText, this.minLength));
return false;
}
if (value.length > this.maxLength) {
this.markInvalid(String.format(this.maxLengthText, this.maxLength));
return false;
}
return true;
}
});
CQ.Ext.reg("industriesmultiselect", CQ.Ext.form.Multiselect);
Envionment CQ 5.5
Short answer:
Instead of using the one hidden field to store your values, you need to use multiple underlying input elements, each having the same name property for the Sling Post Servlet to interpret the output as a multi-valued property. See the multifield widget's setValue and addItem methods at /libs/cq/ui/widgets/source/widgets/form/MultiField.js for an example of dynamically adding new fields.
Longer explanation:
It looks like your getValue does what you expect, but the problem is that that method isn't getting called to provide the value that gets submitted. If you're using this widget in a standard dialog, the parent form panel submits the values that are specified in the input elements beneath it in the DOM hierarchy.
In other words, you need to apply your multiple values to DOM elements.
The CQ.Ext.form.Field that you're extending only defines one underlying input element, which you're trying to set with your values array in setValue:
this.hiddenField.dom.value = values;
and in onViewClick
this.hiddenField.dom.value = this.getValue();
Since hiddenField is an input tag of type 'hidden', it holds a string value and when you try to set it this way, you're actually storing the result of calling toString() on your values array. This is why you end up with one string of comma separated values getting submitted.
You'll need to maintain a whole set of hidden fields if you want this widget to work with the standard form submission infrastructure. Alternatively, you could implement your own submit event listener wherever appropriate and use Ext or jQuery to POST an AJAX request with your array (directly from getValue()) as one of the parameters.

How to load tree store with data from another tree store reader

I have couple of tree panels, each configured with individual tree stores. I have configured a proxy for one store. on load event of this, i am trying to load the second store(proxy memory) like below. But doesn't work.
Ext version: 4.0.7
_treeStore2 = Ext.create('Ext.data.TreeStore', {
model: 'Scenario',
proxy : {
type : 'memory'
}
});
_treeStore1 = Ext.create('Ext.data.TreeStore', {
model: 'Scenario',
root:'data1',
proxy : {
type : 'ajax',
url: '/proj/examples?id='+_Id,
reader : {
type : 'json',
root:'data1'
}
},
listeners: {
'load': {
fn: function(store, records, success, operations) {
_treeStore2.setRootNode(_treeStore1.getProxy().getReader().jsonData.data2);
}
}
});
Sample JSON data:
{"data1":[{"name":"value","children":[]}],"data2": [{"name":"value","children":[]}]}
Instead of using the raw Json data, try just passing the the "records" param from the load listener to the other Store. So something like this:
listeners: {
load: function(store, records) {
_treeStore2.setRootNode.setRootNode(records);
}
}
Try this:
/**
* Because of a bug in Ext.data.NoteInterface in ExtJs < 4.1.3
* we have to do deep cloning.
*/
var clone = function(node) {
var result = node.copy(),
len = node.childNodes ? node.childNodes.length : 0,
i;
// Move child nodes across to the copy if required
for (i = 0; i < len; i++)
result.appendChild(clone(node.childNodes[i]));
return result;
};
var oldRoot = store1.getRootNode(),
newRoot = clone(oldRoot);
store2.setRootNode(newRoot);

ExtJs 4.1 TreeGrid Filter through Column Headers

I've been searching around and looking into the documentation, but I'm not sure how to activate filtering via the column drop down the way you have it in normal grid panels.
I've tried to implement the ux.Grid.FilterFeatures but when I apply it to the tree panel, my panel doesn't render properly (all blue panel). I thought it might have something to do with its deferred layout, but when I do a treegrid.hide()/treegrid.show()/treegrid.doLayout(), it doesn't make a difference.
Has anyone gotten the filter feature working with the treepanel? Or has anyone got any suggestions on how to rectify this problem?
I modified the filter example in the Ext-Js site to use it in tree grid.
Ext.define('TreeGridFilter', {
extend: 'Ext.grid.feature.Feature'
, alias: 'feature.treeGridFilter'
, collapseOnClear: true // collapse all nodes when clearing/resetting the filter
, allowParentFolders: false // allow nodes not designated as 'leaf' (and their child items) to be matched by the filter
, treeGrid: null
, filterPropertyNames: new Array()
, filterPropertyValues: new Array()
, filterColumnRenderers: new Array()
, init: function (tree) {
var me = this;
treeGrid = me.tree = tree;
var view = me.view;
var headerCt = view.headerCt;
// Listen for header menu being created
headerCt.on('menucreate', me.onMenuCreate, me);
tree.filter = Ext.Function.bind(me.filter, me);
tree.clearFilter = Ext.Function.bind(me.clearFilter, me);
}
,filter: function (value, property, re, columnRenderer) {
var me = this
, tree = me.tree
, matches = [] // array of nodes matching the search criteria
, root = tree.getRootNode() // root node of the tree
, property = property || 'text' // property is optional - will be set to the 'text' propert of the treeStore record by default
, visibleNodes = [] // array of nodes matching the search criteria + each parent non-leaf node up to root
, viewNode;
me.updateValueForName(property, value, columnRenderer);
if (me.filterPropertyNames.length == 0) { // if the search field is empty
me.clearFilter();
return;
}
tree.expandAll(); // expand all nodes for the the following iterative routines
//iterate over all nodes in the tree in order to evalute them against the search criteria
root.cascadeBy(function (node) {
var numberOfFiltersMatched = 0;
for (var index=0; index < me.filterPropertyNames.length; index++)
{
var propertyName = me.filterPropertyNames[index];
var propertyValue = me.filterPropertyValues[index]
var propertyValueOfNode = node.get(propertyName);
if(me.filterColumnRenderers[index] != false){
var renderingFunction = me.filterColumnRenderers[index];
propertyValueOfNode = renderingFunction(propertyValueOfNode); //Using the renderer function of the column
}
var regExpn = new RegExp(propertyValue, "ig") // the regExp could be modified to allow for case-sensitive, starts with, etc.
if(propertyValueOfNode != null && (propertyValueOfNode+'').match(regExpn)) {
numberOfFiltersMatched++;
}
}
if(numberOfFiltersMatched == me.filterPropertyNames.length){
matches.push(node); // add the node to the matches array
}
});
if (me.allowParentFolders === false) { // if me.allowParentFolders is false (default) then remove any non-leaf nodes from the regex match
Ext.each(matches, function (match) {
if (match == null || !match.isLeaf()) {
Ext.Array.remove(matches, match);
}
});
}
Ext.each(matches, function (item, i, arr) { // loop through all matching leaf nodes
root.cascadeBy(function (node) { // find each parent node containing the node from the matches array
if (node.contains(item) == true) {
visibleNodes.push(node); // if it's an ancestor of the evaluated node add it to the visibleNodes array
}
});
if (me.allowParentFolders === true && !item.isLeaf()) { // if me.allowParentFolders is true and the item is a non-leaf item
item.cascadeBy(function (node) { // iterate over its children and set them as visible
visibleNodes.push(node);
});
}
visibleNodes.push(item); // also add the evaluated node itself to the visibleNodes array
});
root.cascadeBy(function (node) { // finally loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional
viewNode.setVisibilityMode(Ext.Element.DISPLAY); // set the visibility mode of the dom node to display (vs offsets)
viewNode.setVisible(Ext.Array.contains(visibleNodes, node));
}
});
}
, clearFilter: function () {
var me = this
, tree = this.tree
, root = tree.getRootNode();
if (me.collapseOnClear) {
tree.collapseAll(); // collapse the tree nodes
}
root.cascadeBy(function (node) { // final loop to hide/show each node
viewNode = Ext.fly(tree.getView().getNode(node)); // get the dom element assocaited with each node
if (viewNode) { // the first one is undefined ? escape it with a conditional and show all nodes
viewNode.show();
}
});
},
onMenuCreate: function(headerCt, menu) {
var me = this;
menu.on('beforeshow', me.onMenuBeforeShow, me);
},
onMenuBeforeShow: function(menu) {
var me = this;
var currentHeaderFilter = menu.activeHeader.filter;
if(currentHeaderFilter == null){
if(me.menuItem == null){
return;
}
me.menuItem.hide();
me.menuSeparator.hide();
}else if(me.menuItem != null){
me.menuItem.show();
me.menuSeparator.show();
}
if(me.menuItem){
var perviousFilterValue = me.getValueForName(menu.activeHeader.dataIndex);
if(perviousFilterValue == null || perviousFilterValue == ''){
me.menuItem.setRawValue('');
}else{
me.menuItem.setRawValue(perviousFilterValue);
}
}else{
me.menuSeparator = menu.add('-');
var filterTextFiels = new Ext.form.TextField({
itemId: 'filterTextBox',
cls : 'find-icon',
listeners: {
'change': this.onFilterTextChange
}
});
me.menuItem = menu.add(filterTextFiels);
}
me.menuItem.activeDataIndex = menu.activeHeader.dataIndex;
me.menuItem.activeRenderer = menu.activeHeader.renderer;
me.menuItem.width = (currentHeaderFilter == null || currentHeaderFilter.width == null) ? 150 : currentHeaderFilter.width;
},
onFilterTextChange : function (searchMenuItem, value) {
treeGrid.filter(value,searchMenuItem.activeDataIndex, null, searchMenuItem.activeRenderer);
},
updateValueForName : function(property, value, columnRenderer){
var propertyIndex = -1;
for (var index=0; index < this.filterPropertyNames.length; index++)
{
if(property == this.filterPropertyNames[index]){
propertyIndex = index;
break;
}
}
if(propertyIndex >= 0){
if(value == null || value == ''){
this.filterPropertyNames.splice(propertyIndex, 1);
this.filterPropertyValues.splice(propertyIndex, 1);
this.filterColumnRenderers.splice(propertyIndex, 1);
}else{
this.filterPropertyValues[propertyIndex] = value;
}
}else{
propertyIndex = this.filterPropertyNames.length;
this.filterPropertyNames[propertyIndex] = property;
this.filterPropertyValues[propertyIndex] = value;
this.filterColumnRenderers[propertyIndex] = columnRenderer;
}
},
getValueForName : function(property){
var propertyIndex = -1;
for (var index=0; index < this.filterPropertyNames.length; index++)
{
if(property == this.filterPropertyNames[index]){
propertyIndex = index;
break;
}
}
if(propertyIndex >= 0){
return this.filterPropertyValues[propertyIndex];
}else{
return null;
}
}
});
This feature can be used in grid as
var treeGridFilter = {
ftype: "treeGridFilter"
};
var treeGrid= Ext.create('Ext.tree.Panel', {
id : 'tree-grid-id',
title: 'View Tree Grid',
features: [treeGridFilter],
store: store,
renderTo: 'grid-div',
columns: [{
text: 'Column One',
dataIndex: 'columnOne',
filter : {
width: 150
}
},{
text: 'Column Two',
dataIndex: 'columnTwo',
filter : {
width: 100
}
}]
});
Tree stores don't have a filtering function, but I had to create one for an earlier project. Here's an example.
So, as a suggestion you could create your own filter drop menus using Ext.grid.header.Container configs in the tree columns (as covered here in the docs) these could be set-up to call some kind of filter function on the treestore like the one I linked to above.

Resources