Pattern for modifying knockout observable on parent from child view model
Asked Answered
M

1

14

I have a parent-child view model object structure set up and need to update an observable on the parent from the child. I've basically come up with two patterns for doing so:

1] Pass a reference of the parent property to the child and update the property from within the child:

var ParentViewModel = function(){
    var self = this;
    this.selectedItem = ko.observable();
    this.child = ko.observable(new ChildViewModel(self.selectedItem));
}

var ChildViewModel = function(parentSelectedItem){
    var self = this;
    this.id = ko.observable();
    this.parentSelectedItem = parentSelectedItem;
    this.select = function(){
        self.parentSelectedItem(self);
    }
}

2] Create the child's select method on the parent and reference the parent observable locally:

var ParentViewModel = function(){
    var self = this;
    this.selectedItem = ko.observable();

    var child = new ChildViewModel();
    child.select = function(){
        self.selectedItem(child);
    }
    this.child = ko.observable(child);
}

var ChildViewModel = function(){
    this.id = ko.observable();
}

Neither of these patterns send me head over heels. The first one pushes the entire property reference into children view models, the second defines a child's function outside of the scope of the child.

Does anyone have any other pattern suggestions as to how this operation could be achieved in javascript in a clean and testable manner? Or am I more or less stuck with just these two options?

Merited answered 4/4, 2012 at 4:33 Comment(0)
S
20

The most common pattern to do this in Knockout is to put a "selectChild" method on your parent that takes in a child. In most cases, the actual child does not need to know that it is being selected.

Then in your binding, you can bind to $root.selectChild or $parent.selectChild. The first argument passed to a handler bound to the click/event binding is the actual data (in KO 2.0), so your method can live on the parent and receive the child as the first arg.

var Item = function(id, name) {
    this.id = id;
    this.name = ko.observable(name);    
};

var ViewModel = function() {
    var self = this;
    this.items = ko.observableArray([
        new Item(1, "one"),
        new Item(2, "two"),
        new Item(3, "three")
    ]);  

    this.selectedItem = ko.observable();

    this.selectItem = function(item) {
        self.selectedItem(item);
    };     
};

In this case, your binding would look like:

<ul data-bind="foreach: items">
    <li>
        <a href="#" data-bind="text: name, click: $root.selectItem"></a>
    </li>
</ul>

Here it is in jsFiddle: http://jsfiddle.net/rniemeyer/anRsA/

You can even simplify it further. Observables are functions and the first argument that you pass to them is used to set their value, so you can even choose to not include the selectItem method and simply bind against $root.selectedItem directly (would look like: http://jsfiddle.net/rniemeyer/anRsA/1/). I usually use a separate method to be explicit, to give it a proper name (action), and in case there is extra processing that needs to be done before or after setting the item.

Prior to KO 2.0 (where $root and $parent were introduced along with the change to pass the data as the first arg to click and event handlers), I used to use the first method that you suggested quite a bit. One thing that you can do there is actually not create the child property (this.parentSelectedItem) and just reference parentSelectedItem (that was passed as an argument) directly in the select method, as it will be available in the function because of the closure that is created.

Skillern answered 4/4, 2012 at 6:0 Comment(4)
Thanks Ryan, I appreciate the well thought out answer.Merited
I need a +2 button for this answerMerited
I'll plus it for you too :-) Very clean way to do this. Also, quit often the child view model that people want is really just another model (its just pure data). Sometimes its easier to think of it that way, whatever we call themVallombrosa
Really nice answer. I was having the same problem, my viewmodel was getting out of my hands, and I had a memory leak, because of circular references caused by having a reference to the parent on the child. I removed the parent references, and I got rid of the memory leak. Nice side effect: My code is much more readable :)Pampas

© 2022 - 2024 — McMap. All rights reserved.