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.
contrived demo 1
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);
contrived demo 2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With