Blog of Appliness
10 Things to Know About Knockout.js on Day One

The following tutorial was written by Ryan
Niemeyer and appeared in the September issue of Appliness. You can find the
original version on his blog at: knockmeout.net
Knockout.js is a JavaScript library that allows you to declaratively bind
elements against model data with two-way updates happening automatically
between your UI and model. While Knockout is quite easy to jump into, here are
some areas that I feel are commonly misunderstood or overlooked by people that
are just getting started with Knockout.
1 – How to set and read observables
Observables are functions that cache their actual value and subscribers
internally. You read an observable’s value by calling it as a function with no
arguments and you can set the observable to a new value by passing a single
argument to it.
var name = ko.observable(“Bob”); //initialize with a value
name(“Ted”); //set it to a new value
alert(name()); //read the value
Question:
If you need to reference it as a function to read the value, then how come in a
data-bind attribute you typically specify just the property name?
Answer:
Most bindings call ko.utils.unwrapObservable on any values
passed to them, which will safely return the value for both observables and
non-observables. However, if you need to use an observable in an expression
inside of your binding string, then you do need to reference it as a function,
because it will be evaluated before the binding has access to it. Likewise, in
your view model code, you typically need to reference your observables as
functions, unless you actually want to pass the observable itself (not the
value).
<div data-bind=”visible: someFlag”>...</div>
<div data-bind=”visible: !someFlag()”>...</div>
2
– The basic rules of computed observables
By
default, the value of a computed observable is determined at the time of
creation. However, this behavior can be controlled by creating the computed
observable with an object that has a deferEvaluation property set to true.
this.total = ko.computed({
read: function() {
var result = 0;
ko.utils.arrayForEach(this.items(), function(item) {
result += item.amount();
});
},
deferEvaluation: true //don’t evaluate until someone requests the value
}, this);
A
computed observable will be re-evaluated whenever one of the observables that
it accessed in its last evaluation changes. Dependency detection is done each
time that the computed observable is evaluated. In the snippet below, if
enabled is true, then it will not depend on disabledHelp. However, if enabled
becomes false, then it will no longer depend on enabledHelp and will start
depending on disabledHelp.
//this computed observable will always depend on this.enabled and
//will additionally depend on either this.enabledHelp or this.disabledHelp
this.helpText = ko.computed({
return this.enabled() ? this.enabledHelp() : this.disabledHelp();
}, this);
A
computed observable can also accept writes, if a write function is provided.
Since, a computed observable doesn’t store a value itself, the job of the write
function is to intercept the new value and decide how to update other
observables, such that its own computed value will be appropriate.
this.totalWithTax = ko.computed({
read: function() {
return this.total() * (1 + this.taxRate());
},
write: function(newValue) {
//do the opposite of the read function and set the total to the correct value
this.total(newValue / (1 + this.taxRate()));
}
}, this);
3
– An observableArray is just an extended observable
It
is helpful to understand that observableArrays are actually just observables.
They follow the same rules and have the same features as observables. However,
they are also augmented with extra methods to perform basic array operations.
These functions perform their action on the underlying array and then notify
subscribers that there was a change. This included common operations like pop,
push, reverse, shift, sort, splice, and unshift. In addition to the standard
array operations, there are several other methods added to handle common tasks.
These include remove, removeAll,destroy, destroyAll, replace, and indexOf.
//remove an item
items.remove(someItem);
//remove all items with the name “Bob”
items.remove(function(item) {
return item.name === “Bob”
});
//remove all items
items.removeAll();
//pass in an array of items to remove
items.removeAll(itemsToRemove)
//retrieve the index of an item
items.indexOf(someItem);
//replace an item
item.replace(someItem, replaceItem);
Note:
destroy/destroyAll work like replace/replaceAll, except they mark the items
with a_destroy property that is respected by the template and foreach bindings
instead of actually removing them from the array.
4
– React to changes using manual subscriptions
Manual
subscriptions give you a chance to programmatically react to a specific
observable that changes. This is useful in many scenarios such as setting
default values, interacting with non-Knockout code, and triggering AJAX
requests. You are able to manually subscribe to observables, observableArrays,
and computed observables.
//trigger an AJAX request to get details when the selection changes
this.selectedItem.subscribe(function(newValue) {
$.ajax({
url: ‘/getDetails’,
data: ko.toJSON({
id: newValue.id
}),
datatype: “json”,
contentType: “application/json charset=utf-8”,
success: function(data) {
this.details(data.details);
}
});
}, this);
5
– Templates are flexible
The
template binding is quite flexible. Here are a few ways that you might want to
use it:
The
template binding accepts a data argument that allows you to control the context
of the binding. This is handy for simplifying references to nested content. It
also accepts an if parameter that helps handle cases when the observable value
may be null, so you do not generate errors from binding against properties of
an undefined object.
<div data-bind=”template: { name: ‘nestedTmpl’, ‘if’: myNestedObject, data: myNestedObject }”></div>
The
template binding also accepts a foreach parameter to loop through items in the
array passed to it. If the array is observable and changes, then Knockout
efficiently adds or removes DOM nodes appropriately rather than re-rendering
the nodes for the entire array.
<ul data-bind=”template: { name: ‘itemTmpl’, foreach: items }”></ul>
You
can even dynamically determine which template should be used to render your
data.
<ul data-bind=”template: { name: getTemplate, foreach: items }”></ul>
function getTemplate(item) {
return item.readOnly() ? “viewOnly” : “editable”;
};
Finally,
there are a number of callbacks supported by the template binding including
afterRender,afterAdd, and beforeRemove. These hooks provide you with the
affected elements and their associated data allowing you to perform actions
like animating items that are being added or removed from an observableArray.
6
– Control-flow bindings are wrappers to the template binding
The
control-flow bindings (foreach, if, ifnot, and with) are really wrappers to the
template binding. Rather than pulling their content from a named template, they
instead save off the children of the element to use as the “template” each time
that a change is detected. These bindings help to simplify your markup and are
especially useful in scenarios where you do not need to reuse the template
content in other areas of your page.
<ul data-bind=”foreach: items”>
<li data-bind=”text: name”></li>
</ul>
<div data-bind=”if: nestedObject”>
<div data-bind=”text: nestedObject().value”
</div>
<div data-bind=”with: nestedObject”>
<div data-bind=”text: value”></div>
</div>
Knockout
also provides a comment-based syntax that allows you to use these bindings
without a parent element. This is particularly useful in scenarios where adding
another level is inappropriate, like in a list that contains static and dynamic
content.
<ul>
<li>Static Content</li>
<!-- ko foreach: items -->
<li data-bind=”text: name”></li>
<!-- /ko -->
</ul>
<!-- ko if: editable -->
<button data-bind=”click: save”>Save</button>
<!-- /ko -->
<!-- ko with: nestedObject -->
<div data-bind=”text: value”></div>
<!-- /ko -->
7
– Bindings are aware of the current context
Inside
of your bindings, Knockout provides a number of useful special variables
related to the currentcontext.
$data
– this is the current data bound at this scope.
$root
– this is the top-level view model.
$parent
– this is the data bound one scope level up from the current data.
$parents
– this is an array of parent view models up to the top-level view model.
$parents[0] will be the same as $parent.
$parentContext
– this provides the context (object containing these special variables) of the
parent scope.
$index
– in the foreach binding, this is an observable representing the current array
item’s index.
These
special variables make it easy to bind against data or call methods from a
higher scope without requiring references to these scopes on your view model
itself.
8
– Keeping track of “this”
Knockout
parses the data-bind attribute value on an element and turn it them into a
JavaScript object that is used to process the bindings. In the case of event
handlers, by the time that Knockout has parsed the binding string, it is
dealing with function references that have no implicit value of this. When
executing these handlers, such as the ones bound through the event and click
bindings, Knockout sets the value of this equal to the current data being
bound. This may not always be the appropriate context for your scenario,
especially when executing a function that lives in a parent scope.
Suppose,
our overall view model has a method that deletes an item:
var ViewModel = function() {
this.items = ko.observableArray();
this.deleteItem = function(item) {
this.items.remove(item);
};
};
If
I use this method on a click binding within a foreach loop through an array of
items, then this will be set to my individual array items and not the overall
view model.
<ul data-bind=”foreach: items”>
<li>
<span data-bind=”text: name”></span>
<a href=”#” data-bind=”click: $root.deleteItem”> x </a>
</li>
</ul>
Conveniently,
Knockout also passes the current data as the first argument to any handlers
called from the event and click bindings. This means that if we can control the
value of this, then we will have both values that we need to perform an action
on the array item from its parent. We want the function to execute with this as
the parent and receive the array item as the first argument. There are several
ways to create your view model in a way that ensures an appropriate value of
this when functions are executed. Here are a couple of the most common ways:
Knockout
does provide an implementation of bind that can be used on any function to
create a wrapper that does guarantee the context. In this case, it would look
like:
this.deleteItem = function(item) {
this.items.remove(item);
}.bind(this);
Alternatively,
you can save the correct value of this in a variable and reference it from
within the function like:
var ViewModel = function() {
var self = this;
this.items = ko.observableArray();
this.deleteItem = function(item) {
self.items.remove(item);
};
};
Additionally,
manual subscriptions and computed observables do take in a second argument to
take care of this for you by controlling the value of this when they are
executed.
viewmodel.fullName = ko.dependentObservable(function() {
return this.firstName() + “ “ + this.lastName();
}, viewmodel);
viewmodel.gratuityAdded.subscribe(function(newValue) {
if (newValue) {
this.total(this.total() * 1.15);
}
}, viewmodel);
9
– Custom bindings need not be a last resort
There
seems to be a slight misconception that custom bindings should only be
considered if there is no other way to accomplish the desired functionality
with the default bindings. Custom bindings are a powerful extensibility point
that can be used in a variety of scenarios and should be considered one of the
normal tools that are available to you along with observables, computed
observables, and manual subscriptions. Besides helping control custom behavior
and/or interacting with 3rd party components, they can also be used to simplify
your bindings by encapsulating multiple behaviors.
A
simple custom binding to start with is one that wraps an existing binding.
ko.bindingHandlers.fadeInText = {
update: function(element, valueAccessor) {
$(element).hide();
ko.bindingHandlers.text.update(element, valueAccessor);
$(element).fadeIn(‘slow’);
}
};
Whenever
you find that your view model code is starting to reference DOM elements, then
you will likely want to consider isolating this code in a custom binding. Given
the element, your data, and the values passed to the binding, you can easily
make one or two-way connections between your view model and the UI.
10-
ko.toJSON has multiple uses
ko.toJSON
is a utility function used to convert objects that include observables to a
JSON string. It first creates a clean JavaScript object (you can use ko.toJS to
only take it this far), then it callsJSON.stringify on the clean object.
Typically, you will be using this technique to package your data for sending
back to the server.
It
can also be very useful for debugging purposes. You can place a pre tag at the
bottom of your page and display some or all of your view model using ko.toJSON
to get a nicely formatted, live preview of how changes in your UI are affecting
the underlying data. No need for console.log calls or alerts. Note that the
second and third arguments to ko.toJSON are passed through to JSON.stringify.
<hr />
<h2>Debug</h2>
<pre data-bind=”text: ko.toJSON($root, null, 2)”></pre>
No comments:
Post a Comment