Mike Davis
Mike Davis

Reputation: 293

Property is not defined when using if binding referring to $root inside with binding in Knockout

I'm pretty sure I'm missing something as far as context, but I just can't figure out what.

I have the following ViewModel:

var ViewModel = function(){
    var self = this;
    self.person = ko.observable();
    self.isPerson = ko.observable();

    self.person.subscribe(function(value){
        self.isPerson('firstName' in value);
    });
};

var vm = new ViewModel();
var personA = { };
var personB = { firstName: ko.observable("hello") };

vm.person(personA);
ko.applyBindings(vm);
setTimeout(function(){ vm.person(personB); }, 1000);

and the following View:

<span data-bind="with: person">
    <!-- ko if: $root.isPerson -->
        <span data-bind="text: firstName"></span> 
    <!-- /ko -->    
</span>

JSFiddle

Once the timeout executes, I'd expect the firstName to be shown in the view, however, I get the following error:

 firstName is not defined;

If I start out with personB in the viewModel, it works. If I move the if statement above the with statement, it works.

What am I doing wrong in this scenario?

Updated JSFiddle

Upvotes: 0

Views: 6094

Answers (1)

Chris DaMour
Chris DaMour

Reputation: 4010

i don't think you're doing anything wrong. the problem is the "if" binding to the root.isEmployee is being applied before the binding to the "with" is being updated. so the code is seeing the update to the isEmployee and then re-evaluating the view from there down, but the current context is still the old person (as that subscription hasn't fired).

this is proven via a custom binding in http://jsfiddle.net/drdamour/X6pC9/2/ notice the update receives 2 events, once with the old value cause the isEmployee was updated, and second time with the updated new value. this second update comes from the "with" binding subscription being triggered. The subscription of the 'with' binding happens during the applyBindings call, which happens AFTER your model does a subscription.

you can use $data.PropertyName trick to deal with undefined not causing issues. Ala: http://jsfiddle.net/drdamour/X6pC9/1/

<span data-bind="with: person">
    <span data-bind="text: firstName"></span> 
    <!-- ko if: $root.isEmployee -->
        <span data-bind="text: $data.employeeId"></span> 
        <span data-bind="text: $data.employer"></span> 
    <!-- /ko -->    
</span>

the RIGHT way to solve this is to have a PersonVM that has the isEmployee computed, that way you don't bind to the root. see: http://jsfiddle.net/drdamour/eVXTF/1/

<span data-bind="with: person">
    <span data-bind="text: firstName"></span> 
    <!-- ko if: isEmployee -->
        <span data-bind="text: $data.employeeId"></span> 
        <span data-bind="text: $data.employer"></span> 
    <!-- /ko -->    
</span>

and

var ViewModel = function(){
    var self = this;
    self.person = ko.observable();
};

var PersonVM = function()
{
    var self = this;
    this.firstName = ko.observable();
    this.employeeId = ko.observable();
    this.employer = ko.observable();

    self.isEmployee = ko.computed(function(){return self.employer() != null});
}

var vm = new ViewModel();
var customer = new PersonVM();
customer.firstName("John");

var employee = new PersonVM();
employee.firstName("Bill");
employee.employeeId(123);
employee.employer("ACME");

vm.person(customer);
ko.applyBindings(vm);
setTimeout(function(){ vm.person(employee); }, 3000);

computed is preferred to subscribe methods as it deals with the subscription chain for you and abstracts you away from having to manage all that.

Upvotes: 3

Related Questions