Question

So I have a custom knockout binding for handling time durations. I have an issue where a value in one of my forms might be a duration, but could also be a string or other value. The issue arises from the fact that duration values are represented as objects with two properties, duration and time_unit (which is itself an object with 2 properties. I have the various bound nodes tied up inside if bindings.

init: function(element, valueAccessor, allBindingsAccessor, viewModel, context) {

    var allBindings = ko.toJS(allBindingsAccessor() || {}),
        source = allBindings.source || [],
        observable = valueAccessor(),
        value = ko.toJS(ko.utils.unwrapObservable(observable)),

        duration = new DurationControl({
            inputNode: inputNode,
            source: source,
            defaultValue: value
        });

    //attach duration control to element and render here

    ko.utils.registerEventHandler(inputNode.getDOMNode(), 'blur', function () {
        var observable = valueAccessor();

        if (!observable.viewModelUpdating) {
            observable.viewModelUpdating = ko.observable(false);
        }

        if (duration.isValueValid(true)) {
            observable.viewModelUpdating(true);
            observable.duration(duration.getDuration());
            observable.time_unit.value(duration.getTimeUnit());
            observable.time_unit.id(sourceIdValueMap[duration.getTimeUnit()] || 0);
            observable.viewModelUpdating(false);
        }
    });
}

And my bound html

<!-- ko if: type() == 'string' -->
<div class="control wide">
    <input type="text" data-bind="value: value" />
</div>
<!-- /ko -->

<!-- ko if: type() == 'duration' -->
<div class="control">
   <input type="text" data-bind="duration: value, source: metadata.time_units" />
</div>
<!-- /ko -->

If i do the initial binding with value being the correct object format like so

...,
value: {
    duration: '',
    time_unit: {
        value: '',
        id: '',
    }
},
...

everything works great. But if i start with value in some other format, like ..., value: 'nada', ... it breaks trying to access observable.duration (and observable.time_unit.*).

When I evaluate value with the proper set up, i Get the object described above back out. If i try manually adding the duration/time_unit properties as observables, i still just get the empty string back out.

How do I best go about updating the viewmodel/bindings/etc from inside my init function so that it behaves as if the model was initially in that state when I initialized it?

Was it helpful?

Solution

The custom binding is not the solution to your problem. Knockout will bind everything it encounters in the DOM therefore whichever type of object you initialise your value with, the other one will fail. I've implemented something similar where an observable in my view-model stores multiple 'types' of object, which you will need 'type-specific' portions of UI bound to each. This is how i tackled the problem:

Remove all instances of, if: type() == '<type>' and implement each piece of HTML as a template.

Now refactor that decision making process. Use a computed observable to decide which template is shown based on the type(). Something like this...

function ViewModel(){
    var self = this;

    self.type = ko.observable();
    self.value = ko.observable();

    self.currentValueTemplate = ko.computed(function(){
       switch(self.type()) {
           case 'string':
               return 'stringTemplate';

           case 'duration':
               return 'durationTemplate';

           default:
               throw 'invalid type';
       }
    });

Now simply add a template placeholder...

<!-- ko template: { name: currentValueTemplate, data: value } -->
<!-- /ko -->

You may want to adapt this to fit the specifics of your application, but it is a tidier approach and will scale much better should you want to store an increasing variety of object types.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top