daedalus28
daedalus28

Reputation: 1637

How to access the data context's observable from inside a knockoutjs template

I need to access the entire observable data context from a knockout template, not just it's value.

In the application I'm developing, I often have a lot of meta data that I use to help render views generically. In the past, I've made the view model properties complex - storing both the meta data and data as sub properties (with values in a value property):

ViewModel.AwesomeProperty = {
    value: ko.observable('Awesome Value'),
    label: 'My Awesome Label',
    template: 'awesomeTemplate',
    otherMetaData: 'etc'
}

I'm changing this meta data to become properties of the observables (as I believe Ryan Niemeyer described in one of his blog posts or sessions). I find it to be cleaner, more elegant, and generally more maintainable with less overhead (particularly when it comes to serialization). The equivalent to the above example would be as follows:

ViewModel.AwesomeProperty = ko.observable('Awesome Value');
ViewModel.AwesomeProperty.label = 'My Awesome Label';
ViewModel.AwesomeProperty.template = 'awesomeTemplate';
ViewModel.AwesomeProperty.otherMetaData = 'etc';

The side effect of doing this is that passing ViewModel.AwesomeProperty to a template sets the data context to the value of the observable (in this case 'Awesome Value'), making the metadata inaccessible from $data:

<script id="example" type="text/html">
    <!-- This won't work anymore -->
    <span data-bind="text: $data.label></span>
</script>
<div data-bind="template: {name: 'example', data: AwesomeProperty}"></div>

The workaround I have now is to wrap the data value in an anonymous object like so:

<script id="example" type="text/html">
    <!-- Now it works again -->
    <span data-bind="text: data.label></span>
</script>
<div data-bind="template: {name: 'example', data: {data:AwesomeProperty}}"></div>

But this is inelegant and not ideal. In a case where there's a lot of auto-generation, this is not only inconvenient, but is actually a major roadblock. I've considered making a custom binding to wrap the template binding, but I'm hoping there's a better solution.

Here's a real world example I've been working for cascading drop downs. This JSFiddle works, but that JSFiddle doesn't.

Thanks in advance.

Upvotes: 2

Views: 2189

Answers (1)

Jeff Mercado
Jeff Mercado

Reputation: 134591

The thing is that knockout will always use the value after unwrapping it. If it happens to be an observable, you'll lose those sub-properties. You'll have to rewrap your observable into another object so you don't lose it as you have already found.

A nice way you can wrap this up would be to create a function for subscribables (or any of the more derived types) which will do this rewrapping. You can either tack on all individual metadata onto this rewrapped object or pack them into their own separate object. Your code can be elegant again.

var buildSelection = function (choices, Parent) {
    return _(ko.observable()).extend({
        // add the metadata to a 'meta' object
        meta: {
            choices: choices,
            availableChoices: ko.computed(function () {
                if (!Parent) return choices;
                if (!Parent()) return [];
                return _(choices).where({ ParentID: Parent().ID });
            })
        }
    });
}

ko.subscribable.fn.templateData = function (metaName) {
    return {
        // access the value through 'value'
        value: this,
        // access the metadata through 'meta'
        meta: this[metaName || 'meta'] // meta property may be overridden
    };
}

Then in your bindings, call this function to create the rewrapped object. Just remember to adjust your bindings in your template.

<script id="Selection" type="text/html">
    <select data-bind="
            options: meta.availableChoices,
            optionsText: 'Value',
            value: value,
            optionsCaption: 'Select One',
            enable: meta.availableChoices().length
    "></select>
</script>

<!-- ko template: { 'name': 'Selection', 'data': Level1.templateData() } --><!-- /ko -->
<!-- ko template: { 'name': 'Selection', 'data': Level2.templateData() } --><!-- /ko -->
<!-- ko template: { 'name': 'Selection', 'data': Level3.templateData() } --><!-- /ko -->

Updated fiddle

Upvotes: 1

Related Questions