Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Into the Ring with Knockout.js: The Title Fight

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

This post is part of a series called Into the Ring with Knockout.js.
Into the Ring with knockout.js: Part 2 - the Rematch

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 dependentObservablefilteredPeopleByTerm. 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.

Advertisement