6

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.

incutonez
  • 3,241
  • 9
  • 43
  • 92
  • Interesting... In their documentation said that Treestore has filters property but no mentions for any filtering methods. Have you try to specify filters[] before loading and also setting filterOnLoad: true? – sha Mar 09 '12 at 01:53

5 Answers5

6

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.

Community
  • 1
  • 1
egerardus
  • 11,316
  • 12
  • 80
  • 123
  • So I've been struggling with trying to get this to work. I'm assuming it just doesn't work in 4.0.7... when I filter the tree, I get nothing returned, and if I try to clear the filter, nothing is returned, but that might be because I'm using memory type as the proxy. Also, in your clearFilter function, is filters.clear() defined for an AbstractStore, or does that not matter? Would you by any chance have a working solution for 4.0.7? – incutonez Mar 20 '12 at 11:42
  • I take some of that back... the filtering works if I use your "leaf only filter as a function" code. The clearFilter() still doesn't work. I'll try messing around with it more. – incutonez Mar 20 '12 at 11:58
  • Ok, I think I've got it working... if I add `me.snapshot = me.snapshot || me.getRootNode().copy(null, true);` to the filter code right after you create your variables, and then change clearFilter to `if (me.isFiltered()) { me.getRootNode().removeAll(); me.snapshot.eachChild(function(child) { me.getRootNode().appendChild(child.copy(null, true)); }); delete me.snapshot; } me.hasFilter = false;` everything seems to work. Only problem is, looping over the snapshot's child elements is kind of expensive, so maybe there's a better way? – incutonez Mar 20 '12 at 12:34
  • Sorry, I was out for a few days. It seems like you have it now in 4.07, I never tried with anything other than 4.1b2. – egerardus Mar 22 '12 at 21:12
  • Yep, definitely works. Thanks! Only thing is, reloading the store with `.load` wasn't really what I was looking for, so that's why I changed clearFilter. – incutonez Mar 22 '12 at 22:39
1

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.

Community
  • 1
  • 1
incutonez
  • 3,241
  • 9
  • 43
  • 92
0

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;
        });
Johnny Vu
  • 1
  • 2
  • works fine on the desktop version of sencha touch app, but when creating the phone gap build for ios/android this solution breaks. Any ideas what might be the issue? were you able to successfully run it on iOS? – Meer Jul 11 '17 at 21:17
0

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;
      }
    });
  • Thankfully, you won't be having to rely on this override much longer... in the Ext JS 5 beta release, they have native [tree filtering](http://docs-origin.sencha.com/extjs/5.0.0/apidocs/#!/api/Ext.data.TreeStore-method-filterBy). – incutonez Apr 10 '14 at 12:53
  • 1
    Warning: this causes major problems if you're using autoSync. – Roy Tinker Jan 14 '15 at 17:44
0

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;
        }
    }
});
DanielS
  • 744
  • 6
  • 13