By:Audrunas Matonis Posted On: Topic:Engineering

Knockout: A Real World Example

Introduction

Knockout is a fast, extensible and simple JavaScript library designed to work with HTML document elements using a clean underlying view model. It helps to create rich and responsive user interfaces. Any section of UI that should update dynamically (e.g., changing depending on the user’s actions or when an external data source changes) with Knockout can be handled more simply and in a maintainable fashion.

Knockout has no dependencies. It works without jQuery, Prototype.js, or any other JavaScript library, and it is compatible with all popular web browsers, such as Chrome, Firefox, Safari, Internet Explorer and Opera.

Knockout provides great documentation, amazing real-time tutorials and many live examples with source code. As compared to any other JavaScript engine or framework such as BackBone, JavaScript MVC, Ember or Angular, Knockout is a lightweight library while most of the others are frameworks. That’s why Knockout usually is faster (it depends on OS and browser). While most other JavaScript frameworks can bind to an entire HTML document, Knockout can bind to a specific DOM container instead of applying bindings globally.

Working with Knockout consists of several steps:

  • Get data model. In most cases, data will be returned from the remote server in JSON format with AJAX (Asynchronous JavaScript and XML) call. In the example below, our data will be stored in the static JavaScript object (demo.data-mock.js file).
  • Create View. View is a HTML template with Knockout bindings, using “data-bind” attributes. It can contain grids, divs, links, forms, buttons, images and other HTML elements for displaying and editing data.
  • Create View Model. View model is a pure-code representation of the data operations on a UI. It can have usual properties and observable properties. An observable property means that when it’s changed in the view model, it will automatically be updated in the UI.
  • Map data from data model to view model. In most cases, data in the data model are independent from UI and don’t have a concept of observables. In this step a map from the data model to the view model should be created. It can be done manually or using Knockout mapping plugin.
  • Bind view model to the view. When view model is initialized, it can be bound to part of the HTML document, or the whole HTML document.

Full source code, discussed below, can be found in the Devbridge Public GitHub Examples Repository. Please feel free to check it out and download it.

Real World Example

Let’s assume that you need to create a client side file manager. It should look like the image below:  

Blog 1

Required functions: search function, navigation function, display folders path, ability to rename a file or folder name within the grid, ability to add new file or folder, ability to delete file or folder with confirmation dialog, display a different icon for a different file type, attach to an Enter and Esc key.

Installing Knockout

To install Knockout simply reference the JavaScript file using a <script> tag somewhere on your HTML page. For example:


 
<script type="text/javascript" src="knockout-2.2.1.js"></script>

A script file can be downloaded from the Knockout downloads page or installed from NuGet via Visual Studio Package Manager Console:


 
       PM> Install-Package knockoutjs

When the script file is added to the HTML document, a global variable ko is created and all exposed functions can be reached using this global Knockout variable:

       ko.observableArray();
       ko.observable();
       ko.applyBindings();

Creating the view model

To render a list of files and folders in our example, we’ll need two types of the view model:

  • ListViewModel. It will contain a list of all the available items, list of visible items (sorted by sort column and filtered by current folder and search query), current path, search and sort fields, and current folder id.
  • ItemViewModel. It will contain a reference to the parent list view model and public properties: id, title, file extension, folder id. It will also contain helper properties, which determine if an item is a file or folder, and if an item is in edit mode.

Blog 5

View models can be created as anonymous objects:

var viewModel = {
       id = ko.observable(),
       title = ko.observable()
}

Another way to create view models is to declare a function which can be reused:

      function ItemViewModel() {
             var self = this;
             self.id = ko.observable();
             self.title = ko.observable();
      }

      var viewModel = new ItemViewModel ();

Using observables

Observable properties are one of the main benefits of Knockout. When a value of the observable property is updated, Knockout will update the UI automatically. The default value to the observable property can be passed as constructor parameter. To get a value of the observable property, it must be named as a function. To set a value, the value should be passed as function parameter:

      var title = ko.observable(‘Item Title’); // passing a value as parameter to constructor      
      var currentValue = viewModel.title(); // will return ‘Item Title’
      viewModel.title(‘New Item Title’); // set the value to ‘New Item Title’
      var newValue = viewModel.title(); // will return 'New Item Title'
Chaining syntax could be used too:
             viewModel
                    .id(1)
                    .title(‘Item Title’);

Observable arrays

If you want to detect and respond to changes on one object, you’d use observables. If you want to detect and respond to changes of a collection of things, use an observableArray. When new items are added, or when items are removed from array, they automatically appear and disappear in the UI. As mentioned before, the best practice is to create a view model with observable properties for each list item.

      function ListViewModel(json) {
             var self = this;
             self.items = ko.observableArray();

             for (var i = 0; i < json.length; i ++) {
                   var item = new ItemViewModel(json[i]);
                   self.items.push(item);
             }
      }

To read data from an observable array, you have to name it as a function (just like any other observable property). Data stored in the observable array is just an underlying JavaScript array.


 
      var length = viewModel.items().length; // Gets the length of the array
      var firstItem = viewModel.items()[0]; // Gets the first item from the array 

An observableArray has its own functions for operating with array. To reach them, observable array variables must be named without parentheses. The indexOf() function returns the index of the first array item that equals your parameter:

      viewModel.items.indexOf(item);

The push() functions adds a new item to the end of array:

      viewModel.items.push(item);

Take a look at Knockout Observable Arrays Documentation for more observable array functions.

Activating Knockout

When view models are created and initialized, you have to bind the view model to the view. To apply bindings for the whole HTML document, it can be done using the Knockout function applyBindings() with passing the view model as the first and the only parameter:

       ko.applyBindings(viewModel);

To apply Knockout for a part of the HTML document, pass the DOM element as a second parameter:

       ko.applyBindings(viewModel, document.getElementById(‘htmlElementId’));

Remember that if there is a need for multiple view models, which must be bounded to the different parts of the document, it is better practice to create one view model with child view models and bind it to the whole HTML document:

       var viewModel = {
               childViewModel1: headerViewModel,
               childViewModel2: footerViewModel,
               childViewModel3: contentViewModel
        }

If two or more view models are bound to the same HTML element, it could cause strange exceptions and some events can duplicate.

Binding Syntax

An HTML attribute data-bind is used to bind a view model to the view. It is a custom Knockout attribute and is reserved for Knockout bindings. The data-bind attribute value consists of two parts: name and value, separated by a colon. Multiple bindings are separated by a comma.


 
      <input type="text" data-bind="value: searchQuery, valueUpdate: 'afterkeydown'" />

The binding item name should match a built-in or custom binding handler. The binding item value can be a view model property or any valid JavaScript expression or any valid JavaScript variable:


 
       <a data-bind=" css: {'demo-sort-arrow-top': isSortedAscending('Title'), 'demo-sort-arrow-bottom': 
isSortedDescending('Title') }">File Name</a>

       <a data-bind="html: title() + '/&nbsp;', click: openItem"></a>

All registered bindings can be found in the Knockout documentation page. How to create a custom binding handler we will discuss a little bit later.

Render a Grid

The main part of the real world example is how to render a grid. Speaking in Knockout language, you have to iterate through a collection and render a row for each item: folder or file. The list view model contains an observable array named items(), which  contains a list of items, filtered by the search query and ordered by the sort criteria. There are two ways to iterate through a collection using the foreach binding: Add binding to a DOM element:

      <div data-bind="foreach: items">
             <div data-bind="text: title()"></div>
      </div>

Add binding as XML comment:

      <!-- ko foreach: items -->
            <div data-bind="text: title()"></div>
      <!-- /ko -->

In the example above, items is a property of the ListViewModel view model which is bound to one of the parent HTML elements. While we’re not inside the foreach loop, we work with the list view model. When inside the foreach loop the context is switched and belongs to the child view model (in our example, it is an object of the ItemViewModel type). To reach the current loop item, special context property $data can be used. There are also some other special properties:

  • $index: gets an index of the current iterating item.
  • $parent or $parents[0]: reaches parent view model. $parents[1], $parents[2] and etc. could be used to reach parent’s parent view model and so on.
  • $root: gets the root view model.

CSS class binding

To set a different CSS class for a DOM element, CSS binding should be used. One way is to pass CSS names collection, depending on a boolean value. Another way is to use an external function, which generates CSS class names:


 
     <div data-bind="css: {'demo-system-folder': isFolder, 'demo-system-file': !isFolder, 
'demo-box-active': isActive() }"> ... </div>

            or


 
     <div data-bind="css: rowClassNames()"> ... </div>

Bind to a click event

In order to add a new folder to the list, we have to add an event handler to a “New Folder” link, so that when a user clicks it, view model’s function addNewFolder() should be invoked.

This could be done, using click binding:

      <a data-bind="click: addNewFolder">New Folder</a>

Now, when a user clicks the button, the addNewFolder() function will be invoked. Let’s look at what that function looks like:

      function ListViewModel(json) {
              var self = this;
              self.items = ko.observableArray();

              ...

              self.addNewFolder = function() {
                  // Creating new item view model
                  var newItem = new ItemViewModel();
                  // Setting non-observable value isFolder to true
                  newItem.isFolder = true;
                  // Setting observable isActive to true – it enters new row 
       to edit mode
                  newItem.isActive(true);
                  // Add new item to the start of the list
                  self.items.unshift(newItem); atsi
              }
          }

When a new item is added to the observable array, it will be rendered automatically at the top of the grid. Now let’s attach event handlers to edit and delete a file or folder:


 
         <div data-bind="foreach: items">
                <div data-bind="click: editItem"> </div>
                <div data-bind="text: title()"> </div>
                <div data-bind="click: deleteItem.bind($data, $parent)"> </div>
         </div>

A function to enter into in-line edit mode is very simple – it just sets the observable property isActive value to true. That causes the UI to change the CSS class of the row (as mentioned in the CSS binding example before) and to show edit input fields.

         self.editItem = function() {
                self.isActive(true);
         };

If you want to pass some parameters to click function (as it’s done in delete binding), the first parameter always must be a variable, which can be named as this in the bound function. The second bound parameter will be the first argument, passed to the function. To be more clear, look at the deleteItem() function example:


 
         self.deleteItem = function (parentViewModel) {
                var deletingItem = this; // $data, passed when binding from view

                if (confirm("Are you sure you want to delete this item?")) {
                parentViewModel.items.remove(deletingItem);
                }
         };

If you do not pass any parameters to the bound function, the first parameter always will be the current view model from the context ($data in foreach loop), and second parameter – target event (click, blur, mouseover, etc.). If some parameters are passed, then data and event parameters are passed after specified parameters. For example, stopping event propagation in the example above could be done like this:

         self.deleteItem = function (parentViewModel, data, event) {
                // Operations with event
                event.stopPropagation()
         }

NOTE. Do not add parentheses to the binding function when binding to events – it would name a specified function each time the HTML element would be rendered, and would not attach to event.

Binding to other events

As shown in the example above, we can bind to a click event using click binding. Also, there are other bindings like submit (form submit event), hasfocus (fired when bound element gets a focus) or event (for other JavaScript events, such onkeypress, onmouseover and etc.). In our example, when a file or folder enters into edit mode, we need to set the focus for the title input field. This can be done using hasfocus binding:

         <input type="text" data-bind="value: title, hasfocus: isActive()" />

An event binding can be used for all JavaScript native events, such as mouseover, mouseout, keypress, keydown, etc. For example, if you want to save a file or folder when a user leaves a title input field, the save method can be fired using onblur event binding:


 
         <input type="text" data-bind="value: title, hasfocus: isActive(), event: { blur: saveItem }" />

Computed variables

To render a parent folder in the grid, you need to know if the current folder has a parent folder. Here you can create and use a Knockout computed variable:

          self.isRootFolder = ko.computed(function() {
                 return !self.folderId();
          });

When you have created a computed value you can bind it as a usual observable value.

Working with form elements

When the view model property is bound to the form input field with the ability to change their values, the property must be bound without parentheses. If properties are bound with parentheses, that means that data can only be read from an observable value, and not set. After the input field value changes, the bound observable value, by default, is changed only when control loses focus and native JavaScript onchange event is called. To force the value to be changed as soon as a user presses the key, binding valueUpdate with one of the available values (‘keyup’, ‘keypress’, ‘afterkeydown’) must be applied. afterkeydown is the best choice if you want to keep the view model updated in real-time.


 
         <input type="text" data-bind="value: title, valueUpdate: 'afterkeydown'" />

Other bindings

There are many other popular bindings I haven’t discussed above. Some of these are explained here:

  • text / html. Used to render text or HTML inside a specified container. For example, in a <div> or <span> tag. When using text binding, text is html-encoded, when using html binding – it is not. For example, if an observable property’s text value is “<span>test</span>”, the binding data-bind=”text: value()” will render “&lt;span&gt;test&lt;/span&gt;” and binding data-bind=”html: value()” will render “test” inside the HTML span tag.
  • style. Used to add CSS style for the DOM element. For example:

 
      <input type="text" data-bind="value: title, style: {'background-color': title() != null ? 'white' : 'red' }" />
  • attr. Used to add attributes for the DOM element. For example:
      <a data-bind="attr: { src: url(), title: description() }"></a>
  • enable / disable. These are mirror bindings: when enable is set to false (or disable is set to true), inputs are disabled for editing. This is useful with form elements like input, select, and textarea:

 
      <input type="text" class="demo-grid-input" data-bind="enable: canBeEdited()”>
  • checked. Can be used with checkable form controls: checkboxes and radio controls, and tells a control to be checked or not:

 
      <input type="checkbox" data-bind="checked: isChecked(), click: onClick" />

NOTE. When an element has bindings to checked and click events for the checkbox (as in the example above) the onClick function must always return true. Otherwise, a checkbox never gets checked as the click event causes it to stop clicking the mouse button event propagation and control never gets checked.

  • options / selectedOptions / optionsValue / optionsText / optionsCaption. Used for HTML drop down (select) element. An options binding is used to pass all available values for the select element. A selectedOptions binding can be used only with the multi-select list - it passes all selected values. For single value dropdowns a value binding will set the selected value. optionsText and optionsValue bindings will tell which passed object’s property contains the text and the value fields. An optionsCaption binding is used to set a caption of the preselected value in the list.

For example, let’s create an array with properties id and name and add two options:

      self.extensions = [];
      self.extensions.push({name: '.xlsx', id: 1});
      self.extensions.push({name: '.docx', id: 2});

This array can be bound using such syntax, and create HTML drop down control with expected options:


 
      <select data-bind="options: extensions, optionsText: 'name', optionsValue: 'id', optionsCaption: '… 
      Please select an item …'"></select>

Blog 39

 

 


Custom Bindings

When a file or folder is in edit mode, it would be perfect if the Enter and Esc keys would be attached, too. For that, we can create custom bindings, named enterPress and escPress.


 
       <input type="text" data-bind="value: title, valueUpdate: 'afterkeydown', enterPress: saveItem, 
escPress: cancelEditItem" />

Custom bindings are registered by adding sub-properties to the Knockout bindingHandlers object. It must be done before the view model is bound to the view. Custom binding syntax is (where init and update callbacks are optional):


 
       ko.bindingHandlers.yourBindingName = {
           init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
               // This will be called when the binding is first applied to an element
               // Set up any initial state, event handlers, etc. here
           },
           update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
               // This will be called once when the binding is first applied to an element,
               // and again whenever the associated observable changes value.
               // Update the DOM element based on the supplied values here.
           }
       };

In our example, we’ll implement only the init callback function. It will attach to the keydown event, and when a user presses the Enter or Escape key, different bindings will be named:


 
       ko.bindingHandlers.enterPress = {
           init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
               var allBindings = allBindingsAccessor();
               element.addEventListener('keydown', function (event) {
                   var keyCode = (event.which ? event.which : event.keyCode);
                   if (keyCode === 13) {
                       allBindings.enterPress.call(viewModel);
                       return false;
                   }
                   return true;
                });
             }
         };

You can read more about custom bindings, value and binding accessors, and function arguments in the Knockout custom binding documentation.

Subscriptions

In our example, if edit mode is canceled, then old values (the values that were present before entering edit mode) must be restored. This can be done by subscribing to the observable value change event. In the example below, function subscribes to the title value change event and sets the old value only when it’s changed in non-edit mode:

      self.title.subscribe(function () {
              if (!self.isActive()) {
                  self.oldTitle = self.title();
              }
           });

Validation Extenders

Before an item can be saved, we first need to know if a user has entered any data into the title edit control. Usually, validation is done by using a jQuery unobtrusive validation but in our case, as we don’t use jQuery, we need to check the value ourselves. What we are going to do is create a Knockout extender “required” which can be applied to required values as a validator. An extender can be created by adding a function to the Knockout extenders object. An extender function takes the observable itself as the first parameter and any options in the second parameter:

      ko.extenders.required = function (target, options) {
          target.hasError = ko.observable();
 
          function validate(newValue) {
              target.hasError(newValue ? false : true);
          }

          validate(target());
          target.subscribe(validate);
          return target;
       };

As you can see in the extender example above, target observable value is passed as the first parameter (target) and options is passed as a second parameter (options are not used in the given example). After the extender is registered, a new observable property hasError is created and added to the target. A validation function validate is subscribed to the target value and is named whenever the target value changes.

The required extender can be used like this:

       self.title = ko.observable().extend({ required: true });

In the view we can check title’s hasError observable’s value. If title has errors, CSS class is added automatically:


 
       <input type="text" data-bind="value: title, css: {'input-validation-error': title.hasError()}" />

A hasError function can be called in the code from the view model also:

       self.saveItem = function () {
               if (self.title.hasError()) {
                   return;
               }

               // save logic
           };

Reusable view templates

When you want to reuse some parts of the view, you can create a template instead of copy-pasting the same parts of HTML code. In our example we’ve created a template for a table header which contains columns for sorting. To create a template the most common way is to use the script tag with type=”text/html”:

       <script type="text/html" id="header-template">
              <div class="demo-media-col-1" data-bind="...">...</div>
              <div class="demo-media-col-2" data-bind="...">...</div>
              <div class="demo-media-col-3" data-bind="...">...</div>
       </script>

When template is created, it can be used to add template binding and pass a template id to the property name. Also, the template’s data can be passed to the data property.


 
<div data-bind="template: {name: 'header-template', data: items }"></div>

These and other template properties are explained more in the Knockout template binding documentation.

Conclusion

As you can see, Knockout is a simple and powerful JavaScript library that helps you create rich and responsive user interfaces with a clean underlying data model. The best way to learn it is to start with the official documentation and then play with interactive tutorials. Full source code of the discussed real world example above can be found in the Devbridge Public GitHub Examples Repository.

Audrunas Matonis

Want more industry news?

comments powered by Disqus
Let's Talk