Frage

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!

War es hilfreich?

Lösung

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

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top