In this final part in our knockout mini-series, we'll add a couple more feature to the simple contacts app that we've built over the course of the last two tutorials. We've already covered the core fundamentals of the library – data-binding, templating, observables and dependant observables – so this part will consolidate what we've learned so far.
One of the features that we'll add in this part is the ability to filter the displayed list of contacts by the first letter of their name - quite a common feature that can be difficult to execute manually. Additionally, a reader of part two in this series asked how difficult it would be to add a search feature using Knockout, so we'll also add a search box to the UI that will allow a subset of contacts which match a specific search term to be displayed. Let's get started.
Round 1 – Getting Started
We'll begin by adding the new mark-up to the view. In the index.html
file from the previous tutorial, add the following new mark-up to the start of the <body>
tag:
<div id="alphaFilter"> <span>Filter name by:</span> <ul data-bind="template: 'letterTemplate'"></ul> <a id="clear" href="#" title="Clear Filter" data-bind="click: clearLetter, css: { disabled: filterLetter() === '' }">Clear filter</a> <fieldset id="searchForm"> <span>Search for:</span> <button data-bind="click: setTerm, disable: filterTerm" type="button">Go</button> <input id="term"> <a data-bind="visible: filterTerm, click: clearTerm" title="Clear search" href="#">x</a> </fieldset> </div> <script id="letterTemplate" type="text/x-jquery-tmpl"> {{each(i, val) letters}} <li> <a href="#" title="Filter name by ${ val }" data-bind="click: function() { filterLetter(val) }, css: { disabled: val === filterLetter() }"> ${ val } </a> </li> {{/each}} </script>
We start with a simple outer container to hold our new UI elements, which we give an id
for styling purposes. Inside is a <span>
containing an explanatory label for the letters used to filter the contacts by name, followed by an empty <ul>
element that we bind to the letters
template using the data-bind
attribute.
Following the list is a link; this link is used to clear the filter and has two bindings: the first is a click
binding, which is linked to a method on our viewModel
that we'll add in a moment. The second binding is the css
binding, which is used to add the class name disabled
to the element when a filtering letter has not been selected.
The search component of our UI uses a <fieldset>
with an id
(also for styling) which contains an explanatory text label, a <button>
element that will trigger the search, the <input>
that the search term will be typed into, and a link that can be used to clear the search.
The <button>
uses the click
and disable
bindings; the click
binding is used to trigger the search and the disable
binding will disable the button when the filterTerm
equals an empty string (which equates to false
). The clearing link also has two bindings: visible
and click
. The visible
binding is used to only display the link when a search has been performed, and the click
binding is used to clear the search.
Next we add the letters
jQuery template that is used to create the letters used to filter by the first letter of each contact's name. As with the numeric paging from the last tutorial, we use the jQuery tmpl
syntax here instead of Knockout's templating functionality. This means that the whole template will be re-rendered when one of the items changes, but in this example that doesn't impact performance too much.
We use the {{each}}
template tag and will make use of the second parameter, val
, which is passed to the template on each item in the array the template consumes, which will correspond to the first letter of each contact's name (we'll see how this array is generated when we update our viewModel
shortly).
For each item in the array
, we create an <li>
and an <a>
element. The <a>
element uses the val
parameter passed into the template function to set the title
attribute of the link, and its text content. We also add click
and css
bindings. The click
binding sets the filterLetter viewModel
property (which will be an observable) to the value of the link that was clicked. The css
binding just adds the disabled
class in the same way that we did with the clearing <a>
, but this time, the class is applied if the current element's val
is equal to the filterLetter
property.
Although you won't be able to run the page at this point, the filtering and search components will appear like this once the necessary code has been added to the viewModel
:
Round 2 – Updating the viewModel
To wire up the elements we just added, we first need to add some new properties and methods to our viewModel
. These can go after the navigate
method from the last part of the tutorial (don't forget to add a trailing comma after navigate
):
filterLetter: ko.observable(""), filterTerm: ko.observable(""), clearLetter: function () { this.filterLetter(""); }, clearTerm: function () { this.filterTerm(""); $("#term").val(""); }, setTerm: function () { this.filterTerm($("#term").val()); }
We'll also need a few new dependentObservables
, but we'll add them in a moment. First, we add two new observable properties: filterLetter
, which is used to keep track of the current letter to filter by, and filterTerm
, which keeps track of the current search term. Both are set to empty strings by default.
Next, we add several methods; the first method, clearLetter
, sets the filterLetter
observable back to an empty string, which will clear the filter, and the second method, clearTerm
, sets the filterTerm
observable back to an empty string, which will clear the search. This method will also remove the string entered into the text field in the view. The last new method, setTerm
, is used to obtain the string entered into the text field and add it to the filterTerm
observable.
Round 3 – Filtering by Search Term
Now that we have some new observable properties, we need to add some functions that will monitor these properties and react when their values change. The first dependentObservable
is used to filter the complete set of contacts and return an object containing only the contacts that contain the search term:
viewModel.filteredPeopleByTerm = ko.dependentObservable(function () { var term = this.filterTerm().toLowerCase(); if (!term) { return this.people(); } return ko.utils.arrayFilter(this.people(), function (person) { var found = false; for (var prop in person) { if (typeof (person[prop]) === "string") { if (person[prop].toLowerCase().search(term) !== -1) { found = true; break; } } } return found; }); }, viewModel);
Within the function, we first store the search term in lowercase so that searches are not case-sensitive. If the search term equates to false
(if it is an empty string), the function will return the people
array
. If there is a search term, we use the arrayFilter()
Knockout utility function to filter the people
array
. This utility function takes the array
to filter, and an anonymous function that will be executed for each item in the array
being filtered.
Within our anonymous function, we first set a flag variable to false
. We then cycle through each property that the current array
item contains. We check that the current property is a string, and if so, we determine whether the property contains the search term. This is done by converting the property to lowercase and then using JavaScript's native search()
method. If the search()
method does not return -1
, we know that a match has been found, and so we set our flag variable to true
and break out of the for
loop with the break
statement.
After the for
loop has completed (or we have broken out of it with a match), the flag variable will be returned and will either be true
or false
. The arrayFilter
utility method will only include items from the original array in the array it returns if the anonymous function executed for each item returns true
. This provides an easy mechanism for returning a subset of the people
array to be consumed by other dependentObservables
.
Round 4 – Building the Letter Filter
Our next dependentObservable
is used to build the array of letters that the letters
template consumes in order to add the letter links to the UI:
viewModel.letters = ko.dependentObservable(function () { var result = []; ko.utils.arrayForEach(this.filteredPeopleByTerm(), function (person) { result.push(person.name.charAt(0).toUpperCase()); }); return ko.utils.arrayGetDistinctValues(result.sort()); }, viewModel);
In this dependentObservable
, we first create an empty array called result
. We the use the arrayForEach
Knockout utility method to process each item in the array returned by the previous dependentObservable
– filteredPeopleByTerm
. For each item we simply push the first letter of each item's name
property, in uppercase, to the result
array. We then return this array after passing it through the arrayGetDistinctValues()
Knockout utility method and sorting it. The utility method we use here filters the array and removes any duplicates.
Round 5 – Filtering by Letter
The last dependentObservable
we need to add filters the contacts by letter and is triggered when the filterLetter
observable changes value:
viewModel.filteredPeople = ko.dependentObservable(function () { var letter = this.filterLetter(); if (!letter) { return this.filteredPeopleByTerm(); } return ko.utils.arrayFilter(this.filteredPeopleByTerm(), function (person) { return person.name.charAt(0).toUpperCase() === letter; }); }, viewModel);
In this dependentObservable
we first store the contents of the filterLetter
observable in an array. If the letter
variable equates to false
(e.g. if it's an empty string) we simply return the array that the filteredPeopleByTerm()
method returns without modifying it.
If there is a letter to filter by, we use the arrayFilter()
utility method again to filter the array returned by filteredPeopleByTerm
. This time we convert the first letter of each item's name
property to uppercase and return whether it is equal to letter. Remember, items will only remain in the array we are filtering if the anonymous function returns true
.
Round 6 – Updating the Paging
In the last tutorial in this mini-series, we added the paging feature, which operated directly on the people
array. If we want the paging to work with our new filtering functionality, we'll need to update the showCurrentPage dependentObservable
from the last article. All we need to do is change the return
statement at the end of the function so that it returns a slice of the array returned by the filteredPeople() dependentObservable
instead of the people
array:
return this.filteredPeople().slice(startIndex, startIndex + this.pageSize());
At this point, we should now be able to run the page and filter the displayed contacts by a letter or by a search term. The two new features aren't mutually-exclusive, so we can filter the contacts by a letter and then search the filtered list further using a search term. Or vice-versa – filtering a searched set of contacts. And our paging will still keep up with the currently displayed set of contacts.
Post Fight Review
In this final chapter of this series, we consolidated what we know about using Knockout by adding filtering by letter or search term features to let users view a subset of the data held in the viewModel
. As before, adding these new features to our application is so much easier to do with Knockout than it would be if we were trying to maintain our views and viewModels
manually using jQuery alone.
As well as keeping our UI and data in sync with ease, we also get a range of utility functions including arrayGetDistinctValues()
, and arrayFilter()
which we can use to save ourselves some manual coding when performing common tasks.
This now brings us to the end of the series, but I hope that it isn't the end of your experience with Knockout itself; the library is a fantastic addition to any developer's toolkit and makes creating fast, engaging interactive applications with JavaScript much easier.
Comments