Question

I am stuck on filtering an observableArray in Knockout. I'm displaying a list of people and want to have a list of checkboxes that updates the list of people in real time based on what checkboxes are checked (ex: age: 18 -22, 23-30, 31-36, etc.)

I read Ryan Niemeyer's article on utility functions but I'm still pretty much stuck. I don't get how to update the observable array with the then filtered items.

I'm using Durandal and DozerJS and here's my markup and shell file with what I currently have

<section class="main">
  <div class="main--contain">
    <a data-bind="click: find25to35">Between 25 and 35</a>

    <form>
        <input type="search" name="search" placeholder="Search" class="search-input">
    </form>

    <ul class="user-list" data-bind="foreach: people">
        <li>
            <h2 data-bind="text: username"></h2>
            <h2 data-bind="text: firstname + lastname"></h2>
        </li>
    </ul>
</div>

define(['knockout'], function (ko) {

  var ctor = {
    people: ko.observableArray(),

    activate: function () {
      var self = this;

      var request = $.ajax({
        url: '/api/users/',
        type: 'GET'
      });

      request.done(function (res) {
        if (res.data.length) {
          res.data.reverse();
          for (var i = 0, z = res.data.length; i < z; i++) {
            self.people.push(res.data[i])
          }
        }
      })
    },

    find25to35: function () {
      var self = this;

      self.people = [];

      ko.utils.arrayForEach(self.people(), function(person) {
        if (person.age >=18 && person.age <=25) {
          self.people.push(person);
        }
      });
}
  };

  return ctor;

});
Was it helpful?

Solution

The following line will not work...

<h2 data-bind="text: firstname + lastname"></h2>

Knockout is smart but not that smart. You'll need to add a computed observable to your person view model like so...

function Person(person) {
    var self = this;

    self.username = ko.observable(person.userName);
    self.firstname = ko.observable(person.firstName);
    self.lastname = ko.observable(person.lastName);
    self.age = ko.observable(person.age);
    self.name = ko.computed(function () {
        return self.firstname() + self.lastname();
    });
}

Which changes your bindings to...

<ul class="user-list" data-bind="foreach: people">
    <li>
        <h2 data-bind="text: username"></h2>
        <h2 data-bind="text: name"></h2>
    </li>
</ul>

Then you'd probably want to add a filtered person list which loads all people matching the criteria, starting with all people at first, then clearing it and reloading it with people who match your criteria. What if someone wants to find people by a new set of criteria? Will you really want to make an AJAX call to fetch everyone again? My experience is that unless you're running a complex custom search, it's better to just get the data once and let users manipulate it. This means you'd end up with an array similar to this...

filteredPeople: ko.observableArray([]),

// -- your other relevant code goes here ---

find25to35: function () {
  var self = this;

  // set how many people are currently in array
  var count = filteredPeople().length;

  // loop through array to get rid of all elements to clear it,
  // making sure we're not modifying the collection as we are 
  // iterating through it which creates pointer errors
  for (var i = 0; i < count; i++) {
      filteredPeople().pop();
  }

  ko.utils.arrayForEach(self.people(), function(person) {
      if (person.age() >= 25 || person.age() <= 35) {
          filteredPeople.push(person);
      }
  });
}

Don't forget those parentheses on Knockout observables since those observables are really methods which return an object and you want them to return what you put in them, not the minified (or the full) methods for keeping track of their current values which will then cause an Undefined error. Oh and when you apply your bindings, just to be safe, since you're using a top level function, it may behoove you to add a reference to it like so...

<a data-bind="click: $root.find25to35">Between 25 and 35</a>

And the bindings for your person list change to reflect the filtering...

<ul class="user-list" data-bind="foreach: filteredPeople">

Finally, consider making the function more dynamic in order to be able to reuse it for other search criteria, otherwise you're violating the DRY principle. Consider the following...

<a data-bind="click: $root.findByAge($data, minAge, maxAge)">Find By Age</a>

... and going from there to make your inputs functional and reusable. And keep in mind that they don't need to be in a form if you use jQuery to get their values.

EDIT NOTES: Realized that I forgot to clear the right observable and didn't notice that the ages were off in the method causing all the trouble. Code fixed.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top