Quantcast
Channel: Ryan Vanderpol » filtering
Viewing all articles
Browse latest Browse all 2

Building Some Slick Filtering with KnockoutJS

$
0
0

So, now that we got paging and edit-in-place done, how about some filtering? No, no, not server-side filtering. Let’s build some slick, super fast client-side filtering. It should be extensible, too, so we can tack on whatever crazy-ass filters we could ever dream up. Sound good? I thought so.

Let’s start simple, though. So, I’m bringing back the state list from the previous articles. But I’m gonna modify it a bit. Let’s make it a list of states where we can filter by whether or not that state has good beer. Okay, I’m a bit biased towards the Pacific NorthWest, so cut me some slack!

Here’s what we’re gonna build:

Pretty simple, right? It’s just a list of states with a text-box for searching and a drop-down menu for picking from a preset filter. Both of these filters (and any others we add) all work together. If we were to turn on the “Only States with Good Beer” filter, and we did a text search for “or” we’d get back California, Oregon and Colorado.

Alright, let’s get started. First, we’ll put together our markup. I’m using KnockoutJS and Twitter Bootstrap for this, so hang tight:


<div class="btn-group pull-right">
    <a class="btn dropdown-toggle" data-toggle="dropdown" href="#"><i class="icon-filter"></i> <span data-bind="text: ko.computed(function() { return $root.getFilterText('active'); })"></span> <span class="caret"></span></a>
    <ul class="dropdown-menu pull-left">
        <li><a href="#" data-bind="click: function() { $root.applyFilter('active', function(f) { f.value(-1); }); }"><i data-bind="css: { 'icon-star': $root.findFilter('active').value() === -1 }"></i> All</a></li>
        <li><a href="#" data-bind="click: function() { $root.applyFilter('active', function(f) { f.value(1); }); }"><i data-bind="css: { 'icon-star': $root.findFilter('active').value() === 1 }"></i> Only States with Good Beer</a></li>
        <li><a href="#" data-bind="click: function() { $root.applyFilter('active', function(f) { f.value(0); }); }"><i data-bind="css: { 'icon-star': $root.findFilter('active').value() === 0 }"></i> Only States with Bad Beer</a></li>
    </ul>
</div>
<div class="pull-right">
    <form class="form-inline">
    <div class="control-group">
        <div class="controls">
            <div class="input-prepend">
                <span class="add-on"><i class="icon-search"></i></span><input class="input-medium" id="prependedInput" size="16" type="text" data-bind="value: $root.findFilter('text').value, valueUpdate: 'afterkeydown'" />
            </div>
        </div>
    </div>
    </form>

</div>
<br />

<table class="table table-striped table-bordered">
    <thead>
        <tr>
            <th>Name</th>
            <th>Short Name</th>
            <th>Good Beer?</th>
        </tr>
    </thead>
    <tbody data-bind="foreach: filteredList">
        <tr>
        <td data-bind="text: name"></td>
        <td data-bind="text: shortName"></td>
        <td data-bind="text: active"></td>
    </tr>
    </tbody>
</table>​

Not so bad. We throw in a drop-down button with three options. I know the KO data-binding looks a little crazy, but it should make sense in a bit. We also throw in an input for doing a textual search. Again, data-binding is a little crazy, but hang on. After those pieces, it’s pretty simple. Just a standard data-bound table.

Okay, so now the fun part. Here’s what our KO View Model and data looks like:


var ListViewModel = function (initialData, filters) {
    var self = this;
    window.viewModel = self;

    self.innerList = ko.observableArray(initialData);
    self.filters = ko.observableArray(filters || []);

    self.findFilter = function (filterName) {
        var matchingFilter = null;
        $.each(self.filters(), function (i, v) {
            if (v.name === filterName) {
                matchingFilter = v;
                return false;
            }
        });
        return matchingFilter;
    };

    self.applyFilter = function (filterName, callback) {
        var filter = self.findFilter(filterName);
        callback(filter);
    };

    self.filteredList = ko.dependentObservable(function () {
        var list = [];
        $.each(self.innerList(), function (i, v) {
            var applied = true;
            $.each(self.filters(), function (ii, vv) {
                if (!(vv.apply(v))) {
                    applied = false;
                }
            });
            if (applied) {
                list.push(v);
            }
        });
        return list;
    });

    self.getFilterText = function (name) {
        var text = '';
        self.applyFilter(name, function (f) { text = f.text(); });
        return text;
    };
};


   function State(id,name, shortName, active){
        this.id = ko.observable(id);
        this.name = ko.observable(name);
        this.shortName = ko.observable(shortName);
        this.active = ko.observable(active);
    }

It looks a little crazy, but it’s not really so bad.

First, we define our ListViewModel. It’s got a list of data and a list of filters. You’ll notice later, when we instantiate a new ListViewModel, we pass in a list of states and a list of some filters. The ListViewModel has some cool stuff in it, like the ability to apply a specific filter against the innerList as well as get back all the data that matches only the applies filters, via the filteredList.

Now that we have the view model defined, let’s create our data set and do some KO bindings:


    var initialData = [];
    initialData.push(new State('1','Washington','WA',true));
    initialData.push(new State('2','Alaska','AK',false));
    initialData.push(new State('3','California','CA',true));
    initialData.push(new State('4','Oregon','OR',true));
    initialData.push(new State('5','Idaho','ID',false));
    initialData.push(new State('6','Montana','MT',false));
    initialData.push(new State('7','Utah','UT',true));
    initialData.push(new State('8','Colorado','CO',true));
    initialData.push(new State('9','Nevada','NV',false));
    initialData.push(new State('10','Arizona','AZ',false));
    
    ko.applyBindings(new ListViewModel(initialData, [{
        name: 'active',
        reset: function() { this.value(-1); },
        value: ko.observable(-1),
        text: function() { switch(this.value()) { case -1: return 'All'; case 0: return 'Bad Beer'; case 1: return 'Good Beer'; } },
        apply: function(state){ return this.value() === -1 || state.active() === (this.value() === 1); }
    },{
        name: 'text',
        reset: function() { this.value(''); },
        value: ko.observable(''),
        text: function() { return this.value(); },
        apply: function(state){ if(!this.value()) { return true; } else { return state.name().toLowerCase().indexOf(this.value().toLowerCase()) != -1; }  }
    }]));​

Holy shit. What’s all that crap? You didn’t think this was ALL going to be easy, did you? Let’s break it down a bit. The first part is simple. We make a list of states. But when we go to apply our KO bindings and create an instance of our ListViewModel, we pass in a bunch of filters.

The structure of a filter is actually pretty simple. Every filter has a name and a value. Every filter also has a way to get back a text-based representation of its current state and has a way to be reset back to its default value.

The only tricky part is the apply function. If you remember, from our ListViewModel, the filteredList dependentObservable looped through the full data set (innerList) and then for each item it looped over each filter and called the apply function, passing in the item. The filter is responsible for returning a value that says whether or not the item that was passed in meets the criteria of that filter. If it does, that item gets included in the filteredList. If not, it’s excluded.

Not enough for you? Okay, how about a jsFiddle, too? Yeah, that’s better.

There are tons of ways you could expand this. If you come up with cool ideas, post them in the comments. Have fun!


Viewing all articles
Browse latest Browse all 2

Trending Articles