Botond Balázs
Botond Balázs

Reputation: 2500

Knockout.js - $parent not working as expected

(I know the title is not the best one - feel free to edit it)

JSFiddle for easy testing

I have the following Knockout.js view model:

function Bar() {
    var self = this;
    self.baz = "baz";
}

function Foo() {
    var self = this;
    self.bars = ko.observableArray([]);

    self.addBar = function () { self.bars.push(new Bar()); };
    self.removeBar = function (bar) { self.bars.remove(bar); };
}

function ViewModel() {
    var self = this;
    self.foo = new Foo();
}

ko.applyBindings(new ViewModel());

And the following HTML:

<a href="#" data-bind="click: foo.addBar">Add</a>

<ul data-bind="foreach: foo.bars">
    <li><span data-bind="text: baz"></span> <a href="#" data-bind="click: $root.foo.removeBar">Remove</a></li>
</ul>

This works as expected. But if I change the absolute path $root.foo.removeBar to $parent.removeBar like so:

<a href="#" data-bind="click: $parent.removeBar">Remove</a>

item removal stops working; however, I don't see any errors on the console.

With some experimenting, I could find out that $parent here refers to the current item in the observable array (a Bar instance), not the containing object (a Foo instance).

My question is: how can I refer to the containing object using a relative reference in my bindings?

EDIT: I've found a way that works:

<!-- ko with: foo -->
<ul data-bind="foreach: bars">
    <li>
        <span data-bind="text: baz"></span>
        <a href="#" data-bind="click: $parent.removeBar">Remove</a>
    </li>
</ul>
<!-- /ko -->

It isn't pretty but it works. It basically boils down to opening a new scope for the foo property before the foreach.

So my next question is: is there a better way to do this?

Upvotes: 1

Views: 1237

Answers (1)

Jason Spake
Jason Spake

Reputation: 4304

Knockout binding contexts are created "each time you use a control flow binding such as 'with' or 'foreach'" (reference). So I think the confusion is that the binding hierarchy is determined by the html and not by your view model. In your view model the hierarchy is ViewModel -> Foo -> Bars, but in the markup there's $root which is ViewModel, and then the next context that is created is for the Bars object so its parent is directly mapped up to the ViewModel thus skipping Foo.

As far as a better way... I actually don't mind the With binding you have I think it makes the code cleaner than having to drill down into foo on every bind. Better in this case is purely opinion. You could alternately use $parent.foo.removeBar instead of $root.foo.removeBar knowing that in your context those refer to the same thing. A third option would be to use Foo itself as your root view model since it doesn't seem to be doing much other than serving up Foo in this example, but perhaps your actual use case is more complicated.

Upvotes: 2

Related Questions