42

I would like to know how to create a computed observable array.

In my view model, I have 2 observable arrays, and I would like to have a computed observable array that is simply both arrays combined.

function ViewModel() {
    var self = this;
    self.listA= ko.observableArray([]);
    self.listB = ko.observableArray([]);
    self.masterList= //combine both list A and B
Matt Mangold
  • 690
  • 1
  • 6
  • 10

5 Answers5

33

This will combine the two arrays and return the combined list. However, it is not a computed observable array (don't know if that is even possible), but a regular computed observable.

self.masterList = ko.computed(function() {
    return this.listA().concat(this.listB());
}, this);
Brian S
  • 961
  • 7
  • 10
  • 20
    I believe this answer is flawed for most use-cases: the value of the computed observable is a regular array, not an observable array (roughly stated in the answer). Hence, updating `listA` or `listB` will entirely replace the array itself instead of updating its contents (which is what we want in 99% of the cases). **This means that you should not bind views to this observable.** In effect, this code is as useful as its non-computed variant. See other answers for different approaches. – tne Apr 17 '14 at 15:04
  • It won't work in this case, but the knockout plugin [knockout-projections](https://github.com/stevesanderson/knockout-projections) implements far more efficient computed observable arrays using the newish [array change subscriptions](http://blog.stevensanderson.com/2013/10/08/knockout-3-0-release-candidate-available/). This plugin could be extended to support an efficient concat operation. – Singularity Oct 13 '15 at 00:51
  • when you use such a stratergy **self.masterList.push('element')** says the list is undefined. This becomes vital when you use dependencies like ko sortable – srinath samala May 21 '19 at 08:27
13
self.masterList = ko.observableArray();
ko.computed(function () {
    self.masterList(self.listA().concat(self.listB()));
});

Similar to Joe Flateau's answer in spirit, but I like to think this method is simpler.

tne
  • 6,417
  • 2
  • 38
  • 62
  • This is how I was going to do it as well, but doesn't this still suffer from the issue as the accepted answer; in that any change will cause any view bound to the `masterList` to be redrawn completely? – Adam Lewis May 15 '15 at 14:02
  • 1
    @AdamLewis: Yes, this indeed rebuilds the entire array, and depending on the view engine it may or may not re-render the entire DOM subgraph for whatever views are bound to it (not necessarily though, might just make a diff and apply it). Note that it might still be the best solution to avoid many updates. This is not the issue I outlined regarding the answer you mention though, where if the view engine captures the array property itself (as opposed to the path to it) then it would never detect that you swapped the array (since it's not observable) and thus would never update *at all*. – tne May 15 '15 at 15:26
  • @tne I'm using this answer for now, but it seems really slow... maybe i'm doing something else wrong. however, is this solution breaking the intended purpose of KO? is there another way to solve the problem of needing a computed observable array? just wondering if this is the right tool to use in my (or any) situation. – Nateous Jun 10 '15 at 11:44
  • @Nate There's nothing wrong with the approach as long as you do need to combine the lists in this particular order. I only used `concat` here because everybody else was, to compare easily with other answers. If I were to remove that requirement, I would probably do things differently (because indeed, this is slow). E.g. [push/splice](http://knockoutjs.com/documentation/observableArrays.html#pop-push-shift-unshift-reverse-sort-splice) elements directly. If elements change often then I'd probably use a linked list instead of an array. Maybe you can ask a separate question with your requirements. – tne Jun 10 '15 at 12:56
9

I know this is an old question but I thought I'd throw my answer in there:

var u = ko.utils.unwrapObservable;

ko.observableArray.fn.filter = function (predicate) {
    var target = this;

    var computed = ko.computed(function () {
        return ko.utils.arrayFilter(target(), predicate);
    });

    var observableArray = new ko.observableArray(u(computed));

    computed.subscribe(function (newValue) { observableArray(newValue); });

    return observableArray;
};
Joe Flateau
  • 1,135
  • 1
  • 14
  • 31
6

An observableArray is just an observable with a few more properties. Therefore, a computed observable that returns an array in the closure will be treated as an array.

Paolo del Mundo
  • 2,081
  • 12
  • 18
  • 4
    Well, sort of. I just tested it and it seems that unless it's declared as an observable array, methods like shift and pop are not broken out for you. – Eirinn Jan 24 '14 at 15:30
  • 1
    For the benefit of people like me finding this later: this is not true, see the comments on the accepted answer as to why – Gricey May 14 '18 at 08:24
3

I'm not sure if this is the most efficient option - but it is fairly simple and works for me. The ko.computed returns an observable array as below:

self.computedArrayValue = ko.computed(function() {
    var all = ko.observableArray([]);
    ....
    return all(); 
});

A working example of the code: Html:

<div data-bind="foreach: days">
    <button class="btn btn-default btn-lg day" data-bind="text: $data, click: $root.dayPressed"></button>        
</div> 

Javascript function on the view model:

self.days = ko.computed(function() {
    var all = ko.observableArray([]);
    var month = self.selectedMonth();   //observable
    var year = self.selectedYear();     //observable
    for (var i = 1; i < 29; i++) {
        all.push(i);
    }
    if (month == "Feb" && year % 4 == 0) {
        all.push(29);
    } else if (["Jan","Mar","May","Jul","Aug","Oct","Dec"].find((p) => p == month)) {
        [29,30,31].forEach((i) => all.push(i));
    } else if (month != "Feb") {
        [29,30].forEach((i) => all.push(i));                
    }
    return all(); 
});
onemorecupofcoffee
  • 1,617
  • 9
  • 18