Book Image

Mastering KnockoutJS

By : Timothy Moran
Book Image

Mastering KnockoutJS

By: Timothy Moran

Overview of this book

Table of Contents (16 chapters)
Mastering KnockoutJS
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

The Contacts List application


It's time to start putting these concepts together into a usable application. Isolated samples can only take you so far. We are going to cover the application in the cp1-contacts branch in detail. The application's functionality is all on the Contacts page, which you can get to from the navigation bar in your browser. Before we start digging into the code, I encourage you to play around with the application a bit (it does persist data). It will help in understanding the relationships in the code.

Overview

The application has three main JavaScript objects:

  • The contact model

  • The Contacts page viewmodel

  • The mock data service

The application only uses the HTML in the index.html file, but the two sections are mostly independent.

  • The entry form (create and edit)

  • The contacts list

The JavaScript code in the example follows the Immediately-Invoked Function Expression (IIFE) pattern (sometimes pronounced "iffy") to isolate code from the global scope, and a namespace called app to share code between files:

(function(app, $, ko) {
  /* CODE IN HERE */
})(window.app = window.app || {}, jQuery, ko);

This is definitely not the only way to organize JavaScript code, and you may have a pattern you prefer. If you want to understand this pattern better, here are a few online resources:

The contact model

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:

app.Contact = function(init) {
  var self = this;
  self.id = ko.observable(0);
  self.firstName = ko.observable('');
  self.lastName = ko.observable('');
  self.nickname = ko.observable('');
  self.phoneNumber = ko.observable('');
  /* More below */

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:

self.displayName = ko.computed(function() {
      var nickname = self.nickname() || '';
      if (nickname.length > 0)
        return nickname;
      else if ((self.firstName() || '').length > 0)
        return self.firstName() + ' ' + self.lastName();
      else
        return 'New Contact';
    });

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):

//Generic update method, merge all properties into the viewmodel
self.update = function(update) {
  data = update || {};
  Object.keys(data).forEach(function(prop) {
    if (ko.isObservable(self[prop]))
      self[prop](data[prop]);
  });
};

//Set the initial values using our handy-dandy update method.
self.update(init);

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:

//Remove unwanted properties from serialized data
    self.toJSON = function() {
      var copy = ko.toJS(self);
      delete copy.displayName;
      return copy;
    };

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:

app.ContactsPageViewmodel = function(dataService)

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:

ko.applyBindings(new app.ContactsPageViewmodel(app.mockDataService));

Note

For more information on the DOM local storage, see the page on the Mozilla Developer Network at https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/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:

self.contacts = ko.observableArray();

dataService.getContacts(function(contacts) {
  self.contacts(contacts);
});

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:

self.entryContact = ko.observable(null);

    self.newEntry = function() {
      self.entryContact(new app.Contact());
    };
    self.cancelEntry = function() {
      self.entryContact(null);
    };

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:

self.editContact = function(contact) {
      self.entryContact(contact);
    };

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:

self.saveEntry = function() {
  if (self.entryContact().id() === 0) {
    dataService.createContact(self.entryContact(), function() {
      self.contacts.push(self.entryContact());
      self.entryContact(null);
    });
  } else {
    dataService.updateContact(self.entryContact(), function() {
      self.entryContact(null);
    });
  }
};

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:

self.contacts.push(self.entryContact());
self.entryContact(null);

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:

self.deleteContact = function(contact) {
      dataService.removeContact(contact.id(), function() {
        self.contacts.remove(contact);
      });
    };

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:

self.query = ko.observable('');
self.clearQuery = function() { self.query(''); };

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:

self.displayContacts = ko.computed(function() {  
  //No query, just return everything
  if (self.query() === '')
    return self.contacts();
  var query = self.query().toLowerCase();
  //Otherwise, filter all contacts using the query
  return ko.utils.arrayFilter(self.contacts(), function(c) {
    return c.displayName().toLowerCase().indexOf(query) !== -1 
        || c.phoneNumber().toLowerCase().indexOf(query) !== -1;
  });
});

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:

self.displayContacts = ko.computed(function() {
  /* computed body */
}).extend({
  rateLimit: {
    timeout: 100,
    method: 'notifyWhenChangesStop'
  }
});

This extender has two modes: notifyAtFixedRate and notifyWhenChangesStop. These two options will throttle or debounce the computed.

Note

If you aren't familiar with the throttling and debouncing functions, there is an excellent explanation with visuals at http://drupalmotion.com/article/debounce-and-throttle-visual-explanation.

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.

Mock data service

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:

createContact: function(contact, callback) {
  $.ajax({
      type: "POST",
      url: "/contacts",
      data: ko.toJS(contact)
    })
    .done(function(response) {
      contact.id(response.id);
      callback()
    });
}

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.

The view

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.

The edit form

This is what is shown when adding or editing contacts:

<form class="form" role="form" data-bind="with: entryContact, submit: saveEntry">
      <h2 data-bind="text: displayName"></h2>
      <div class="form-group">
        <label for="firstName" class="control-label">First Name</label>
        <input type="text" class="form-control" id="firstName"placeholder="First Name" data-bind="value: firstName">
      </div>
      <div class="form-group">
        <label for="lastName" class="control-label">Last Name</label>
        <input type="text" class="form-control" id="lastName" placeholder="First Name" data-bind="value: lastName">
      </div>
      <div class="form-group">
        <label for="nickname" class="control-label">Nickname</label>
        <input type="text" class="form-control" id="nickname" placeholder="First Name" data-bind="value: nickname">
      </div>
      <div class="form-group">
        <label for="phoneNumber" class="control-label">Phone Number</label>
        <input type="tel" class="form-control" id="phoneNumber" placeholder="First Name" data-bind="value: phoneNumber">
      </div>
      <div class="form-group">
        <button type="submit" class="btn btn-primary">Save</button>
        <button data-bind="click: $parent.cancelEntry" class="btn btn-danger">Cancel</button>
      </div>
    </form>

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.

Contacts list

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:

<div data-bind="ifnot: entryContact">
  <h2>Contacts</h2>
  <div class="row">
    <div class="col-xs-8">
      <input type="search" class="form-control" data-bind="value: query, valueUpdate: 'afterkeydown'" placeholder="Search Contacts">
    </div>
    <div class="col-xs-4">
      <button class="btn btn-primary" data-bind="click: newEntry">Add Contact</button>
    </div>
  </div>
  <ul class="list-unstyled" data-bind="foreach: displayContacts">
    <li>
      <h3>
        <span data-bind="text: displayName"></span> 
          <small data-bind="text: phoneNumber"></small>
        <button class="btn btn-sm btn-default" data-bind="click: $parent.editContact">Edit</button>
        <button class="btn btn-sm btn-danger" data-bind="click: $parent.deleteContact">Delete</button>
      </h3>
    </li>
  </ul>
</div>

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.