MojoDK
MojoDK

Reputation: 4528

Knockout subscribe is not called when changing observable array

Take this code:

var koEvents = new ko.subscribable();

var viewModel = function() {
   var self = this;

   self.data = ko.observableArray([{
      valid: true
   }, {
      valid: true
   }]);

   self.isValid = ko.computed(function() {
      var isValid = true;
      ko.utils.arrayForEach(self.data(), function(item) {
         console.log(item.valid);
         if (!item.valid) {
            isValid = false;
            return;
         };
      });
      return isValid;
   }, this).subscribe(function(newValue) {
      alert("Subscribe called!");
      koEvents.notifySubscribers(newValue, "dataChanged");
   }.bind(this));

   return {
      data: self.data,
      isValid: self.isValid,
   };
}

var vm = new viewModel();
ko.applyBindings(vm, document.getElementById("container"));

vm.data()[0].valid = false;
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="container">
   <div data-bind="text: isValid ? 'valid': 'invalid'">
   </div>
</div>

I have two questions...

  1. Why is self.isValid not called, when I do this vm.data()[0].valid = false;?
  2. Why is subscribe (alert("Subscribe called!");) not called when initially isValid is true and later set to false? I expect this to be called twice in my code.

Thanks

Upvotes: 1

Views: 1633

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074148

There are several issues with that code, but the main issue in relation to what you've asked is that changing a non-observable property (valid) on an object in an observable array is not changing the observable array, just a property on an object within it. So naturally there's no notification. If you want notification, you'll need to watch the valid property (which in turn means it will have to be observable).

Other issues:

  1. One of the issues with Knockout is that it sometimes unwraps observables/computeds for you, but it doesn't if they're part of an expression — you have to do it (with ()):

    <div data-bind="text: isValid() ? 'valid': 'invalid'"></div>
    <!-- ------------------------^^                          -->
    

    Your code was testing whether isValid (not isValid()) was truthy. Which it always is, because it's a function reference.

    KO only does the automatic unwrapping for you when the identifier isn't part of an expression. For instance, this works:

    <!-- Works -->
    <div data-bind="visible: isValid">...</div>
    

    but this doesn't:

    <!-- Doesn't work -->
    <div data-bind="visible: !isValid">...</div>
    

    (when isValid is an observable/computed).

  2. You're setting self.isValid to the subscription handle, not the computed, because you've overdone your chaining. :-) You need to complete the assignment after the end of the call to computed, and then subscribe:

    self.isValid = ko.computed(function() {
       // ...
    }, this); // <=== End the assignment here
    self.isValid.subscribe(function(newValue) {
       // ...
    }.bind(this));
    
  3. You're using self = this, but then also passing this to computed and using bind with subscribe. This is harmless, but pointless. One or the other is all you need.

  4. There's no need to create a new, separate object as the return value of your VM constructor; you already have an object (the one created by new).

Here's an example with those various changes. The valid property on the first entry is set to false after 800ms:

var koEvents = new ko.subscribable();

var viewModel = function() {
   this.data = ko.observableArray([{
      valid: ko.observable(true)
   }, {
      valid: ko.observable(true)
   }]);

   this.isValid = ko.computed(function() {
      var isValid = true;
      ko.utils.arrayForEach(this.data(), function(item) {
         console.log(item.valid());
         if (!item.valid()) {
            isValid = false;
            return;
         };
      });
      return isValid;
   }, this);
   this.isValid.subscribe(function(newValue) {
      alert("Subscribe called!");
      koEvents.notifySubscribers(newValue, "dataChanged");
   });
}

var vm = new viewModel();
ko.applyBindings(vm, document.getElementById("container"));

setTimeout(function() {
  vm.data()[0].valid(false);
}, 800);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="container">
   <div data-bind="text: isValid() ? 'valid' : 'invalid'"></div>
</div>

Upvotes: 3

Related Questions