0

Let's say I have an observableArray:

var outerViewModel = {
    observableArray: ko.observableArray()
};

And then we push a viewmodel in:

var viewModel = {
  prop: ko.observable()
  // Plus lots of other properties
};

outerViewModel.observableArray.push(viewModel);

How do I know when a property of viewModel has changed?

Update

Here is a more specific use case:

I have a ViewModel with a property IsValid. IsValid itself is a computed field based on all the properties in that ViewModel.

I have a ViewModel which contains an observableArray of those ViewModels. I want to create a computed field on that ViewModel called IsValid, which will be true when IsValid is true on all the ViewModels in the array.

My naive attempt, which used return _.all(models,function(model){return model.IsValid();}), did not work.

When I add a new item to that collection, it correctly makes the outer ViewModel invalid, because the collection listens to push, pop, etc. However, changing the items in that collection, does not trigger anything, so even though the ViewModel may now be valid itself, the outer collection is not aware.

blockhead
  • 9,655
  • 3
  • 43
  • 69
  • Your `observableArray` seems to be outside of any model. That way it won't be included in ko bindings, not with `var` at least. What do you want to get a notification that `viewModel` property has changed? – Nikolay Ermakov Jan 18 '16 at 20:45
  • Let me change that, however the basic point is the same. I want to know when any VM in observableArray is updated. – blockhead Jan 18 '16 at 20:49
  • The `observableArray` will issue notifications only when an item has been added or removed from it. It won't trigger anything if you change properties of items it holds, it doesn't know anything about them. But, if I got you right, you can trigger such updates splicing changed element (`viewModel` in your case) out and in the array or manually calling `outerViewModel.observableArray.valueHasMutated()` for whole array. – Nikolay Ermakov Jan 18 '16 at 20:56
  • In order to do that, I need to know when the item changed. That's my question. – blockhead Jan 18 '16 at 20:59
  • Got it. Sorry, don't know. Try to check this: http://stackoverflow.com/questions/10622707/detecting-change-to-knockout-view-model – Nikolay Ermakov Jan 18 '16 at 21:02
  • To get notifications of updates, you need to `subscribe` to whatever you're interested in. You could wrap the `push` method of the observable array to subscribe to whatever you're pushing. – Roy J Jan 19 '16 at 00:13
  • I've written an answer, but I'm sure you can improve the question with more details, as stated in the last paragraph of my answer. If you give a concrete sample of what you want to achieve, or give more concrete details, it will be possible to give a more appropriate answer. – JotaBe Jan 19 '16 at 00:40

1 Answers1

0

The easiest way is to manually subscribe to all the properties of all the items in the array, and update this subscriptions whenever the observable array changes. That's brute force, but it works. Take into account that you can subscribe a simple function to all the desired observables.

If you want a more efficient way, you can use ko.projections to map each item in the array to a computed observable that changes whenever any of the inner properties changes, and subscribe to all the computed observables in the mapped array. This plugin is fully optimized to remap only the necessary properties. As you make a simple subscription to each item, this is a bit more efficient.

Another brute force idea is to use the a computed observable that calculates the JSON representantion of the array, as in the isDirty that R Niemeyer explains here: Creating a Smart, Dirty Flag in KnockoutJS. Or use the dirty flag of kolite, resetting it after its change is detected.

You can also extend the items and do a manula notifications, as explained here: Knockout extenders notify. In this case, you also need to subscribe this notifications to all the items in the array.

I'm sure that, whith a little more information, this solution could be fine-tuned to your particular case. You don't specify why you need it, if you need to know which property or item changes or simply that something changed, what you want to do with the detected changes, and so on. If you improve the question, I can try to fine-tune the answer.

UPDATE

Once I know the specific scenario, I can show you a working sample.

It's important to note that you must access all the elements inside the array so that the computed get subscribed to all of them. If you short-circuit the code and don't access all of them (for example breaking the loop in the first not valid) subscriptions would not be created, and this wouldn't work. I've also made explicit the dependency to the observable array. This achieves that the allValid is recomputed on any change of the observable array or of the valid property of any of its items.

var vm = {
    vals: ko.observableArray([
        { n: 0, valid: ko.observable(true) }, 
        { n: 1, valid: ko.observable(false) },
        { n: 2, valid: ko.observable(true) }
    ])
};

vm.allValid = ko.computed(function() {
    var valid = true;
    // make dependen on observable array
    var vals = vm.vals(); 
    // make dependent on all the items
    for (var i = 0; i<vals.length; i++) {
        valid = valid && vals[i].valid();
    }
    return valid;
});

vm.make1Valid = function() {
 vm.vals()[1].valid(true);
}

vm.make1NotValid = function() {
 vm.vals()[1].valid(false);
}

vm.pushValid = function() {
  vm.vals.push({n:100, valid:ko.observable(true)})
}

vm.pushNotValid = function() {
  vm.vals.push({n:100, valid:ko.observable(false)})
}

vm.pop = function() {
  vm.vals.pop()
}

ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: vals">
  <div>
    n:<span data-bind="text: n"></span>&nbsp;
    valid:<span data-bind="text: valid"></span> 
  </div>
</div>
<hr>
<div style="font-weight:bold">
    <b>All valid:<span data-bind="text: allValid"></span> </b>
</div>
<div>
  <input type="button" value="Make 1 valid" data-bind="click: make1Valid"/>
  <input type="button" value="Make 1 not valid" data-bind="click: make1NotValid"/>
  <input type="button" value="Push valid" data-bind="click: pushValid"/>
  <input type="button" value="Push not valid" data-bind="click: pushNotValid"/>
  <input type="button" value="Pop" data-bind="click: pop"/>
</div>

You can also see an extende version in this jsfiddle.

However I'd recommend you using the ko.validation plugin which already has this kind of functionality implemented. Both validating each individual properties, as well as all the properties of a viewmodel tree. For that case, please, see this: How to validate an array?

Community
  • 1
  • 1
JotaBe
  • 38,030
  • 8
  • 98
  • 117
  • I added a more specfic use case. – blockhead Jan 19 '16 at 03:34
  • Sorry...just noticed that you updated. So far the only difference I can tell between my code and yours is that you're using a `for` loop, and I use `_.all` from underscore (which doesn't short-circuit). I changed you fiddle to use `_.all`, and yours still works. I'm still investigating what the difference is. – blockhead Jan 22 '16 at 08:21
  • Are you sure your computed is invoking all the observables, the array and the items properties, with `()`? If so, it should create the subscriptions and work fine. Is it detecting array changes? Is it detecting proeprty changes? What is exactly failing? (BTW, I thought SO notified when an answer was edited, but I see it doesn't). – JotaBe Jan 22 '16 at 09:08
  • As it turns out, my issue was actually because of something else. However, since you're answer is correct, I will award you the points. – blockhead Jan 23 '16 at 19:31