barbarianmeetscoding

hackerz edition

Barbarian Meets Knockout: Introduction to Knockout.js Observable Arrays

| Comments

The “barbarian meets” series are a collection of articles that intend to introduce and explain useful libraries, frameworks, tools and technologies in simple and straightforward terms. These new series will focus on Knockout.js, the popular JavaScript MVVM library

Hi you! Time for some knockout.js goodness! Today I bring thee Knockout.js observable arrays, if you are a .NETter, all you need to hear to grasp how observable arrays work is ObservableCollection<T>, if you are not, thing of it as a JavaScript array that notifies you when you add of remove elements to/from it. Easy peasy :)

In this article I will introduce knockout.js observable arrays, provide useful code examples and describe important things that you need to consider when using observable arrays in your web applications. In upcoming articles I will focus on new features of observable arrays released in Knockout 3.0 and the different helpers provided by knockout to facilitate operating with arrays.

Barbarian Meets Knockout

  1. Introduction to Knockout.js
  2. Knockout.js Observables
  3. Knockout.js Computed Observables
  4. Introduction to Knockout.js Observable Arrays
    1. Knockout.js Observable Arrays in Knockout 3.0
  5. Knockout.js Bindings
  6. Knockout.js Templating Engine
  7. Extending Knockout.js with Custom Bindings
  8. Inside the Source Code
  9. Scaling Knockout.js
  10. Knockout.js and SPAs (Single Page Applications)
  11. Persisting Data When Using Knockout.js in the Front End
  12. Using Knockout in an unobstrusive fashion
  13. The Bonus Chapters

Observable Arrays

In a similar fashion to other types of observables, you can use the observableArray function to declare an observable array in your view model. You can either use ko.observableArray() to initialize an observable array with an empty array [] or pass in an already existing array like in the example below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Model
var player = {
  ...
  inventory: [ // vanilla array with items
    {
      name: "rusty broadsword",
      image: "http://www.barbarianmeetscoding.com/images/barbarian-meets-knockout/broadsword.png",
      weight: "10 st"
    },...
  ]
};

// ViewModel
var PlayerViewModel = function(){
    var self = this;
    ...
    self.inventoryItems = ko.observableArray(player.inventory);

As you will be able to verify later when we take a look at knockout’s source code, an observable array is pretty much an observable whose value is an array, with the addition of providing a series of commodity functions – syntactic sugar if you will – to help you perform operations with the array underneath. Thus, an observable array will implement an interface simliar to that of a vanilla array and offer functions such as push, pop, shift, etc.

The codepen below offers a detailed example of these and other different methods available to observable arrays and how they impact knockout notification system. It illustrates one of the most common usages of observable arrays in conjunction with the foreach binding for rendering collections of items (in this case dwarves, hobbits and wizards):

See the Pen Barbarian Meets Knockout: Knockout.js Observable Arrays by Jaime (@Vintharas) on CodePen.

Gotcha’s and Good Things to Know About Observable Arrays

The most important gotcha regarding observable arrays you need to know about is that they can have a significant negative impact in performance, particularly when we add a lot of elements to them in a short period of time. In these cases, it is advised to add all these new elements to the underlying array instead of adding them directly to the observable array itself, this way we ensure that the notification is triggered only once. The following piece of code and the jsFiddle below provide a more illustrative example of this impact in performance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ViewModel.prototype.addGandalfsesBad = function(){
    var self = this;
    moreGandalfses.forEach(function(aGandalf){
        self.gandalfses.push(aGandalf);
    });
};

ViewModel.prototype.addGandalfsesGood = function(){
    var self = this,
        innerArray = self.gandalfses();

    moreGandalfses.forEach(function(aGandalf){
        innerArray.push(aGandalf);
    });

    // and now is when we explicitely trigger
    // the notification that the array has changed
    self.gandalfses.valueHasMutated();
};

Setting the Context Explicitely and Subscribing Manually to an Observable Array

As it happens with any other observable, you can explicitly set the context in which the observable will be evaluated:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var PlayerViewModel = function(name, characterClass, race){
   var self = this;
   self.name = ko.observable(name);
   self.charactedClass = ko.observable(characterClass);
   self.race = ko.observable(race);
};

var vm = new PlayerViewModel("Drizzt Do'Urden", 'Ranger', 'Drow');

// later...
// see how we explicitly set the context o the view model vm
// by passing it as an argument to ko.observableArray()
vm.inventory = ko.observableArray([{
    type: 'sword',
    name: 'scimitar',
    weight: '4 stones'}], vm);

and manually subscribe to be notified when elements are added or removed from the array:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
...
Result = function(arrayToObserve){
    var self = this;
    self.numberOfNotifications = 0;

    // we use the subscribe function to explicitely
    // subscribe to notifications and perform
    // arbitrary tasks
    arrayToObserve.subscribe(function(){
        self.numberOfNotifications++;
    });

    // note how you can add the additional argument "beforeChange" 
    // to subscribe to the observable array and be notified
    // just before the array changes
    arrayToObserver.subscribe(function(valueBeforeChanging){
      alert("the observable array is about to change!!! It had this values " + valueBeforeChanging + "but no more!!");
    }, /* context, the value of this in the callback */ null, "beforeChange");
};

A sneak peek inside the Source Code: Observable Arrays

So! Finally! It’s time to look to some real Knockout.js source code again. You can find the source code for observables arrays under subscribables/observableArrays.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
ko.observableArray = function (initialValues) {
    initialValues = initialValues || [];

    if (typeof initialValues != 'object' || !('length' in initialValues))
        throw new Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");

    // Jaime: check this out. An observable array is just a normal observable
    var result = ko.observable(initialValues);
    // Jaime: on steroids (I hate that expression xD)
    ko.utils.setPrototypeOfOrExtend(result, ko.observableArray['fn']);
    return result.extend({'trackArrayChanges':true});
};

// Jaime: here are the additional observable array functionality
ko.observableArray['fn'] = {
    'remove': function (valueOrPredicate) {
        // Jaime: remove a particular array or those elements within
        // the array that satisfy a given predicate
        // note how operations are performed on the underlying array
        // and then the valueHasMutated function is called

        var underlyingArray = this.peek();
        var removedValues = [];
        var predicate = typeof valueOrPredicate == "function" && !ko.isObservable(valueOrPredicate) ? valueOrPredicate : function (value) { return value === valueOrPredicate; };
        for (var i = 0; i < underlyingArray.length; i++) {
            var value = underlyingArray[i];
            if (predicate(value)) {
                if (removedValues.length === 0) {
                    this.valueWillMutate();
                }
                removedValues.push(value);
                underlyingArray.splice(i, 1);
                i--;
            }
        }
        if (removedValues.length) {
            this.valueHasMutated();
        }
        return removedValues;
    },

    'removeAll': function (arrayOfValues) {
        // If you passed zero args, we remove everything
        ...
    },

    'destroy': function (valueOrPredicate) {
        ...
    },

    'destroyAll': function (arrayOfValues) {
        ...
    },

    'indexOf': function (item) {
        var underlyingArray = this();
        return ko.utils.arrayIndexOf(underlyingArray, item);
    },

    'replace': function(oldItem, newItem) {
        ...
    }
};

// Populate ko.observableArray.fn with read/write functions from native arrays
// Important: Do not add any additional functions here that may reasonably be used to *read* data from the array
// because we'll eval them without causing subscriptions, so ko.computed output could end up getting stale
ko.utils.arrayForEach(["pop", "push", "reverse", "shift", "sort", "splice", "unshift"], function (methodName) {
    ko.observableArray['fn'][methodName] = function () {
        // Use "peek" to avoid creating a subscription in any computed that we're executing in the context of
        // (for consistency with mutating regular observables)
        var underlyingArray = this.peek();
        this.valueWillMutate();
        this.cacheDiffForKnownOperation(underlyingArray, methodName, arguments);
        var methodCallResult = underlyingArray[methodName].apply(underlyingArray, arguments);
        this.valueHasMutated();
        return methodCallResult;
    };
});

// Populate ko.observableArray.fn with read-only functions from native arrays
ko.utils.arrayForEach(["slice"], function (methodName) {
    ko.observableArray['fn'][methodName] = function () {
        var underlyingArray = this();
        return underlyingArray[methodName].apply(underlyingArray, arguments);
    };
});

// Note that for browsers that don't support proto assignment, the
// inheritance chain is created manually in the ko.observableArray constructor
if (ko.utils.canSetPrototype) {
    ko.utils.setPrototypeOf(ko.observableArray['fn'], ko.observable['fn']);
}

ko.exportSymbol('observableArray', ko.observableArray);

As usual, the test suite is an awesome place to see knockout in action and learn each an every use case. Take a look at the test suite for observable arrays at GitHub.

Conclusion

Well, that was it for introducing knockout.js observable arrays. In summary, use them when you want to react to changes within an array, be it to render collection of items in your web application or to perform other types of calculations over them. At the end of the day, observable arrays behave just like vanilla JavaScript arrays but with the observability feature on top. Don’t forget the performance impact when adding/removing lots of elements from an observable array and you should do fine.

In the next article of this series, I will go through the new observable array features added in Knockout.js 3.0 and a couple more interesting tips. Until then, take care!

Additional References

Comments