Trident D'Gao
Trident D'Gao

Reputation: 19762

Special binding in knockout that renders one template in another

I am trying to solve a problem of rendering one template in context of another template with knockout. The outer template doesn't know and shouldn't care about the inner template and its view model. All it cares about is it's own template, a place to embed the inner template passing the name of it and its view model.

So ideally I wish I know how to implement the following binding:

<script type="text/html" id="outerTemplate">
    <div class="outer-template" data-bind="here: {}"></div>
</script>

<!-- ko nested: { to: 'outerTemplate', data: { name: 'I am an inner view model' } } -->

    <div class="inner-template" data-bind="text: name"></div>

<!-- /ko -->

If anyone knew the knockout well enough to easily outline such kind of binding I would greatly appreciate it.

UPDATE: The feature request was proposed: https://github.com/knockout/knockout/issues/1251

Upvotes: 0

Views: 904

Answers (2)

Trident D&#39;Gao
Trident D&#39;Gao

Reputation: 19762

There is a working example I made myself: http://jsfiddle.net/m34wp/4/

var templateComputedDomDataKey = '__ko__templateComputedDomDataKey__';
function disposeOldComputedAndStoreNewOne(element, newComputed) {
    var oldComputed = ko.utils.domData.get(element, templateComputedDomDataKey);
    if(oldComputed && (typeof (oldComputed.dispose) == 'function')) {
        oldComputed.dispose();
    }
    ko.utils.domData.set(element, templateComputedDomDataKey, (newComputed && newComputed.isActive()) ? newComputed : undefined);
}
function makeArray(arrayLikeObject) {
    var result = [];
    for(var i = 0, j = arrayLikeObject.length; i < j; i++) {
        result.push(arrayLikeObject[i]);
    }
    ;
    return result;
}
function moveCleanedNodesToContainerElement(nodes) {
    var nodesArray = makeArray(nodes);
    var container = document.createElement('div');
    for(var i = 0, j = nodesArray.length; i < j; i++) {
        container.appendChild(ko.cleanNode(nodesArray[i]));
    }
    return container;
}
ko.bindingHandlers['nested'] = {
    'init': function (element, valueAccessor) {
        var elementType = 1;
        var commentType = 8;
        var bindingValue = ko.utils.unwrapObservable(valueAccessor());
        if(element.nodeType == elementType || element.nodeType == commentType) {
            // It's an anonymous template - store the element contents, then clear the element
            var templateNodes = element.nodeType == 1 ? element.childNodes : ko.virtualElements.childNodes(element);
            var container = moveCleanedNodesToContainerElement(templateNodes);
            new ko.templateSources.anonymousTemplate(element)['nodes'](container);
        }
        return {
            'controlsDescendantBindings': true
        };
    },
    'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var options = ko.utils.unwrapObservable(valueAccessor());
        var outerTemplateName = options['to'];
        var dataValue = ko.utils.unwrapObservable(options['data']) || viewModel;
        var innerContext = bindingContext['createChildContext'](dataValue);
        innerContext.innerTemplateElement = element;
        var templateComputed = ko.renderTemplate(outerTemplateName, innerContext, options, element);
        disposeOldComputedAndStoreNewOne(element, templateComputed);
    }
};
ko.bindingHandlers['here'] = {
    'init': function (element, valueAccessor) {
        return {
            'controlsDescendantBindings': true
        };
    },
    'update': function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
        var templateElement = bindingContext.innerTemplateElement;
        if(viewModel != null) {
            var innerContext = bindingContext['createChildContext'](viewModel);
            var templateComputed = ko.renderTemplate(templateElement, innerContext, {
            }, element);
            disposeOldComputedAndStoreNewOne(element, templateComputed);
        } else {
        }
    }
};
ko.virtualElements.allowedBindings['nested'] = true;

Upvotes: 0

RP Niemeyer
RP Niemeyer

Reputation: 114792

The template binding allows you to dynamically select the template name to use, so you can do something like:

<script id="outer" type="text/html">
    <h2>Outer</h2>
    <div data-bind="template: { name: tmplName, data: data }"></div>
</script>

<script id="inner" type="text/html">
    <h3>Inner</h3>
    <input data-bind="value: name" />
</script>

<div data-bind="template: 'outer'"></div>

In this case the view model would look like:

var vm = {
    tmplName: 'inner',
    data: {
        name: ko.observable("Bob")   
    }
};

ko.applyBindings(vm);

The view model could be structured however you want. The key is just that you are passing the template name and data into the template binding.

Sample: http://jsfiddle.net/rniemeyer/LHhc8/

Upvotes: 1

Related Questions