1

I am building a sidebar to filter a main view, like for instance the one at John Lewis. I have the code working but it ain't pretty.

I know there are several SO questions on similar lines but I can't quite fathom my own use case.

I need to get the names of the checkboxes from the server ( eg via JSON ) to dynamically create observableArrays on my ShopView.

Here's how it is:

var data = {
    'gender' : [ ],
    'color' : [ ]
};

var filterMapping = {
    create: function( obj ) {
        return ko.observableArray( obj.data );
    }
}

var ShopView = new function() {

    var self = this;

    ko.mapping.fromJS( { filters: data }, filterMapping, self );

    // this is the bit I don't like
    this.filterChange = ko.computed(function () {
        for( var key in self.filters )  {
            var obj = self.filters[key]; 
             if( ko.isObservable(obj)){
                obj();                 
             }             
        }
    });

    this.filterChange.subscribe( function( ) {
        //make AJAX request for products using filter state
    });

}

My HTML looks as you'd expect:

Gender

    <ul>
        <li><input type="checkbox" value="male" data-bind="checked: filters.gender" />Male</li>
        <li><input type="checkbox" value="female" data-bind="checked: filters.gender" />Female</li>
    </ul>

As I say, it works, but it's not nice. In an ideal world I could subscribe to this.filters, eg

this.filters.subscribe( function() { 
    //make AJAX request for products using filter state
});

NB I'm not trying to do the filtering on the client side - just update my viewmodel when the dynamically-bound checkboxes change.

Any ideas? thanks!

4
  • What's stopping you from subscribing to this.filters? As long as you subscribe after the ko.mapping.fromJS call you should be fine. Commented Jan 23, 2013 at 16:19
  • this.filters isn't an observable - it's just an object property Commented Jan 23, 2013 at 16:38
  • Do you have control of both the client and the server code? Are you willing to make changes to your viewmodel? Commented Jan 23, 2013 at 19:15
  • @Tyrsius yes I do. and yes, naturally :-) Commented Jan 23, 2013 at 23:40

1 Answer 1

1

First, the mapping plugin should be treated as an aid to code duplication. I don't think its a good idea to think of the mapping plugin as a solution in and of itself; at least not directly. It also obscures what is happening when you post your code on SO, since we can't see the models you are working with. Just a thought.

Now, ff you want to get dynamic filters from the server, and use them to filter a list of items (like you would in a store), I would do it something like this (here is the fiddle):

var FilterOption = function(name) {
    this.name = name;
    this.value = ko.observable(false);
};

var Filter = function(data) {
    var self = this;
    self.name = data.name;
    options = ko.utils.arrayMap(data.options, function(o) {
        return new FilterOption(o);
    });
    self.options = ko.observableArray(options);
    self.filteredOptions = ko.computed(function() {
        var options = []
        ko.utils.arrayForEach(self.options(), function(o) {
            if (o.value()) options.push(o.name);
        });
        //If no options, false represents no filtering for this type
        return options.length ? options : false;
    });
};

var ViewModel = function(data) {
    var self = this;
    self.items = ko.observableArray(data.items);
    filters = ko.utils.arrayMap(data.filters, function(i) {
        return new Filter(i);
    });
    self.filters = ko.observableArray(filters);
    self.filteredItems = ko.computed(function() {
        //Get the filters that are actually active
        var filters = ko.utils.arrayFilter(self.filters(), function(o) {
            return o.filteredOptions();
        });
        //Remove items that don't pass all active filter
        return ko.utils.arrayFilter(self.items(), function(item) {
            var result = true;
            ko.utils.arrayForEach(filters, function(filter) {
                var val = item[filter.name.toLowerCase()];
                result = filter.filteredOptions().indexOf(val) > -1;
            });
            return result;
        });
    });
};

The next obvious step would be to add support for items that had multiple properties, but or options properties, but this should give you the basic idea. You have a list of filters, each with any number of options (which stack additively), and you use a computed items array to store the result of filtering the items.


Edit: To get the items using an ajax subscription, you would replace the FilteredItems prop with a computed that gets the selected filters, and then subscribe to it, like this:

var ViewModel = function(data) {
    var self = this;
    self.items = ko.observableArray(data.items);
    filters = ko.utils.arrayMap(data.filters, function(i) {
        return new Filter(i);
    });
    self.filters = ko.observableArray(filters);
    self.selectedFilters = ko.computed(function() {
        ko.utils.arrayFilter(self.filters(), function(o) {
            return o.filteredOptions();
        });
    });
    self.selectedFilters.subscribe(function() {
        //Ajax request that updates self.items()
    });
};
Sign up to request clarification or add additional context in comments.

6 Comments

+1. nice work! thank you. this way surpasses what I was attempting, in that you're doing the actual filtering very elegantly on the client side. However in my app, all that happens is the JS sends the filters to the server in an ajax request and gets back the duly filtered products. I'm sorry I didn't make this clearer in the question... which unfortunately still stands. Do I need to use that ugly ko.computed function to make my ShopView.filters 'observable'?
@djb That depends on a lot of things. If you are doing all of your filtering on the server, then no. If you filter on the client, a computed function will be needed, but there is a shorter way to write it. I wrote the long version so you could see whats happening, but the filters filter and the items filter can be combined (its uglier, IMO, and harder to understand). Computed observables are a very powerful Knockout tool, and should not be avoided. The are so useful the docs touch on them before they even get to ObservableArrays. I'll give an answer showing it with a server response.
@djb Before I write another answer you don't want, can you be a little more specific about what it is you are after. Are you trying to avoid computed observables entirely? (I don't think that is wise). Are you just trying figure out what the subscribe looks like?
hey @Tyrsius - thanks for your patience. I don't mind computeds at all and am very happy to use one. It just seems odd to me that i need to iterate over the entire collection of self.filters, invoking the observables on each one, just in order to subscribe to changes in members of self.filters. I'd rather just declare it observable in the first place and attach my callback to self.filters.subscribe(), but that doesn't seem to work...
@djb In short: no. The filters array on self is an observable list of filters (possible name confusion), but these objects each have a list of selections (color can be "white" AND "red"). This is the array whose changes we actually want to subscribe to, as its the one that changes when you select the options of a filter. The only way to have just the subscription would be for the list of selected filters to be on the main viewmodel as its own ObservableArray, and I can't think of a good way to do that. Making the self.filters observable doesn't make all of its properties observable.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.