O'Reilly logo

Knockout.js by Jamie Munro

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Chapter 4. Dynamically Changing Properties

So far, we’ve only touched on a small piece of the KnockoutJS framework. In this chapter, we’ll start taking advantage of properties that change dynamically through user interaction.

Knockout calls these properties observables. When you define a variable or property as an observable, Knockout tracks when it changes. This can be used in a variety of different ways that we’ll see in the next few chapters.

Defining an Observable

There are three different types of observables that are most commonly used. The first, shown in Example 4-1, is an observable variable.

Example 4-1. Creating an observable variable
var myObservable = ko.observable();
myObservable('Hello');

alert(myObservable());

To create an observable, assign the ko.observable function to the variable. A default value can be specified in the constructor of the call. Knockout then converts your variable into a function and tracks when the value changes, in order to notify the UI elements associated with the variable.

Accessing an Observable

After an observable is defined, it needs to be called like a function in order to get or set its value. If you try setting it directly as if it were a variable, the observable would be destroyed.

In Example 4-1, notice how the setting of the variable is done like a function. When accessing the value for the alert statement, this is also done. If you were to attempt to alert the observable without the brackets, it would output a large chunk of JavaScript code that contains the Knockout observable function implementation.

The second type of observable, as shown in Example 4-2, is an observable array.

Example 4-2. Creating an observable array
var myObservableArray = ko.observableArray([]);
myObservableArray.push('Hello');

In Example 4-2, the array is instantiated as an empty array by passing two square brackets in the constructor.

Just like observable variables, when elements are added or removed from the array, Knockout notifies elements that are subscribed to the notifications.

An observable array is great for use with tables where elements are being dynamically added and removed.

The final type of observable, as shown in Example 4-3, is a computed observable. This is slightly different than the previous two types in that a computed observable is commonly used to combine one or more observables into a single object.

Example 4-3. Creating a computed observable
self.firstName = ko.observable('Steve');
self.lastName = ko.observable('Kennedy');

self.fullName = ko.computed(function() {
    return 'Hello ' + self.firstName() + ' ' + self.lastName();
});

Once the ViewModel is bound to Knockout, the computed function is executed. For each observable that is used within that function, it subscribes to any change events to that variable. When it changes, Knockout knows that the computed variable should be updated.

pureComputed Observables

As of Knockout version 3.2 (which this book uses), a new type of observable called pureComputed observable was introduced (see Example 4-4). It is quite similar to the computed observable with several performance and memory improvements. The name is borrowed from the Pure function programming term.

A common example of when to use this type of observable is in Example 4-3 where the first and last name were concatenated into a computed observable.

Example 4-4. Creating a pureComputed observable
self.firstName = ko.observable('Steve');
self.lastName = ko.observable('Kennedy');

self.fullName = ko.pureComputed(function() {
    return 'Hello ' + self.firstName() + ' ' + self.lastName();
});

The observables are treated differently because when there are no elements listening for changes to the computed variable, they are placed in sleeping mode versus listening mode. While in sleeping mode, Knockout disposes all dependencies and re-evaluates the content when it is read—unlike listening mode, which manages references to all subscribers and ensures the value is up-to-date prior to first access.

The previous example follows both rules for being a pure function:

  • Given the same input, it will output the same result.
  • No side effects occur because of the function executed.

The second rule is probably the most important when deciding whether to use a computed or a pureComputed observable with Knockout. Within your observable, if you need to execute other code, then you should use a computed observable to ensure it is in listening mode instead of sleeping mode. See the difference in Example 4-5.

Example 4-5. Computed versus pureComputed
<!DOCTYPE html>
<html>
<head> 
    <title>Data Binding with KnockoutJS</title>
</head>
<body> 

    <script type='text/javascript' src='js/knockout-3.2.0.js'></script> 
    <script> 
        function ViewModel() { 
            var self = this; 

            self.firstName = ko.observable('Steve'); 
            self.lastName = ko.observable('Kennedy'); 

            self.pureComputedExecutions = 0; 
            self.computedExecutions = 0; 

            self.pureComputedFullName = ko.pureComputed(function() { 
                // This is not recommended 
                self.pureComputedExecutions++; 
                return 'Hello ' + self.firstName() + ' ' + self.lastName(); 
            }); 

            self.computedFullName = ko.computed(function() { 
                self.computedExecutions++; 
                return 'Hello ' + self.firstName() + ' ' + self.lastName(); 
            }); 
        }; 

        var viewModel = new ViewModel(); 
        ko.applyBindings(viewModel); 

        alert('Pure computed executions: ' + viewModel.pureComputedExecutions); 
        alert('Computed executions: ' + viewModel.computedExecutions); 
    </script>
</body>
</html>

Example 4-5 breaks the pure function rule by placing a side effect within it; however, it is demonstrating the difference between sleep and listening mode.

When this example is run, after the ViewModel has been bound to the view, two alert messages are shown that display the number of times the pureComputed and computed functions are called.

Because the computed observables are not data-bound to anything, the pureComputed is in sleeping mode; thus, the computed function has never been accessed, and the counter is at 0. However, the computed function is automatically evaluated on data binding to set its listeners, causing the counter to increase to 1.

If you were to data-bind to both computed observables, it would result in both counters being 1 because the pureComputed is now in listening mode as it has a subscriber.

Showing and Hiding Elements

Example 4-6 combines the use of an observable variable with a new data binding called visible. The visible data binding sets the CSS property display to either block or none depending on the results of the condition used in the binding.

In Example 4-6, when the button is pressed by the user, it triggers the observable variable to change, which causes the UI to reveal HTML elements that were previously hidden.

Example 4-6. Using the visible data bind with an observable
<!DOCTYPE html>
<html>
<head> 
    <title>Data Binding with KnockoutJS</title>
</head>
<body> 
    <button type="button" data-bind="click: updateObservable">Click me</button> 

    <div data-bind="visible: showExtraData" style="display: none"> 
        Now you see me! 
    </div> 

    <script type='text/javascript' src='js/knockout-3.2.0.js'></script> 
    <script> 
        function ViewModel() { 
            var self = this; 

            self.showExtraData = ko.observable(false); 

            self.updateObservable = function() { 
                self.showExtraData(!self.showExtraData()); 
            }; 
        }; 

        var viewModel = new ViewModel(); 
        ko.applyBindings(viewModel); 
    </script>
</body>
</html>

Let’s start with the HTML. A button is created and data-bound to an event, click. When the user clicks the button, Knockout calls the function updateObservable.

Below the button is a div tag. This element is using the visible data binding. When the result of the condition is false, the element is hidden. When it changes to true, it becomes visible.

CSS Style display: none

You might notice that in the above HTML, I also added style="display: none" to the div. I did this to prevent a flicker when the page is being loaded. A flicker would occur because without the display: none style, the user would temporarily see the contents within the div element. When the Knockout bindings execute, the element would be hidden.

Next, let’s explore the JavaScript ViewModel. The first thing I did was create an observable variable called showExtraData and default it to false (hidden by default). And finally, the updateObservable function is defined, which Knockout calls when the button is clicked. This function simply inverses the value of the showExtraData variable. The first time the button is clicked, the HTML within the div is revealed. If it is clicked again, the text is hidden again.

Adding and Removing Elements

The if and ifnot data bindings are quite similar to the previous visible data binding. The difference between the two is that, unlike the visible binding setting a CSS style to show or hide the element, if and ifnot physically add or remove the elements from the Document Object Model (DOM). See Example 4-7.

Example 4-7. If data binding with an observable
<!DOCTYPE html>
<html>
<head> 
    <title>Data Binding with KnockoutJS</title>
</head>
<body> 
    <button type="button" data-bind="click: updateObservable">Click me</button> 

    <!-- ko if: showExtraData --> 
    <div> 
        Now you see me! 
    </div> 
    <!-- /ko --> 

    <script type='text/javascript' src='js/knockout-3.2.0.js'></script> 
    <script> 
        function ViewModel() { 
            var self = this; 

            self.showExtraData = ko.observable(false); 

            self.updateObservable = function() { 
                self.showExtraData(!self.showExtraData()); 
            }; 
        }; 

        var viewModel = new ViewModel(); 
        ko.applyBindings(viewModel); 
    </script>
</body>
</html>

Example 4-7 is nearly identical to Example 4-6; in fact, the JavaScript ViewModel is untouched. The HTML contains the slight difference. Instead of the div containing a data binding for visible, I’ve used the if data binding inside an HTML comment.

If you inspect the HTML when the page is loaded, you will notice that the div tag is nowhere to be found. Clicking the button will cause the HTML to be dynamically inserted into the DOM.

Adding and Removing from the DOM

If an element is being added to the DOM because of a user interaction that had one or more JavaScript side effects performed on it during the original page load, these would need to be executed after adding them back to the DOM.

For example, if you have a field that is linked to a jQuery date picker, JavaScript is required to initialize it. This needs to be executed after the element is added to the DOM.

In a scenario like this, it might be more prudent to use the visible data binding because the elements will remain in the DOM and can be initialized upon the document load.

I find using the if and ifnot data bindings very convenient when I want to remove content that I only want the user to be able to access in specific scenarios.

Example 4-8. ifnot data binding
ifnot: !showExtraData()

Example 4-7 could be accomplished using ifnot by updating the binding to invert the condition. Instead of adding the elements when the condition evaluates to true, the ifnot adds the elements when the condition evaluates to false.

The Use of Brackets and Observables

In case you didn’t notice, in the previous example, I had to execute the showExtraData observable like it was a function by adding brackets at the end. All of the previous examples did not require the brackets because Knockout intuitively knew to execute the observable; however, because I added the ! to the state when false, I had to tell Knockout to execute the observable and then apply the ! statement.

In Chapter 2, when demonstrating conditional data binding, if the id property were an observable, the example would be updated as follows: id() == 0, thus telling Knockout to execute the id observable and compare it to the value 0.

When to Use Observables

Observables are extremely powerful, and as Peter Parker is told by his Uncle Ben: “With great power comes great responsibility.” (Yes, I must always use this Spider-Man quote when this comes up.)

When an observable is first defined, Knockout initializes the variable as a function to track any changes to the value. This contains a certain amount of processing overhead.

It’s important to make conscious decisions about what you define as an observable. If the value can potentially change (either programmatically or from user interaction), an observable property is completely valid. However, if you have four properties and only two of them will change, and the other properties are needed but will never change, there is no need to make them observed (see Example 4-9).

Example 4-9. Object that is partially observed
function ViewModel(person) { 
    var self = this; 

    self.person = { 
        id: person.id, 
        firstName: ko.observable(person.firstName), 
        lastName: ko.observable(person.lastName), 
        status: person.status 
    }; 
}; 

var person = { 
    id: 1, 
    firstName: 'Steve', 
    lastName: 'Kennedy', 
    status: 'Active' 
}; 

var viewModel = new ViewModel(person); 
ko.applyBindings(viewModel);

Example 4-9 implies that I can only change the first and last name of a person object. The id and status cannot be changed; however, they can still be used in a Knockout data binding for display purposes.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required