Knock Me Out

Thoughts, ideas, and discussion about Knockout.js

Quick Tip: Telling Knockout to Skip Binding Part of a Page

| Comments

Recently, I worked with several people on questions related to binding multiple view models in a single page. One common approach is to bind a view model to a particular root element using a call like ko.applyBindings(vm, containerNode);. However, a limitation with this approach is that when binding multiple view models, none of the container elements can overlap. This means that you could not bind one view model nested inside of another.

One way to address this issue is to create a top level view model that contains your “sub” view models and then call ko.applyBindings on the entire page with the overall view model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var profileModel = {
    first: ko.observable("Bob"),
    last: ko.observable("Smith")
};

var shellModel = {
    header: ko.observable("Administration"),
    sections: ["profile", "settings", "notifications"],
    selectedSection: ko.observable()
};

//the overall view model
var viewModel = {
    shell: shellModel,
    profile: profileModel
};

ko.applyBindings(viewModel);

Now in the view, you can use the with binding along with $root to bind a nested view model:

1
2
3
4
5
6
7
<div data-bind="with: shell">
    <h2 data-bind="text: header"></h2>
    <div data-bind="with: $root.profile">
        ...
    </div>
    ...
</div>

This technique is nice, because you only have to make a single ko.applyBindings call and you can use $root or $parent/$parents to access data at any time from another view model. However, based on a desire to maintain modular code and to control how and when elements are bound, it is often not convenient or practical to build a top level view model.

With Knockout 2.0, there is a simple alternative that can provide for greater flexibility. Bindings are now able to return a flag called controlsDescendantBindings in their init function to indicate that the current binding loop should not try to bind this element’s children. This flag is used by the template and control-flow bindings (wrappers to the template binding), as they will handle binding their own children with an appropriate data context.

For our scenario, we can take advantage of this flag and simply tell Knockout to leave a certain section alone by using a simple custom binding:

1
2
3
4
5
ko.bindingHandlers.stopBinding = {
    init: function() {
        return { controlsDescendantBindings: true };
    }
};

Now, we can bind our “shell” model to the entire page and bind our “profile” model to the specific container:

1
2
3
4
5
6
7
8
9
10
11
12
13
var profileModel = {
    first: ko.observable("Bob"),
    last: ko.observable("Smith")
};

var shellModel = {
    header: ko.observable("Administration"),
    sections: ["profile", "settings", "notifications"],
    selectedSection: ko.observable()
};

ko.applyBindings(shellModel);
ko.applyBindings(profileModel, document.getElementById("profile"));

In our view, we can now use the simple stopBinding custom binding around our inner container element:

1
2
3
4
5
6
7
8
9
10
<div>
    <h2 data-bind="text: header"></h2>
    <div data-bind="stopBinding: true">
        <div id="profile">
            <input data-bind="value: first" />
            <input data-bind="value: last" />
        </div>
    </div>
    ...
</div>

Adding the extra div to hold our stopBinding binding may not cause our app any problems, but if it does then in KO 2.1 we can now create containerless custom bindings by adding our binding to ko.virtualElements.allowedBindings.

1
2
3
4
5
6
7
ko.bindingHandlers.stopBinding = {
    init: function() {
        return { controlsDescendantBindings: true };
    }
};

ko.virtualElements.allowedBindings.stopBinding = true;

and finally we can clean up our view to look like:

1
2
3
4
5
6
7
8
9
<div>
    <h2 data-bind="text: header"></h2>
    <!-- ko stopBinding: true -->
    <div id="profile">
        <input data-bind="value: first" />
        <input data-bind="value: last" />
    </div>
    <!-- /ko -->
</div>

With this simple binding, we can now compose pages with multiple view models without the worry of conflicting/overlapping bindings.

Here is a live sample:

Link to full sample on jsFiddle.net

Comments