There are numerous ways to achieve this goal, this is my approach - It's simple, and it works – ‘nuff said.
The first thing we need to do is set up some sample data to work with. Lets deal with the MVC C# side of things first.
We create a data model MVC server side to generate some random data to send down to the server. To do this, construct a simple class that has some basic members.
- using Newtonsoft.Json;
-
- public string ClientAjaxLoader()
- {
- ClientModel cm = new ClientModel();
- ClientList cl = new ClientList();
- cl.Clients.AddRange(cm.GetClients());
- var jsonData = JsonConvert.SerializeObject(cl);
- return jsonData;
- }
Setup - client side
As this is an MVC application, for simplicity of this article, we will go into the automatically generated index.cshtml, remove everything in there and add the bare bones code we require.
There are many Knockout tutorials out there to cover the basics – so here is just a quick explanation of how I set things up client-side.
Step 1: Create the main Knockout ViewModel. At this stage, this is extremely simple, containing an observable array of “Clients”,
- var viewModel = function () {
- var self = this;
- self.Clients = ko.observableArray([]);
- }
Step 2: create a Model that mirrors the ClientModel data incoming from the server.
<pre lang="> var ClientDetail = function (data) { var self = this; if (data != null) { self.ClientName = ko.observable(data.ClientName); self.Address = ko.observable(data.Address); self.Phone = ko.observable(data.Phone); self.Active = ko.observable(data.Active); self.Status = ko.observable(data.Status); } }
Step 3: As it stands, the ViewModel has to have data manually pushed to its Clients array. Let's change this so that the array can be populated by passing data in as a parameter.
- var viewModel = function (data) {
- if (data != null)
- {
- ko.mapping.fromJS(data, { Clients: clientMapping }, self);
- }
- var self = this;
- self.Clients = ko.observableArray([]);
We call the ko.mapping.fromJS, passing in a parameter of (a) the data packet (that will be received from the server), (b) instructions telling it how to unwrap the data using mapping, (c) where to put it (into itself).
Step 4: create the mapping function “ClientMapping” that will “unwrap” the JSON string that is generated server side.
- var clientMapping = {
- create: function (options) {
- return new ClientDetail(options.data);
- }
- };
This is the first mention we have in our code of the “ClientDetail” model we started off with and how it relates to the ViewModel. So what we are saying to the client is … you have an array of something. When data comes in, take that data, and look for a block of records **inside that data*** that is **called CLIENTS** and unwrap each record you find in that block, casting each record as model type “ClientDetail”. This is part of the gotcha, and important … remember, your data that comes down from the server, must be flagged with the name of the mapping filter text, otherwise it will not find it and your data will not map. Here it is again – don’t forget!
MVC Server side
- ClientList cl = new ClientList();
This contains a primary member called “Clients”:
- public List Clients {get; set;}
The client side ViewModel has a member that is an array called “Clients”:
- self.Clients = ko.observableArray([]);
And the two are linked together by the KO mapping procedure "{ Clients: clientMapping }"
- ko.mapping.fromJS(data, { Clients: clientMapping }, self);
Populating the Knockout observable array using Ajax
To get the data from server to client we will set up a function within the ViewModel itself, and call it from the click of a link.
- self.getClientsFromServer = function () {
- $.ajax("/home/ClientAjaxLoader/", {
- type: "GET",
- cache: false,
- }).done(function (jsondata) {
- var jobj = $.parseJSON(jsondata);
- ko.mapping.fromJS(jobj, { Clients: clientMapping }, self);
- });
- }
So this is very simple, we call the url “/home/ClientAjaxLoader” (which calls the domain used, in our test case “localhost”). In the “done” callback, we take the data received “jsondata”, and convert it to a Javascript object using “parseJSON”. Then we call the Knockout Mapping utility to unwrap and map the JSON data into our array of Clients.
To display the data, we need to hook up some html controls and bind the Knockout array. Create a simple table, and use the “for each” binding to fill it with Client detail data.
(it's not going to win any UX contests, but it works!)
Search and filter
Ok, here's the reason we came to this party – let the games begin!
This search and filter method is based on using the inbuilt “observable” behavior and two-way binding of knockout. In short, what we do is this:
- create a new member of the viewmodel that’s a simple observable that acts as our “search filter”
- create a computed member that observes the search filter, and when it changes, for each record in the clients array, compares the value in the array to the search value, and if it matches, returns that client record in an array of “matching records”
- hook up the new computer member in a “for each” to display the filtered results
Let's start it off simple and put a single search in place to find any client records that have a “ClientAddress” value that *contains* the value of our search item – in other words, a wildcard search.
Step 1: create a new observable member to hold our search string,
- self.search_ClientAddress = ko.observable('');
Step 2: create a new computed member that when our search string changes, scans through the client records and filters out just what we want,
- self.filteredRecords = ko.computed(function () {
- return ko.utils.arrayFilter(self.Clients(), function (rec) {
- return (
- (self.search_ClientAddress().length == 0 || rec.Address().toLowerCase().indexOf(self.search_ClientAddress().toLowerCase()) > -1)
- )
- });
- });
In this computed member, we use KO.UTILS.arrayFilter to scan through the Client's array, and for each record, make a comparison, and return only records from the Client's array that we allow through the net. Look at what's happening ... the arrayFilter takes a first parameter of the array to search within, and a second parameter of a function that carries out the filtering itself. Within the filtering function, we pass in this case a variable called "rec" - this represents the single array record object being examined at the time. So we are basically saying "scan through our client list, and for each record you encounter, test to see if the current record being examined should be let through the array filter
In this case we say, allow the record through, EITHER if there is ZERO data (length == 0) in the search string
- self.search_ClientAddress().length == 0
OR,
If the value of the search string is contained within the client record being scanned using “arrayfilter”
- rec.Address().toLowerCase().indexOf(self.search_ClientAddress().toLowerCase()) > -1
The arrayFilter method is an extremely powerful tool as we will see shortly.
The last thing to do to check if it works is to put an html control in pace and bind this to the new observable search string member (self.search_ClientAddress).
Address contains: <input data-bind=" value:=" valueupdate:="" />
Here we bind to the new member, and critically, use the “valueUpdate” trigger of ‘afterKeyDown’ to send a message to the search observable that the value has updated. This then triggers the computed member to do its thing.
To show that it's working, we will change our original display table from showing the “Clients” array of data, to showing the “filteredRecords” returned array.
The approach we are taking here is that our original data array of clients stays put, and we do any visual / manipulation on a copy of the data in the data set returned in the filtered search result.
So here is it is after entering values into the address search box. The ‘44’ value is found in any part of the “Address” field, therefore two records return.
Now that we know how the basics of search works, we can quickly build up a powerful combined search and filter functionality. We will add functionality to allow the user to search on the first part of the Client.Name, the Client.Address as before, and also filter to show within that search, only Active or Inactive client records, or those with a status value, say greater or equal to two.
Step 1: add more search observables,
- self.search_ClientName = ko.observable('');
- self.search_ClientActive = ko.observable();
- self.search_ClientStatus = ko.observable('');
Step 2: add html to set those values,
Step 3: some JQuery goodness to bind to the Click events on those links,
- $(function () {
- $('#showAll').click(function () {
- vm.search_ClientActive(null);
- vm.search_ClientStatus(null);
- });
- $('#showActive').click(function () {
- vm.search_ClientActive(true);
- });
- $('#showInActive').click(function () {
- vm.search_ClientActive(false);
- });
- $('#showStatus').click(function () {
- vm.search_ClientStatus(2);
- });
- });
(note the “show all” action resets the value of the search observables to null, thus triggering the list to show all records).
Step 4: Finally, we will expand out our computed member to include the parameters we have introduced above,
- self.filteredRecords = ko.computed(function () {
- return ko.utils.arrayFilter(self.Clients(), function (rec) {
- return (
- (self.search_ClientName().length == 0 || ko.utils.stringStartsWith(rec.ClientName().toLowerCase(), self.search_ClientName().toLowerCase()))
- &&
- (self.search_ClientAddress().length == 0 || rec.Address().toLowerCase().indexOf(self.search_ClientAddress().toLowerCase()) > -1)
- &&
- (self.search_ClientActive() == null || rec.Active() == self.search_ClientActive())
- &&
- (self.search_ClientStatus() == null || rec.Status() >= self.search_ClientStatus())
- )
- });
- });
So here it is all working nicely together..
Show all records,
Show only active,
Show active, with search on ClientName and ClientAddress,
Last filtering trick, add to the mix where the Client.Status value is greater or equal to 2,
Sorting the list
Its great to be able to apply a filter, and even cooler you can search within filter results – the only missing piece of the puzzle perhaps is the ability to sort. So here's a quick addition to make that happen:
Server side, I am going to add a “SortCode” field to our client model, and throw in some spurious data to fill the space that we can sort on.
- cm.Status = rnd.Next(0, 4);
-
- switch (cm.Status) {
-
- case 0:
- cm.SortCode = "AAA";
- break;
- case 1:
- cm.SortCode = "BBB";
- break;
- case 2:
- cm.SortCode = "CCC";
- break;
- case 3:
- cm.SortCode = "DDD";
- break;
- case 4:
- cm.SortCode = "EEE";
- break;
- default :
- break;
-
- }
Client-side, we add in a KO observable that the computed filtered array can monitor
- self.isSortAsc = ko.observable(true);
We add some markup/Jquery to turn that on/off
- <a href="#" id="flipSortCode">Flip sort code</a>
Next, we add the new observable to the computed function, and finally, chain a SORT function onto the computed member
- self.filteredRecords = ko.computed(function () {
-
- return ko.utils.arrayFilter(self.Clients(), function (rec) {
- return (
- (self.search_ClientName().length == 0 ||
- ko.utils.stringStartsWith(rec.ClientName().toLowerCase(), self.search_ClientName().toLowerCase()))
- &&
- (self.search_ClientAddress().length == 0 ||
- rec.Address().toLowerCase().indexOf(self.search_ClientAddress().toLowerCase()) > -1)
- &&
- (self.search_ClientActive() == null ||
- rec.Active() == self.search_ClientActive())
- &&
- (self.search_ClientStatus() == null ||
- rec.Status() >= self.search_ClientStatus())
- &&
- (self.isSortAsc() != null)
- )
- }).sort(
- function (a, b) {
- if (self.isSortAsc() === true)
- {
- var x = a.SortCode().toLowerCase(), y = b.SortCode().toLowerCase();
- return x < y ? -1 : x > y ? 1 : 0;
- }
- else {
- var x = b.SortCode().toLowerCase(), y = a.SortCode().toLowerCase();
- return x < y ? -1 : x > y ? 1 : 0;
- }
- }
- );
- });
So, clicking on the “flip sort” now sorts our list asc/desc as required.
Summary
This article has given you a whirlwind tour of using the very powerful “Ko.Utils.arrayFilter” feature to implement clean quick search and filter functionality using Knockout, with data server to the ViewModel via Ajax from a MVC backend. As always, the trick is in the details so don't forget the gotchas.
And there we have it - the final article in the KnockoutJS series. Hopefully you have learned a few tricks to help your front-end dev efforts!