The client/app/contacts.js
file defines our basic contact object. Let's go over it piece by piece.
It starts with a standard declaration of observable properties with some default values. There are a lot of reasons to organize code in a variety of ways, but for the smaller models, I prefer to keep all of their persistable properties together at the top:
Next is the displayName
property, some simple logic to generate a nice "title" for UI display. The JavaScript or operator (||
) is used here to ensure we don't try to read the length
property on a null
or undefined
value by returning a default value in case all the names are empty. This essentially makes it a null-coalescing operator when used during an assignment:
Next is a utility method to update the model that accepts an object and merges in its properties. I generally put a similar method onto all of my models so that I have a standard way of updating them. Once again, we are using ||
as a safety net, in case the method is called without a parameter (in the real world, you would want a stronger check, one that ensured update
was an object and not a primitive value or an array):
Also note that after defining the update
function, the model calls it with the constructor argument. This lets the constructor provide the ability to create a new model from existing data and partial data as well. This is very useful when deserializing data, for example, JSON from an Ajax request.
Lastly, we have the toJSON
method. The standard JSON.stringify
method in JavaScript will look for this method to allow an object to control how it is serialized. As Knockout's ko.toJSON
calls JSON.stringify
underneath after it unwraps all the observables so that the serialization gets values and not functions.
As the serialized form of our model is the one we will try to persist, usually by sending it to the server with Ajax, we don't want to include things such as our computed display name. Our toJSON
method override takes care of this by just deleting the property:
The copy with ko.toJS
is important. We don't want to delete displayName
from the actual model; we only want it removed from the serialized model. If we made the variable with copy = self
, we would just have a reference to the same object. The ko.toJS
method is a simple way to get a plain JavaScript copy that we can safely delete properties from without affecting the original object.
The Contacts page viewmodel
The client/app/contactspage.js
file defines the viewmodel for the Contacts page. Unlike our contacts model, the page does a lot more than expose some observable properties, and it isn't designed to be constructed from existing data either. Instead of taking an object to control its starting values, which doesn't make much sense for a page, the constructor's argument is designed for dependency injection; its constructor arguments take in its external dependencies.
In this example, dataService
is a dependency used by the page viewmodel:
Very briefly, if you aren't familiar with dependency injection, it lets us define our page against an API (sometimes called a contract or interface) of methods to get and save data. This is especially useful for us, as in this sample application, we aren't using real Ajax but mocking it with an object that just writes to the DOM's local storage:
However, when we write the real Ajax service later, our ContactsPageViewmodel
doesn't need to change at all. We will just construct it with a different dataService
parameter. As long as they expose the same methods (the same API) it will just work.
The first section inside the constructor is for the contacts list. We expose an observable array and get the contacts from our data service:
We are passing callback to the getContacts
call because our data service provides an asynchronous API. When the data service has finished getting our contacts, it will call the callback with them. All our callback needs to do is put them into the contacts
array.
The next block of code is to control the
CRUD (Create, Read, Update, Delete) operations for individual contacts. First, we expose an observable object that we will use for all edits:
Our UI is going to bind an edit form against the entryContact
property. The entry contact property is pulling a double duty here; it contains the contact that is being created or edited, and it indicates that editing is occurring. If the entry contact is null, then we aren't editing; if it has an object, then we are editing. The UI will use with
and if
bindings to control which content to show based on this logic.
The newEntry
and cancelEntry
functions provide the UI with a means to switch between these two states.
For editing existing contacts, we just expose another function that takes a contact and sets the entry contact to it:
The last thing we need for real editing is the ability to persist our changes. As in the real world, we have three scenarios, namely creating new objects, saving existing objects, and deleting existing objects.
Creating and updating are both going to be done using the entryContact
property, and we want to be able to bind the same form for both, which means we need to target a single function:
Internally, our saveEntry
method checks for a non-default id
value to determine whether or not it's making a new object or updating an existing one. Both are calls to the data service using the entry contact with a callback to clear the entryContact
property out (as we are done with editing). In the creation case, we also want to add the newly created contact to our local list of contacts before emptying the entry contact:
You might think that the contact is going to be null out by the second line, but that is not the case. The entryContact
property is an observable and its value is a contact. The first line reads this value and pushes it into the contacts
array. The second line sets the value of the entryContact
property to null
; it does not affect the contact that was just pushed. It's the same as if we had set a variable to null after adding it to an array. The variable was a reference to the object, and making the variable null removes the reference, not the object itself.
The delete function is simple by comparison:
It's going to take an existing contact, like editContact
did, and call the data service. As we are deleting the contact, the only thing we need is the id
property. The callback will remove the contact from the list of contacts when the service is done, using the remove
function provided on all observable arrays by Knockout.
The last piece of functionality on the page is the search mechanism. It starts with an observable to track the search and a function to clear it out:
The query
property is going to be used to filter out any contacts that don't have a matching or partially-matching property. If we wanted to be as flexible as possible, we could search against every property. However, since our list of contacts is only going to show our computed displayName
and phone number, it would look odd to return results matching on properties we didn't show. This is the computed observable from the code sample that filters the contacts list:
Note
If you want to filter all of the contact's properties, they are listed in the repository code as comments. They can easily be re-enabled by uncommenting each line.
First, we check to see whether the query is empty, because if it is, we aren't going to filter anything so we don't want to waste cycles iterating the contacts anyway.
Before starting, we call the toLowerCase()
function on the query to avoid any case sensitivity issues. Then, we iterate on the contacts. Knockout provides several utilities methods for arrays (among other things) on the ko.utils
object. The arrayFilter
function takes an array and an iterator function, which is called on each element of the array. If the function returns true
, arrayFilter
will include that element in its return value; otherwise it will filter the element out. All our iterator needs to do is compare the properties we want to keep the filter on (remembering to put them in lowercase first).
Now if the UI binds against displayContacts
, the search functionality will filter the UI.
However, we might experience poor performance with a large list of contacts if we are looping through them all every time the query is updated, especially if the query updates every time a key is pressed. To address this, we can use the standard Knockout rateLimit
extender on our filtered computed to stop it from updating too frequently:
This extender has two modes: notifyAtFixedRate
and notifyWhenChangesStop
. These two options will throttle or debounce the computed.
This lets us control how often the computed re-evaluates itself. The preceding example will only re-evaluate the computed once all dependencies have stopped changing for 100 ms. This will let the UI update when the query typing settles down while still appearing to filter as the user types.
A philosophical note on a model versus a viewmodel
The line between model and viewmodel in client-server application can get blurry, and even after reading Knockout's documentation (http://knockoutjs.com/documentation/observables.html) it can be unclear whether or not our contact object is really a model or viewmodel. Most would probably argue that it is a viewmodel as it has observables. I like to think of these smaller objects, which are barely more than their persisted data, as models and to think of viewmodels as the objects containing operations and view representations, such as our Contacts page viewmodel removeContact
operation or the entryContact
property.
Normally, you would use an Ajax call, probably with jQuery, to retrieve data and submit data to and from the server. Because this is a book on Knockout and not Node.js, I wanted to keep the server as thin as possible. From the "Mastering Knockout" perspective, whether we call a JavaScript object making Ajax requests or store it in the DOM is immaterial. As long as we are working with something that looks and functions like an asynchronous service, we can explore how Knockout viewmodels might interact with it. That being said, there is some functionality in the data service that would be used in an Ajax data service object, and it is interesting from a Knockout application development perspective.
You might have noticed in the previous section that when the Contacts page view model communicated with the data service, it wasn't dealing with JSON but real JavaScript objects. In fact, not even plain JavaScript objects but our contact model. This is because part of the data service's responsibility, whether it's a mock or a real Ajax service, is to abstract away the knowledge of the service mechanisms. In our case, this means translating between JSON and our Knockout models:
This is the createContact
method from our mock data service if it was rewritten to use real Ajax (this code is in the mockDataService.js
file as a comment). The data service is part of our application, so it knows that it's working with observable properties and that it needs to translate them into plain JavaScript for jQuery to properly serialize it, so it unwraps the contact that it's given with ko.toJS
. Then, in the done
handler, it takes the id
that it gets back from the server's response and updates the contact's observable id
property with it. Finally, it calls the callback to signify that it's done.
You might wonder why it doesn't pass contact
as an argument to the callback. It certainly could, but it isn't necessary. The original caller already had the contact, and the only thing that the caller is going to need is the new id
value. We've already updated the id
, and as it's observable, any subscriber will pick that new value up. If we needed some special handling before setting the id
value, that would be a different case and we could raise the callback with id
as an argument.
Hopefully, you have already played with the application a bit. If you haven't, now is the time. I'll wait.
You would have noticed that when adding or editing contacts, the contacts list is removed. What you might not have noticed is that the URL doesn't change; the browser isn't actually navigating when we switch between these two views. Though they are in the same HTML file, these two different views are mostly independent and they are controlled through a with
and an ifnot
binding.
This is what is shown when adding or editing contacts:
Because the with
binding is also implicitly an if
binding, the entire form is hidden when the entryContact
property is null or undefined.
The rest of the form is pretty straightforward. A submit
binding is used so that clicking the save button or hitting the enter key on any field calls the submit handler, a header showing the display name, value bindings for each field, a save button with type="submit
" (so that it uses the submit handler), and a cancel button that binds to $parent.cancelEntry
. Remember, the $parent
scope is necessary because the with
binding creates a binding context on the entry
contact and cancelEntry
is a function on ContactPageViewmodel
.
The list starts with an ifnot
binding on the entryContact
property, ensuring that it only shows in the case that the previous form is hidden. We only want one or the other to be seen at a time:
The search input has a value
binding as well as the valueUpdate
option. The value update option controls when the value
binding reports changes. By default, changes are reported on blur, but the afterkeydown
setting causes changes to be reported immediately after the input gets a new letter. This would cause the search to update in real time, but remember that the display contacts have a rateLimit
extender that debounces the updates to 100 ms.
Next to the search box is a button to add a new contact. Then, of course, the list of contacts is bound with a foreach
binding on the displayContacts
property. If it was bound against contacts
directly, the list would not show the filtering. Depending on your application, you might even want to keep the unfiltered contacts list private and only expose the filtered lists. The best option really does depend on what else you're doing, and in most cases, it's okay to use your personal preference.
Inside the contacts list, each item shows the display name for the phone number, with a button to edit or delete the contact. As foreach
creates a binding context on the individual contact and the edit and delete functions are on the parent, the click
binding uses the $parent
context property. The click
binding also sends the current model to each of the edit and delete functions, so that these functions don't have to try to find the right JavaScript object by looking through the full list.
That's really all there is to the application. We've got a list view with searching that switches to a view that's reused easily for both editing and creating.