I'm trying to implement a HasOpenIssues
computed observable that binds to a UI element that updates if any member of a nested observableArray meets a condition, and can't get it to work. I'm using KO 2.2.0.
My viewmodel has an observableArray of Visits; each Visit has an observableArray of Issues; each Issues array can contain instances of Issue, which has an observable IsFixed property. I also have a computed observable LatestVisit, which 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
All these start out empty, and before I ko.applyBindings()
, I get some initial data from the server and pass it in as an argument to my viewmodel, which uses it to initialize the 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 one point in the process, I can't observe the LatestVisit
, since the Visits
array hasn't yet been populated, nor the Issues
array, since its parent Visit
hasn't yet been populated. After I initialize, these structures have data, and I need to update HasOpenIssues
to reflect the state of the initial data.
Then, I allow the user to add new Issues
to the LatestVisit
, and to mark the new or existing Issues
as Fixed
. So I need HasOpenIssues
to react to those changes, too.
I've tried to add a HasOpenIssues
computed as a property on the root of the viewmodel, on the Visits
array, on the Visit
prototype, and directly on the LatestVisit
computed, and none work. 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 variation of
Uncaught TypeError: Object [object Window] has no method 'Issues'
or, if I add , root, { deferEvaluation: true }
arguments to the computed function call, I get
Uncaught TypeError: Cannot set property 'HasUnfixedIssues' of undefined
If I leave off the (), like self.LatestVisit.HasOpenIssues
, then I get
Uncaught TypeError: Object [object Window] has no method 'Issues'
if I don't use the deferred option. If I add it, I don't get an error on initialization, but nothing happens when I update the Issues
later.
Any advice on how to implement this?
Thanks!
I would make HasOpenIssues
a property of all Visits. That way you can check if any visit has open issues, not just the latest 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 are no issues, you're not attempting to add a property to an undefined
value.
If you only cared about the latest visit, then attaching the HasOpenIssues
property to the LatestVisit
is a good place to put it. You just gotta do it right. Check if you have a LatestVisit
first, then return the appropriate value, otherwise some default value (false in this case).
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);