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!

+3


source to share


1 answer


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

.

contrived demonstration 1




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);

      

contrived demo 2

+1


source







All Articles