4
votes

I am running into a problem, where trying to update a knockoutJS observable to a computed observable dynamically doesn't update the UI.

I have implemented a parent-child dependent Relationship on the Checkboxes Following rules govern the interaction.

  1. When Parent Checkbox is checked, Then all the child Checkbox are checked.
  2. When Parent Checkbox is unchecked, Then all the child Checkbox are unchecked.
  3. When any one of the child Checkbox is unchecked, Then parent Checkbox should be unchecked.
  4. When all of the child checkbox are checked, Then parent Checkbox should be checked.

Now i have a scenario, where the parent-child dependency behavior is dynamic, i.e. the Above rules start working only when the user clicks the button.

I have an implementation where the parent checkbox binds to a property which is observable and later the property is overridden to a new Computed Observable. After changing to the Computed Observable, the first 2 Rules (Rule 1 & 2) works but last 2 Rule (Rule 3 & 4) stops working. I can see in the debugger that the return value is correctly returned but it doesn't Update the UI.

To demonstrate my problem, i have created 2 JS fiddle

  1. Fiddle at https://jsfiddle.net/agalo/rjc1Lsbe/4/ describes the scenario where the parent child relationship rules are working fine. In this case, the Computed Observable property is defined initially on the object and is not changed dynamically. This works perfectly.
  2. Fiddle at https://jsfiddle.net/agalo/uc7yt9tw/4/ describes my current implementation for which Rule 3 and 4 are not working. In this case, the Observable property is overridden with the computed Observable on the click of the button. So, initially the parent child relationship is not enforced, but on click of button the relationship is enforced.

I am not looking for alternative solutions but i am interested to understand as to why the second fiddle shows the behavior.

function ViewModel(){
   var self = this;
   
   var childrenArray = [  {isSelected : ko.observable(true)},
   						  {isSelected : ko.observable(true)},
                          {isSelected : ko.observable(true)},
                          {isSelected : ko.observable(true)}];
   self.parentChildData = {
                            parentCheckBox: ko.observable(false),
                            children: ko.observableArray([])
                          };
   self.parentChildData.children(childrenArray);
   
   self.makeDependent = function(){
		self.parentChildData.parentCheckBox = ko.computed({

                    read: function () {
                        var anyChildUnChecked = ko.utils.arrayFirst(self.parentChildData.children(), function (childCheckBox) {
                                return !childCheckBox.isSelected();
                            });
                            var result = anyChildUnChecked == null;
							return result;
                        
                        //return false;
                    },

                    write: function (value) {
                                ko.utils.arrayForEach(self.parentChildData.children(), function (childCheckBox) {
                                childCheckBox.isSelected(value);
                            });
                    }
      })};
	  
     return self;	  
	  
}

ko.applyBindings(new ViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.1/knockout-min.js"></script>
<p>
Describes my current implementation for which Rule 3 and 4 are not working. In this case, the Observable property is overridden with the computed Observable on the click of the button. So, initially the parent child relationship is not enforced, but on click of button the relationship is enforced.
</p>

<p>
After clicking the button, the first 2 Rules (Rule 1 and 2) works but last 2 Rule (Rule 3 and 4) stops working. I can see in the debugger that the return value is correctly returned but it doesn't Update the UI.

</p>
<div id="parent">
parent <input type="checkbox" data-bind="checked:parentChildData.parentCheckBox"/>
  
  <br/>
  <div id="child">
  Child
<!-- ko foreach: parentChildData.children -->
    <input type="checkbox" data-bind="checked:isSelected"/>
   <!-- /ko -->
  </div>
    <br/>
  <input type="button" value="Make dependent" data-bind="click:makeDependent">
  </div>
1

1 Answers

1
votes

I guess the issue is with the sequence of events that are happening at the time of initialization of your setup. Consider following flow in your implementation -

  • parentCheckBox is first defined as an observable property on self.parentChildData.
  • self.parentChildData.children array is initialized with children items.
  • parentCheckBox is now changed to a computed observable.

The issue I see here is that before the parentCheckBox was asked to react to the changes made to the children in self.parentChildData.children, this children array was already initialized and hence that dependency was not created which is why you were not getting the behavior you expect.

So, I changed the flow of your code a bit and initialized the self.parentChildData.children after the parentCheckBox was assigned a role of computedObservable (though it does not change how it was initially defined).

self.parentChildData.parentCheckBox = ko.computed({

read: function() {
  var anyChildUnChecked = ko.utils.arrayFirst(self.parentChildData.children(), function(childCheckBox) {
    return !childCheckBox.isSelected();
  });
  var result = anyChildUnChecked == null;
  return result;

  //return false;
},

write: function(value) {
  ko.utils.arrayForEach(self.parentChildData.children(), function(childCheckBox) {
    childCheckBox.isSelected(value);
  });
}
});

self.parentChildData.children(childrenArray); //values are pushed here

Also, this removes the need for the self.makeDependent function, though it is strange to me as well that how was this function able to make it work when still the self.parentChildData.children was already having values before the computed was created :-)

Updated fiddle.