30 Mär 2015
Client side JavaScript: Knockout in practice

Client side JavaScript: Knockout in practice

When developing a web application that extensively works with user input  – there is no simple and out of box solution to make your UI and data communicate with each other dynamically. That is exact type of work for  Knockout  a JavaScript library that helps you to create rich, responsive display and editor user interfaces with a clean underlying data model. Any time you have sections of UI that update dynamically (e.g., changing depending on the user’s actions or when an external data source changes), KO can help you implement it more simply and maintainable.

Model-View-View Model (MVVM) is a design pattern for building user interfaces. It describes how you can keep a potentially sophisticated UI simple by splitting it into three parts:

  • model: your application’s stored data. This data represents objects and operations in your business domain (e.g., bank accounts that can perform money transfers) and is independent of any UI. When using KO, you will usually make Ajax calls to some server-side code to read and write this stored model data.
  • view model: a pure-code representation of the data and operations on a UI. For example, if you’re implementing a list editor, your view model would be an object holding a list of items, and exposing methods to add and remove items. Note that this is not the UI itself: it doesn’t have any concept of buttons or display styles. It’s not the persisted data model either - it holds the unsaved data the user is working with. When using KO, your view models are pure JavaScript objects that hold no knowledge of HTML. Keeping the view model abstract in this way lets it stay simple, so you can manage more sophisticated behaviors without getting lost.
  • view: a visible, interactive UI representing the state of the view model. It displays information from the view model, sends commands to the view model (e.g., when the user clicks buttons), and updates whenever the state of the view model changes. When using KO, your view is simply your HTML document with declarative bindings to link it to the view model. Alternatively, you can use templates that generate HTML using data from your view model.

The data-binding syntax

A binding consists of two items, the binding name and value, separated by a colon. The binding name should generally match a registered binding handler (either built-in or custom) or be a parameter for another binding. If the name matches neither of those, Knockout will ignore it (without any error or warning). So if a binding doesn’t appear to work, first check that the name is correct.

The binding value can be a single value, variable, or literal or almost any valid JavaScript expression.

Simple Example:

=form_for :book, url: books_path  do |f|
  - if @book.errors.any?
    %h4= pluralize(@book.errors.count, "error") + ' ' + 'prohibited this article from being saved:'
    %ul
      - @book.errors.full_messages.each do |msg|
        %li= msg
  %p
    = f.label :title
    %input{'data-bind' => 'value: title, valueUpdate: "keyup" ', :name => 'book[title]'}

  %p
    = f.label :author
    = f.select :author_id, options_for_select(@authors.map {|author| [author[0], author[1]]}, @resource_instance.author_id)
   
  %p
    = f.label :price
    = f.text_field :price, :placeholder => "Set book price"

  %p
    %button{'type' => 'submit', 'data-bind' => 'enable: activeButton' } Save

  :javascript

    var viewModel = function(){
      var self = this;

      self.title = ko.observable('');
      self.activeButton = ko.computed(function(){
         return self.title().length > 0;
                                                 });
    };
     var model = new viewModel();
    ko.applyBindings(model);

So, the following features have been used:

1. To activate Knockout, the following line was added to a <script> block: ko.applyBindings(model);

Where, actually, model equals to new viewModel(); and was initialized for convenience, while debugging in console.

2. As to the line var self = this; - it is a popular convention that simplifies things:

There’s a popular convention that avoids the need to track this altogether: if your viewmodel’s constructor copies a reference to this into a different variable (traditionally called self), you can then use self throughout your viewmodel and don’t have to worry about it being redefined to refer to something else.

3. self.title = ko.observable('');

Input element should have value to send it to the controller for its further saving in object. In this example there is the title value, which is detecting and processing with the help of Knockout JS.

How can KO know when parts of your view model change? Answer: you need to declare your model properties as observables, because these are special JavaScript objects that can notify subscribers about changes, and can automatically detect dependencies.

4. Event valueUpdate

The value binding links the associated DOM element’s value with a property on your view model. This is typically useful with form elements such as <input>, <select> and <textarea>.

When the user edits the value in the associated form control, it updates the value on your view model. Likewise, when you update the value in your view model, this updates the value of the form control on screen.

If your binding also includes a parameter called valueUpdate, this defines additional browser events KO should use to detect changes besides the change event.

In this example the event "keyup" is used, which updates the view model when the user releases a key.

5. Computed observables are functions that are dependent on one or more other observables, and will automatically update whenever any of these dependencies change. 

self.activeButton = ko.computed(function(){
         		return self.title().length > 0;
      	    });

activeButton() function is called when event 'keyup' is executing. Its sense is to enable the button when the length of the input value will be > 0, which means when a user would type something into input element. 

6. So in HTML template from line %input{'data-bind' => 'value: title, valueUpdate: "keyup" ', :name => 'book[title]'} the attribute name is remaining to explain: name has to be defined to send the value of title in the same form.

Another Example:

=form_for :shopping_card, url: shopping_cards_path  do |f|
  .liveExample
    %table{:width => "100%"}
      %thead
        %tr
          %th{:width => "25%"} Author
          %th{:width => "25%"} Book
          %th.price{:width => "15%"} Price
          %th.quantity{:width => "10%"} Quantity
          %th.price{:width => "15%"} Subtotal
          %th{:width => "10%"}
      %tbody{"data-bind" => "foreach: shopping_card_items"}
        %tr
          %td
          %select{"data-bind" => "options: booksToChoose, optionsText: 'author', optionsCaption: 'Select...', value: author"}
          %td{"data-bind" => "with: $data.author()"}
            %select{"data-bind" => "options: books, optionsText: 'title', optionsCaption: 'Select...', value: $parent.book"}
          %td.price{"data-bind" => "with: book"}
            %span{"data-bind" => "text: price"}
          %td.quantity
            %input{"data-bind" => "visible: book, value: quantity,  valueUpdate: 'afterkeydown'"}
          %td.price
            %span{"data-bind" => "visible: book, text: subtotal()"}
          %td
            %a{"data-bind" => "click: $parent.removeLine", :href => "#"} Remove
    %p.grandTotal
      Total value:
      %input{"data-bind" => "text: grandTotal(), value: $root.grandTotal", :name => 'shopping_card[total_amount]'}
      %input{:type => 'hidden', "data-bind" => "value: $root.dataOrder()", :name => 'shopping_card[shopping_card_item]'}
    %button{"data-bind" => "click: addLine"} Add product
    %button{:type => 'submit'} Submit order

:javascript

  var booksToChoose = #{@books_set};

  var CartLine = function(index) {
      var self = this;
      self.index = ko.observable(index);
      self.author = ko.observable();
      self.id = ko.observable();
      self.book = ko.observable();
      self.quantity = ko.observable(1);

      self.subtotal = ko.computed(function() {
          return self.book() ? self.book().price * parseInt("0" + self.quantity(), 10) : 0;
      });

      // Whenever the category changes, reset the product selection
      self.author.subscribe(function() {
          self.book(undefined);
      });
  };


  var Cart = function() {
      // Stores an array of lines, and from these, can work out the grandTotal
      var self = this;
      self.shopping_card_items = ko.observableArray([new CartLine([0])]); // Put one line in by default
      self.grandTotal = ko.computed(function() {
          var total = 0;
          $.each(self.shopping_card_items(), function() { total += this.subtotal() })
          return total;
                                                                        });
      // Operations
      self.addLine = function() {
      var index = self.shopping_card_items().length;
      self.shopping_card_items.push(new CartLine(index))
                                               };
	self.removeLine = function(shopping_card_item) { self.shopping_card_items.remove(shopping_card_item) };

      self.dataOrder = function() {
         return ko.toJSON( this );
                                                  };
                                    };
  
model = new Cart();
ko.applyBindings(model);

This example shows how computed observables can be chained together. Each cart line has a ko.pureComputed property for its own subtotal, and these in turn are combined in a further ko.pureComputed property for the grand total. When you change the data, your changes ripple out through this chain of computed properties, and all associated UI is updated.

This example also demonstrates a simple way to create cascading dropdowns.

So, the following features have been used:

1. variable booksToChoose is a ruby variable, which is described in appropriate method in the specific controller:

defnew
	@books = []
    	authors = Author.all
   	authors.each do |author|
            	author_name = author.last_name
            	books = author.books
            	item = {author: author_name, books: books}
            	@books << item
    	end
    	@books_set = @books.to_json
    	return  @books_set
end

2. %tbody{"data-bind" => "foreach: shopping_card_items"}

The foreach binding duplicates a section of markup for each entry in an array, and binds each copy of that markup to the corresponding array item. This is especially useful for rendering lists or tables.

Assuming your array is an observable array, whenever you later add, remove, or re-order array entries, the binding will update the UI to match - inserting or removing more copies of the markup, or re-ordering existing DOM elements, without affecting any other DOM elements. This is far faster than regenerating the entire foreach output after each array change.

So:

self.shopping_card_items = ko.observableArray([new CartLine([0])]); // Put one line in by default

3. %select{"data-bind" => "options: booksToChoose, optionsText: 'author', optionsCaption: 'Select...', value: author"}

The options binding controls what options should appear in a drop-down list (i.e., a <select>element).

The value you assign should be an array (or observable array). The <select> element will then display one item for each item in your array.

            optionsCaption

A single-select drop-down list usually starts with some item selected. The usual solution is to prefix the list of options with a special dummy option that just reads “Select an item” or “Please choose an option” or similar, and have that one selected by default.

This easy to do: just add an additional parameter with the name optionsCaption, with its value being a string to display. For example:

<select data-bind='options: myOptions, optionsCaption: "Select an item...", value: myChosenValue'></select>

KO will prefix the list of items with one that displays the text “Select an item…” and has the value undefined. So, if myChosenValue holds the value undefined (which observables do by default), then the dummy option will be selected. If the optionsCaption parameter is an observable, then the text of the initial item will update as the observable’s value changes.

            optionsText

You can bind options to an array of arbitrary JavaScript object - not just strings. In this case, you need to choose which of the objects’ properties should be displayed as the text in the drop-down list or multi-select list. 

4. %td{"data-bind" => "with: $data.author()"} %select{"data-bind" => "options: books, optionsText: 'title', optionsCaption: 'Select...', value: $parent.book"}

A binding context is an object that holds data that you can reference from your bindings. While applying bindings, Knockout automatically creates and manages a hierarchy of binding contexts. The root level of the hierarchy refers to the viewModel parameter you supplied to ko.applyBindings(viewModel). Then each time you use a control flow binding such as with or for each, that creates a child binding context that refers to the nested view model data.

One of the special properties (that could be referenced in any binding) offered by bindings contexts – $data.

This is the view model object in the current context. In the root context, $data and $root are equivalent. Inside a nested binding context, this parameter will be set to the current data item (e.g., inside a with: author binding, $data will be set to author). $data is useful when you want to reference the viewmodel itself, rather than a property on the viewmodel.

Another bindings contexts property is $parent.

This is the view model object in the parent context, the one immediately outside the current context. In the root context, this is undefined. Simpler - you can use $parent to refer to data from outside the foreach.

The with binding creates a new binding context, so that descendant elements are bound in the context of a specified object.

The with binding will dynamically add or remove descendant elements depending on whether the associated value is null/undefined or not.

Just like other control flow elements such as if and foreach, you can use with without any container element to host it. This is useful if you need to use with in a place where it would not be legal to introduce a new container element just to hold the with binding. See the documentation for if or foreach for more details.

Also in the current example such property was used as $root.

This is the main view model object in the root context, i.e., the topmost parent context. It’s usually the object that was passed to ko.applyBindings. It is equivalent to$parents[$parents.length - 1].

5. %span{"data-bind" => "visible: book, text: subtotal()"}

To calculate the total amount of a specific item there is a function, which is described in the viewModel, while processing a separate item in the function CartLine(). Other necessary fields of an item are described as observables:

var CartLine = function(index) {
      var self = this;
      self.index = ko.observable(index);
      self.author = ko.observable();
      self.id = ko.observable();
      self.book = ko.observable();
      self.quantity = ko.observable(1);

      self.subtotal = ko.computed(function() {
          return self.book() ? self.book().price * parseInt("0" + self.quantity(), 10) : 0;
      });

      // Whenever the category changes, reset the product selection
      self.author.subscribe(function() {
          self.book(undefined);
      });
  }; 

The only thing remains is self.author.subscribe(function() { self.book(undefined); });

So, if you want to register your own subscriptions to be notified of changes to observables, you can call their subscribe function. The subscribe function is how many parts of KO work internally. Mostly you don’t need to use this, because the built-in bindings and templating system take care of managing subscriptions.

The subscribe function accepts three parameters: callback is the function that is called whenever the notification happens, target (optional) defines the value of this in the callback function, and event (optional; default is "change") is the name of the event to receive notification for. 

6. %a{"data-bind" => "click: $parent.removeLine", :href => "#"} Remove %p.grandTotal

    Total value:

 %input{"data-bind" => "text: grandTotal(), value: $root.grandTotal", :name => 'shopping_card[total_amount]'}
 %input{:type => 'hidden', "data-bind" => "value: $root.dataOrder()", :name => 'shopping_card[shopping_card_item]'}
    %button{"data-bind" => "click: addLine"} Add product

As to removeLine() function:

self.removeLine = function(shopping_card_item { self.shopping_card_items.remove(shopping_card_item) };

As to grandTotal() function, which calculates the total amount of the specific shopping card:

self.grandTotal = ko.computed(function(){
          var total = 0;
          $.each(self.shopping_card_items(), function(){ total += this.subtotal() })
          return total;
                                                                  });

The $.each()function can be used to iterate over any collection, whether it is an object or an array. In the case of an array, the callback is passed an array index and a corresponding array value each time. (The value can also be accessed through the this keyword, but Javascript will always wrap the this value as an Object even if it is a simple string or a number value.) The method returns its first argument, the object that was iterated.

As to the code line

%input{:type => 'hidden', "data-bind" => "value: $root.dataOrder()", :name => 'shopping_card[shopping_card_item]'}

The HTML element has to be created with the hidden type and the specific name to send all the necessary data with the form to the controller. This element wouldn't be shown on the page, but its purpose is to collect data, that has to be converted firstly into JSON with the help of function dataOrder():

self.dataOrder = function() {
         return ko.toJSON( this );
};

As to the addLine()  function – this function is responsible for the adding lines if it is planned to put several items into the shopping card, each line gets its own index:

self.addLine = function() {
      var index = self.shopping_card_items().length;
      self.shopping_card_items.push(new CartLine(index))
};

We sincerely hope you enjoy reading this article and that it helps you better understand the topic of study. Thank you for reading.

Ähnliche Posts


Video review of the post - powered by Webucator

Favoriteneinträge

What it Takes to Get an e-Commerce Site Online

Getting an e-Commerce website online might sound like a huge undertaking,...

WebView Interactions with JavaScript

WebView displays web pages. But we are interested not only in web-content...

Google Maps API for Android

Google Maps is a very famous and helpful service, which firmly entrenched...

Unit Testing with RSpec

RSpec is an integral part of Test Drive Development (TDD) and its main id...

Client side JavaScript: Knockout in practice

When developing a web application that extensively works with user input ...

Accessing Field Configurations in JIRA for changing field description

Field configuration defines behavior of all standart (system) fields and ...

A Guide for Upgrading to Ruby on Rails 4.2.0

As you might have already heard, the latest stuff for upgrading rails was...