Search code examples
javascriptknockout.jscomputed-observable

knockout.js computed function to create two arrays?


I have an array of objects that a user can perform search on. I'm using a ko.computed function based on the search to create another array of matched items for display.

self.matchedRecords = ko.computed(function() {
  return ko.utils.arrayFilter(self.transponders(), function(r) {
    return r.title.toLowerCase().indexOf($('.search-input').val().toLowerCase()) > -1
  }
};

This works great and I'm really impressed with the performance so far.

This issue is I also need the "unmatched" records because I have to perform an addition operation on them in some cases (60% of the time). I don't really want to create a second ko.computed function because then I have to run through this array a second time every time a search is performed.

So, my question: Is there a way that I can use the same ko.computed to create a second array of unmatched items? Basically run through the array and put each item in the matched or unmatched array...

If not, is it faster to: 1) create a second ko.computed to get the unmatched items from my array as the user searches; or 2) write an arrayDiff function and determine the unmatched items on demand if I need them.

Cheers!


Solution

  • If you are worried about performance, you could have a non-observable array that you populate while you iterate the search results in your computed. Also note that you are repeatedly selecting using jQuery inside your loop, which I think negates any KO-caused slowdowns.

    self.missedRecords = [];
    
    self.matchedRecords = ko.computed(function() {
        var searchQuery = $('.search-input').val().toLowerCase(),
            transponders = self.transponders(),
            matched = [];
    
        // Clear out missed records
        self.missedRecords.length = 0;
    
        _.each(transponders, function(transponder) {
            if (transponder.title.toLowerCase().indexOf(searchQuery) >= 0) {
                matched.push(transponder);
            } else {
                self.missedRecords.push(transponder);
            }
        });
    
        return matched;
    });
    

    I used _.each from Underscore to keep the code shorter. The drawback of this approach is that changes to missedRecords can't (reliably) be bound to the UI (e.g. in case you have a foreach binding).

    If you do need the missedRecords array to be observable, and still want to keep things fast(er), you could do something like this:

    self.missedRecords = ko.observableArray([]);
    
    self.matchedRecords = ko.computed(function() {
        var searchQuery = $('.search-input').val().toLowerCase(),
            transponders = self.transponders(),
            matched = [],
            missed = [];
    
        _.each(transponders, function(transponder) {
            if (transponder.title.toLowerCase().indexOf(searchQuery) >= 0) {
                matched.push(transponder);
            } else {
                missed.push(transponder);
            }
        });
    
        // Clear out missed records, without triggering subscriptions
        self.missedRecords().length = 0;
    
        // Copy the local missed array to the KO observable array
        // This will NOT trigger notifications
        ko.utils.arrayPushAll(self.missedRecords(), missed);
    
        // Tell KO that the observable array has mutated - this will trigger changes
        // to anything observing the missedRecords array
        self.missedRecords.valueHasMutated();
    
        return matched;
    });
    

    You could also skip computed altogether and just subscribe to changes to change the state of your arrays. For example:

    self.missedRecords = ko.observableArray([]);
    self.matchedRecords = ko.observableArray([]);
    
    self.transponders.subscribe(function(newTransponders) {
        var matched = [],
            missed = [];
    
        _.each(newTransponders, function(transponder) {
            // Populate matched/missed local arrays
        });
    
        // Copy the arrays to the observableArray instances using the technique above
    });