2

I am currently implementing a treeview using slickgrid.

My code is essentially based on this example.

What I am trying to do is get a search filter, similar to the one in the example, but which works on the branches as well as the parents. For instance if a tree looks like this:

-Parent 1
  -branch 1
   -sub_branch 1
  -branch 2
-Parent 2
  -branch 1
  -branch 2

and I search for the number '1' it should show this:

-Parent 1
  -branch 1
   -sub_branch 1
  -branch 2
-Parent 2
  -branch 1

rather than this:

-Parent 1

Sorry I don't have any of my code to show, I've not got anywhere. Any ideas? Thanks

Captastic
  • 1,036
  • 3
  • 10
  • 19

2 Answers2

2

UPDATE:

I had to improve the code i wrote over a year ago this week, and with alot of testing, this is what i ended up with. This method is alot faster than the old one, and i mean alot! Tested with a node depth of 5 nodes and 5100 rows this data preparation takes around 1.3s, but if you don't need case insensitive search, removing toLowerCase will half that time to around 600ms. When the searchstrings are prepared, the search is instant.

This is from our setData function where we prepare the data

var self = this,
    searchProperty = "name";

//if it's a tree grid, we need to manipulate the data for us to search it
if (self.options.treeGrid) {

    //createing index prop for faster get
    createParentIndex(items);

    for (var i = 0; i < items.length; i++) {
        items[i]._searchArr = [items[i][searchProperty]];
        var item = items[i];

        if (item.parent != null) {
            var parent = items[item.parentIdx];

            while (parent) {
                parent._searchArr.push.apply(
                    parent._searchArr, uniq_fast(item._searchArr)
                    );

                item = parent;
                parent = items[item.parentIdx];
            }
        }
    }

    //constructing strings to search
    //for case insensitive (.toLowerCase()) this loop is twice as slow (1152ms instead of 560ms for 5100rows) .toLowerCase();
    for (var i = 0; i < items.length; i++) {
        items[i]._search = items[i]._searchArr.join("/").toLowerCase(); 
        items[i]._searchArr = null;
    }

    //now all we need to do in our filter is to check indexOf _search property
}

In the code above, I use some functions. The first one creates two properties, one for its own position in the array, and the second parentIdx for parents index. I'm not so sure if this actually speeds up the performance, but it removes the need for a nested loop in the setData function.

The one that actually makes all the difference here is the uniq_fast, which takes an array and removes all the duplicates in it. The method is one of the many functions from this answer remove-duplicates-from-javascript-array

function createParentIndex(items) {
    for (var i = 0; i < items.length; i++) {
        items[i].idx = i; //own index
        if (items[i].parent != null) {
            for (var j = 0; j < items.length; j++) {
                if (items[i].parent === items[j].id) {
                    items[i].parentIdx = j; //parents index
                    break;
                }
            }
        }
    }
}

function uniq_fast(a) {
    var seen = {};
    var out = [];
    var len = a.length;
    var j = 0;
    for (var i = 0; i < len; i++) {
        var item = a[i];
        if (seen[item] !== 1) {
            seen[item] = 1;
            out[j++] = item;
        }
    }
    return out;
}

Now with all this preparation of the data, our filter function actually becomes pretty small and easy to handle. The filter function is called for each item, and as we now have the _search property on each item, we just check that. If no filter applied we need to make sure that we don't show closed nodes

function treeFilter(item, args) {
    var columnFilters = args.columnFilters;

    var propCount = 0;
    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            propCount++;

            if (item._search === undefined || item._search.indexOf(columnFilters[columnId]) === -1) {
                return false;
            } else {
                item._collapsed = false;
            }
        }
    }

    if (propCount === 0) {
        if (item.parent != null) {
            var dataView = args.grid.getData();
            var parent = dataView.getItemById(item.parent);
            while (parent) {
                if (parent._collapsed) {
                    return false;
                }

                parent = dataView.getItemById(parent.parent);
            }
        }
    }     

    return true;
}

So, the question was asked long ago, but if someone is looking for an answer for this, use the code above. It's fast, but any improvements of the code would be much appritiated!

END OF EDIT

old answer (this is very slow):

As a start, you have to create a filter function that you use with your dataView. The dataView will call your function as soon as you type something. The function will be called for each row in the dataView, passing the row as the item parameter. Returning false indicates that the row should be hidden, and true for visible.

Looking at the Tree example, the filter function looks like this

function myFilter(item, args) {
  if (item["percentComplete"] < percentCompleteThreshold) {
    return false;
  }

  if (searchString != "" && item["title"].indexOf(searchString) == -1) {
    return false;
  }

  if (item.parent != null) {
    var parent = data[item.parent];

    while (parent) {
      if (parent._collapsed || (parent["percentComplete"] < percentCompleteThreshold) || (searchString != "" && parent["title"].indexOf(searchString) == -1)) {
        return false;
      }

      parent = data[parent.parent];
    }
  }

  return true;
}

In my first attempt to do this, I tried to manipulate the parent so that it should not be hidden. The problem is that i have no clue how to unhide it, and the problem is also that you don't know in which order the rows will be filtered (if the parent row is the last to be filtered, parent property is null)

I abandoned that thought and tried to work with the item passed into the method, as this is how it's intended. The way to do it when working with basic parent/child tree structures is to use recursion.

My solution

To start, create a function that holds all the filtering and returns true or false. I've used fixed header row for fast filters as a base and then added my own rules to it. This is a really stripped down version of my realFilter function, so you might need to tweak it a little bit.

function realFilter(item, args) {
    var columnFilters = args.columnFilters;
    var grid = args.grid;
    var returnValue = false;

    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            returnValue = true;
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];

            if (item[c.field].toString().toLowerCase().indexOf(
                columnFilters[columnId].toString().toLowerCase()) == -1) { //if true, don't show this post
                returnValue = false;
            }
        }
    }
    return returnValue;
}

Secondly, it's time for the recursive function. This is the tricky part if you'r not familiar with how they work.

//returns true if a child was found that passed the realFilter
function checkParentForChildren(parent, allItems, args) { 
    var foundChild = false;
    for (var i = 0; i < allItems.length; i++) {
        if (allItems[i].parent == parent.id) {
            if (realFilter(allItems[i], args) == false && foundChild == false) //if the child do not pass realFilter && no child have been found yet for this row 
                foundChild = checkParentForChildren(allItems[i], allItems, args);
            else
                return true;
        }
    }
    return foundChild;
}

At last, we implement the original filter function. This is the function that is called by slickgrid and should be registered to the dataView

//registration of the filter
dataView.setFilter(filter);

//the base filter function
function filter(item, args) {
    var allRows = args.grid.getData().getItems();
    var columnFilters = args.columnFilters;
    var grid = args.grid;
    var checkForChildren = false;

    for (var i = 0; i < allRows.length; i++) {
        if (allRows[i].parent == item.id) {
            checkForChildren = true;
            break;
        }
    }

    for (var columnId in columnFilters) {
        if (columnId !== undefined && columnFilters[columnId] !== "") {
            var c = grid.getColumns()[grid.getColumnIndex(columnId)];
            var searchString = columnFilters[columnId].toLowerCase().trim();

            if (c != undefined) {
                if (item[c.field] == null || item[c.field] == undefined) {
                    return false;
                }
                else { 
                    var returnValue = true;

                    if (checkForChildren) {
                        returnValue = checkParentForChildren(item, allRows, args);
                        if(!returnValue)
                            returnValue = realFilter(item, args);
                    }
                    else
                        returnValue = realFilter(item, args);

                    if (item.parent != null && returnValue == true) {
                        var dataViewData = args.grid.getData().getItems();
                        var parent = dataViewData[item.parent];

                        while (parent) {
                            if (parent._collapsed) {
                                parent._collapsed = false;
                            }
                            parent = dataViewData[parent.parent];
                        }
                    }

                    return returnValue;
                }
            }
        }
    }

    if (item.parent != null) {
        var dataViewData = args.grid.getData().getItems();
        var parent = dataViewData[item.parent];

        while (parent) {
            if (parent._collapsed) {
                return false;
            }

            parent = dataViewData[parent.parent];
        }
    }
    return true;
}

I'm currently working on this so I have not really bothered to improve the code yet. It is working as far as i know, but you may have to tweak some things in filter and realFilter to get it to work as you expect. I wrote this today so it's not tested more than under the development phase.

Note: If you want to use another input for your search you can just use $.keyup() on that field and then pass the data to the header filter. This way you get all the functionality to use column-level filters, even if you don't want to use them in this particular case.

Community
  • 1
  • 1
Binke
  • 897
  • 8
  • 25
1

Personally I use the grouping example and I also helped on making it multi-column (nested) grouping and with that one it does exactly what you are looking for... So instead of using the one you said, which I think is mainly made for indenting only, you should use this one Interactive grouping and aggregates.

The example does not include the search but it's easy to add it, just like in my project. And yes the parent group never goes away. In the example for multi-grouping, choose 50k rows and then click on "Group by duration then effort-driven then percent", you will see a nice 3 columns grouping :) Copy it, add the search bar and it should work

EDIT

I added support for Tree Data with filtering and also recently Tree Totals with Aggregators feature. You can see these Slickgrid-Universal examples (which I created)

Also note that the exact same feature is available in my other projects

ghiscoding
  • 12,308
  • 6
  • 69
  • 112
  • Thanks for the reply. Yeah, I agree the multi-column grouping is a good solution. Infact I have used it elsewhere within the project. Sadly this requirement didn't suit it as well as I have varying grouping levels and up to 8 levels of sub-branches so I don't think the grouping will work as well. It may be that slickgrid isn't the right solution here if I want to be able to search as well. I'll keep trying anyway. Thanks for the help – Captastic May 31 '13 at 08:07
  • Hmm ok but I still don't know why you said it might not the right solution? I did try to search within a multi-group and the parent always stays there, isn't it what you want in fact? Unless I did not fully understand your needs – ghiscoding May 31 '13 at 13:35