Bryan Dellinger
Bryan Dellinger

Reputation: 5294

live table search in knockout (calling function on keyup)

sorry I am a bit new to knockout I am simply trying to do a live search on a table however I can't quite seem to get my model's function to fire on the key up event of the search box. here is the fiddle.

http://jsfiddle.net/LkqTU/26466/

here is the code.

<div class="container">
    <table class="table table-condensed  table-hover">
        <thead>
            <tr>
                <th>First Name</th>
                <th>Last Name</th>
                <th>Department</th>
            </tr>
        </thead>
        <tbody data-bind='foreach: employees'>
            <tr>
                <td data-bind='text: firstName'></td>
                <td data-bind='text: lastName'></td>
                <td data-bind='text: department'></td>
            </tr>
        </tbody>
    </table>
     <h2 class="text-center">Search</h2>

    <div class="form">
        <div class="form-group">
            <label>first name:</label>
            <input type="search" class="form-control" data-bind="value: query valueUpdate: 'keyup' event: { keyup: search }" autocomplete="off" />
        </div>
    </div>
</div>

here is the javascript

function employee(firstName, lastName, department) {
    this.firstName = ko.observable(firstName);
    this.lastName = ko.observable(lastName);
    this.department = ko.observable(department);
    this.isVisible = ko.observable(true);

}

function model() {
    var self = this;
    this.employees = ko.observableArray("");
    this.query = ko.observable("");
    this.search = function () {

        $.each(self.employees, function (i, item) {
            item.isVisible(false);
            if (item.firstName().toLowerCase().indexOf(this.query().toLowerCase()) >= 0) {
                item.isVisible(true);
            }

        });
    };
}

var mymodel = new model();

$(document).ready(function () {
    loaddata();
    ko.applyBindings(mymodel);
});

function loaddata() {
    mymodel.employees.push(new employee("Bob", "Jones", "HR"));
    mymodel.employees.push(new employee("Mary", "Smith", "HR"));
    mymodel.employees.push(new employee("Greg", "Black", "Finance"));
}

Upvotes: 1

Views: 4841

Answers (1)

Karl Anderson
Karl Anderson

Reputation: 34844

I would recommend using the power of the observable array to show/hide the results of the search, rather than keeping track of which items in the list are visible/hidden in your employee function/object. One way to achieve that is through the computed observable, like this:

self.filteredEmployees = ko.computed(function () {
    var filter = self.query().toLowerCase();

    if (!filter) {
        return self.employees();
    } else {
        return ko.utils.arrayFilter(self.employees(), function (item) {
            return item.firstName().toLowerCase().indexOf(filter) !== -1;
        });
    }
});

Whenever any observable inside of the computed is updated, then the computed itself is re-evaluated. So in your case, this effectively puts a subscription on your search text box value (query observable in the view model).

The other change is that your markup will now need to foreach bind to the filteredEmployees computed instead of the actual employees observable array.

The logic inside of the computed does the following:

  1. Checks to see if anything was typed into the search input text box (query observable). If not, then the entire employees observable array is returned from the computed and bound to the foreach binding.
  2. If there was something typed into the search input text box, then an internal Knockout helper function named arrayFilter is used to loop through each of the items in the employees observable array. Each iteration uses the employee's first name (firstName observable) to compare against the lower case version of that name to the lower case version of the text typed into the search input text box (query observable). The nested return syntax might look a little strange at first, but the inner return is populating the observable array that the outer return is using as the value returned for the computed observable. The filtered returned observable array might be everything, nothing or something in between, but it is not touching the actual employees observable array; a new/different object is returned from the computed observable.

Note - The filter variable inside of the computed observable captures the lower case value of what was typed in by the user. This eliminates the need to force the search input text box value to lower case on each loop iteration (saving time and resources).

One last markup change, instead of using the value binding and the updateValue binding, the updated jsFiddle now uses the newer textInput binding. This is the recommended approach for newer versions of Knockout (3+). It is more efficient and supports copying/pasting/cutting into text boxes, which the value binding and updateValue binding struggled with.

Here is the markup change for the textInput binding:

<input type="search" class="form-control" 
       data-bind="textInput: query" autocomplete="off" />

Using multiple bindings

Your posted code was missing , (commas) to separate multiple bindings on the same element.

This is your posted code:

<input type="search" class="form-control" 
       data-bind="value: query valueUpdate: 'keyup' event: { keyup: search }"
       autocomplete="off" />

It should have been this:

<input type="search" class="form-control" 
       data-bind="value: query, valueUpdate: 'keyup', event: { keyup: search }"
       autocomplete="off" />

Note - I have updated your jsFiddle

Upvotes: 6

Related Questions