Skip to content
Anas Nakawa edited this page Feb 15, 2015 · 4 revisions

Originally posted on the forum:

The normal behavior in Knockout is for all bindings on an element to be linked together. Their dependencies are combined so that an update to any of the observables in the bindings will update all the bindings.

This can be a performance issue because updating some bindings is relatively expensive (options, template). Take following example from an application I'm developing:

<!-- ko foreach: navigation.pages -->
<div class="body row scroll-y" data-bind="
        visible: isCurrent,
        attr: { id: 'content' + id },
        template: { name: id, 'if': isLoaded, afterRender: afterRender }">
</div>
<!-- /ko -->

The current page will have isCurrent == true, while all others will have isCurrent == false. With the above code, whenever the current page changes, the template is also re-rendered. My initial solution was to wrap the div in another and move the 'visible' binding to the outer div:

<!-- ko foreach: navigation.pages -->
<div data-bind="visible: isCurrent">
    <div class="body row scroll-y" data-bind="
            attr: { id: 'content' + id },
            template: { name: id, 'if': isLoaded, afterRender: afterRender }">
    </div>
</div>
<!-- /ko -->

But now with the async wrapper binding (see below), I can make it a single div again:

<!-- ko foreach: navigation.pages -->
<div class="body row scroll-y" data-bind="
        async: { visible: isCurrent },
        attr: { id: 'content' + id },
        template: { name: id, 'if': isLoaded, afterRender: afterRender }">
</div>
<!-- /ko -->

The async wrapper binding can be used to isolate bindings that modify a property of an element and are likely to be updated dynamically, such as visible, enable, and hasfocus, from bindings that are used to generate content, such as template, html, and options. See this post about using this with 'options'.

Here is the source for the binding:

ko.bindingHandlers['async'] = {
    'init': function(node, valueAccessor, parsedBindingsAccessor, viewModel, bindingContextInstance) {
        var parsedBindings = valueAccessor();
        function makeValueAccessor(bindingKey) {
            return function () { return parsedBindings[bindingKey] }
        }
        var binding, bindingKey;
        for (bindingKey in parsedBindings) {
            if (!(binding = ko.bindingHandlers[bindingKey])) {
                continue;
            }
            if (!ko.isObservable(parsedBindings[bindingKey])) {
                throw new Error('async binding must be used with observables only');
            }
            ko.dependentObservable((function(bindingKey, binding){
                var isInit = false;
                return function () {
                    if (!isInit && typeof binding["init"] == "function") {
                        var initResult = binding["init"](node, makeValueAccessor(bindingKey), 
                            parsedBindingsAccessor, viewModel, bindingContextInstance);
                        isInit = true;
                    }
                    if (typeof binding["update"] == "function") {
                        binding["update"](node, makeValueAccessor(bindingKey), parsedBindingsAccessor,
                            viewModel, bindingContextInstance);
                    }
                }
            })(bindingKey, binding), null, {'disposeWhenNodeIsRemoved': node});
        }
    }
};

Another way to use this binding is to create a new binding using this function:

var createAsyncBinding = function(binding, newname) {
    if (typeof newname == 'undefined') {
        newname = binding + 'Async';
    }
    var makeAsyncValueAccessor = function(valueAccessor) {
        return function() { var r = {}; r[binding] = valueAccessor(); return r; };
    };
    ko.bindingHandlers[newname] = {
        'init' : function(node, valueAccessor, parsedBindingsAccessor, viewModel, bindingContextInstance) {
            return ko.bindingHandlers['async']['init'](node, makeAsyncValueAccessor(valueAccessor),
                parsedBindingsAccessor, viewModel, bindingContextInstance);
        }
    }
};

Calling the function will create a new binding:

createAsyncBinding('visible');    // will create a visibleAsync binding

Now the above example can be redone as:

<!-- ko foreach: navigation.pages -->
<div class="body row scroll-y" data-bind="
        visibleAsync: isCurrent,
        attr: { id: 'content' + id },
        template: { name: id, 'if': isLoaded, afterRender: afterRender }">
</div>
<!-- /ko -->