Ext JS 4: Filtering a TreeStore

2020-02-05 09:26发布

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.

5条回答
贪生不怕死
2楼-- · 2020-02-05 10:16

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;
        });
查看更多
仙女界的扛把子
3楼-- · 2020-02-05 10:22

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;
      }
    });
查看更多
爷的心禁止访问
4楼-- · 2020-02-05 10:24

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.

查看更多
啃猪蹄的小仙女
5楼-- · 2020-02-05 10:24

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;
        }
    }
});
查看更多
smile是对你的礼貌
6楼-- · 2020-02-05 10:31

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.

查看更多
登录 后发表回答