2
votes

I have a tree view in my Backbone app, I use nested collections and models:

Collection:

define(function(require) {

    var Backbone      = require('backbone')
      , UserListModel = require('app/models/userList');

    return Backbone.Collection.extend({
        model: UserListModel,
        url: '/api/lists',
    });
});

Model:

define(function(require) {

    var Backbone = require('backbone');

    return Backbone.Model.extend({

        constructor: function(data, opts) {
            opts = _.extend({}, opts, {parse: true});

            var UserLists  = require('app/collections/userLists');
            this.children  = new UserLists();

            Backbone.Model.call(this, data, opts);
        },

        parse: function(data) {
            if (_.isArray(data.children))
                this.children.set(data.children);
            return _.omit(data, 'chilren');
        }

    });
});

Part of The View: (full views here: http://laravel.io/bin/O9oYX)

var UserListTreeItemView = Backbone.View.extend({

    render: function() {
        var data = this.model.toJSON();
            data.hasChildren = !!this.model.get('isFolder');

        this.$el.html(this.template(data));

        if( this.model.get('isFolder') ) {
            var list = new UserListTreeView({
                collection: this.model.children
            });
            this.$el.append(list.render().el);
        }

        return this;
    }

});

And I use two Views to render my collection as a tree view. I want to add a search feature to my tree view, I can’t figure out how. It should be able to search name attributes on all models and their nested ones.

Any ideas?

2
Could you post render part of the View for this tree? - Mostafa
@Mostafa I pasted all 3 views here: laravel.io/bin/O9oYX - Sallar

2 Answers

1
votes

If you have already the models you want on your collection, just use the inherited Underscore method filter() on the collection itself. It will return an Array of models, not a Backbone Collection, though.

http://underscorejs.org/#filter

Supposing filtering by attribute name:

var nameToSearch = "whatever";
var itemsByName = this.model.children.filter(function(item){
  return item.get("name").indexOf(nameToSearch) >=0;
}

What I would do is isolate your getData method to cover both cases: filtering on/off.

You didn't specify how do you search, but I'll suppose you have a text input around and you want to use that value. Will that search in the top items only? A search-in-depth would be a little more complicated, involving each parent item to look for the name on its children. For the simple case that you'll be searching for files in every folder, keep the search filter in you parent View state. For that, I normally use a plain vanilla Backbone Model, just to leverage events.

var MySearchView = Backbone.View.extend({
  initialize: function(options){
     //I like the idea of having a ViewModel to keep state
     this.viewState = new Backbone.Model({
       searchQuery: ""
     });
     //whenever the search query is changed, re-render
     this.listenTo(this.viewState, "change:searchQuery", this.render);
  },
  events: {
    "click .js-search-button": "doSearch"
  },
  doSearch: function(e){
    e.preventDefault();
    var query = this.$(".js-search-input").val();
    this.viewState.set("seachQuery", query);
  },  
  render: function(){
    var data = this.model.toJSON();
        data.hasChildren = !!this.model.get('isFolder');

    this.$el.html(this.template(data));

    if( this.model.get('isFolder') ) {
        //be careful with this, you're not removing your child views ever
        if(this._listView) {
           this._listView.remove();
        }
        this._listView = new UserListTreeView({
            collection: this.model.children,
            **searchQuery: this.viewState.get("searchQuery")**
        });
        this.$el.append(this._listView.render().el);
    }

    return this;
  }
});

Now in your UserListTreeView, abstract the data-feeding for the template into a method that takes into account the search query:

var UserListTreeView = Backbone.View.extend({
  initialize: function(options){
    this.searchQuery = options.searchQuery || "";
  },
  ...
  getData: function(){
     //filter your collection if needed
     var query = this.searchQuery;
     if(query !== ""){
       return this.collection.filter(function(file){
         return file.get("name").indexOf(query) >= 0;
     }
     else {
       return this.collection.toJSON();
     }
  },
  render: function() {
    var items = this.getData(),
        template = this.template(items);

    this.$el.empty().append(template);
    return this;
  }
});

Voilá, the same view will render either the full collection or a filtered version whose items contain the searchQuery in their name. You can adjust the search method just by changing the comparison inside the filter call: you could do RegExp, search only for files starting with (indexOf(searchQuery) == 0), and so on.

Took it longer than expected, hope it helps. Another option would be to implement this in the collection itself, you can override its toJSON() method to return either all, or some items on it. If you find yourself writing another view that needs filterint, then probably it's a better idea to create a SearchableCollection and inherit both from there. Keep it DRY. :)

As a side note: you should have a look at MarionetteJS or build your own specialized views (Collection, and so on) just to save from typing the same over and over again.

0
votes

I’m not sure I’ve totally understood your app, but here’s how I’ve done something similar before:

In your model add this:

matches: function(search) {
    // a very simple and basic implementation
    return this.get('name').indexOf(search) != -1;
}

And use it in UserListTreeView’s render:

render: function() {
    var search = $someElement.val();
    var _this = this;
    _.each(this.collection.models, function(model) {
        if (model.matches(search)) {
            _this.addItem(model);
        }
    });
    return this;
}

Very simple, yet effective. This is actually the most basic version to transfer the idea. You can improve this approach by extending it to other models and collections, checking for some edge cases, and improving its performance by simple optimizations.