Knockout computed by observable will not update after nested data initialization
I am trying to implement a computed observable HasOpenIssues
that binds to a UI element that updates if any member of a nested observable meets a condition and cannot get it to work. I am using KO 2.2.0.
My view model has an observable array of Visits ; each Visit has an Observable Problem array ; each Problem array can contain instances of Problem , which has a Fixed property visible . I also have a computed Observable LatestVisit that returns the last Visit in the Visits array :
function myVM( initialData ) {
var self = this;
var Issue = function( id, isFixed, description ) {
var self = this;
self.Id = id;
self.IsFixed = ko.observable( isFixed );
self.Description = ko.observable( description );
};
var Visit = function( id, visitDate, issues ) {
var self = this;
self.Id = id;
self.VisitDate = ko.observable( visitDate );
self.Issues = ko.observableArray([]);
// init the array
issues && ko.utils.arrayForEach( issues, function( issue ) {
self.addIssue( issue.Id, issue.Fixed, issue.Description );
});
}
Visit.prototype.addIssue = function( id, isFixed, description ) {
this.Issues.push( new Issue( id, isFixed, description ) );
};
self.LatestVisit = ko.computed( function() {
var visits = self.Visits();
return visits[ visits.length - 1 ];
});
... vm continues
It all starts out blank and before I ko.applyBindings()
get some raw data from the server and pass it as an argument to my viewmodel, which uses it to initialize various observables:
... continued from above...
self.init = function() {
// init the Visits observableArray
ko.utils.arrayForEach( initialData.Visits, function( visit ) {
self.Visits.push( new Visit( visit.Id, visit.InspectionDateDisplay, visit.Issues ) );
});
... more initialization...
};
self.init();
}
function registerVM() {
vm = new myVM( initialDataFromServer );
ko.applyBindings( vm );
}
So, at some point in the process, I cannot observe LatestVisit
since the array Visits
has not yet been populated, or the array Issues
since its parent Visit
hasn 'not populated yet. Once initialized, these structures have data and I need to update HasOpenIssues
to reflect the state of the original data.
Then I allow the user to add a new one Issues
to LatestVisit
and mark new or existing Issues
as Fixed
. Therefore, I need to HasOpenIssues
react to these changes.
I tried to add HasOpenIssues
, computed as a property in the root of the viewmodel, in the array Visits
, in the prototype Visit
and directly on the computed LatestVisit
, and none of it works.It looks something like this:
self.LatestVisit().HasOpenIssues = ko.computed( function() {
var unfixed = ko.utils.arrayFirst( this.Issues(), function( issue ) {
return issue.IsFixed == false;
});
console.log('HasUnfixedIssues:', unfixed);
if ( unfixed ) { return true; }
});
If I let it run before initialization I get some change
Uncaught TypeError: Object [object Window] has no method 'Issues'
or, if I add arguments , root, { deferEvaluation: true }
to the computed function call, I get
Uncaught TypeError: Cannot set property 'HasUnfixedIssues' of undefined
If I omit () like self.LatestVisit.HasOpenIssues
then I get
Uncaught TypeError: Object [object Window] has no method 'Issues'
unless i use the deferred option. If I add it, I don't get an initialization error, but nothing happens when I update Issues
later.
Any advice on how to implement this?
Thank!
source to share
I would make a HasOpenIssues
property of all visits. This way, you can check if any visit has open issues, not just the last one.
function Visit(id, visitDate, issues) {
this.Id = id;
this.VisitDate = ko.observable(visitDate);
this.Issues = ko.observableArray();
this.HasOpenIssues = ko.computed(function() {
var unfixed = ko.utils.arrayFirst(this.Issues(), function (issue) {
return !issue.IsFixed();
});
return unfixed !== null;
}, this);
// init the array
this.Issues(ko.utils.arrayMap(issues, function (issue) {
return new Issue(issue.Id, issue.Fixed, issue.Description);
}));
}
This way, even if there is no problem, you are not trying to add a property to the value undefined
.
If you care about the last visit, then binding the property HasOpenIssues
to LatestVisit
is a good place to put it. You just have to do it right. Check if you have it first LatestVisit
, then return the appropriate value, otherwise some default value (in this case, false).
this.LatestVisit.HasOpenIssues = ko.computed(function() {
var visit = this.LatestVisit();
if (!visit) {
return false;
}
var unfixed = ko.utils.arrayFirst(visit.Issues(), function (issue) {
return !issue.IsFixed();
});
return unfixed !== null;
}, this);
source to share