25

I'm trying to create an HtmlHelper extension that outputs some HTML to the view. In this HTML I'm wiring up some KnockoutJS binding. I'm new to KO so I'm still struggling in getting some things done. Anyway, what I'm trying to do is generate input fields (in the server-side code) bound to observables on my client-side code, then set the initial values of the observables through the value of the hidden fields. Unfortunately, that is not working for me. So I'm wondering if there any way I could get this done (even if I have to do it completely different).

Here's what I'm basically doing:

In my client side view model I have the following:

self.dataSource = ko.observable();
self.pageSize = ko.observable();

And my extension method outputs the following:

<input type="hidden" value="/Employee/Get" data-bind="value: dataSource" />
<input type="hidden" value="30" data-bind="value: pageSize" />

But when the page renders, when I inspect the elements I notice that the value of the input fields is being set to an empty string, which I believe is because of the way observables are being declared. But is there a way to override this behavior or something?

Kassem
  • 8,116
  • 17
  • 75
  • 116
  • One alternative you can use to keep your code a bit cleaner is to use a custom binding that wraps the `value` binding by initializing it with the element's current value. You can even have it create observables on your view model, if that don't exist. Here is a sample: http://jsfiddle.net/rniemeyer/BnDh6/ – RP Niemeyer Aug 26 '12 at 15:17
  • Hmmm... Now I got the values showing up in the emitted HTML markup, but the value of the observables is still being undefined. – Kassem Aug 26 '12 at 17:32
  • Can you put something in jsFiddle? – RP Niemeyer Aug 26 '12 at 18:12
  • Nevermind, I got it working. It was some silly mistake (as usual!). Could you please post your answer so I could accept it as the correct one? Thanks for the help, much appreciated :) – Kassem Aug 26 '12 at 19:03

4 Answers4

24

A little late here. I wasn't actually satisfied with RP's answer, because it breaks the declarative nature of Knockout. Specifically, if you use valueWithInit to define your property, you can't use it in an earlier binding. Here's a fork of his jsfiddle to demonstrate.

You use it the same way, yet it's still document-wide declarative under the hood:

<input data-bind="valueWithInit: firstName" value="Joe" />

Extending on the idea, you can also use this to separate the initialization and the binding:

<input data-bind="initValue: lastName, value: lastName" value="Smith" />

This is a little redundant, but becomes useful when you're using a plugin instead of the built-in bindings:

<input data-bind="initValue: lastName, myPlugin: lastName" value="Smith" />

Expanding a little more, I also needed a way to initialize checkboxes:

<input type="checkbox" data-bind="checkedWithInit: isEmployed" checked />

And here are the handlers:

ko.bindingHandlers.initValue = {
    init: function(element, valueAccessor) {
        var value = valueAccessor();
        if (!ko.isWriteableObservable(value)) {
            throw new Error('Knockout "initValue" binding expects an observable.');
        }
        value(element.value);
    }
};

ko.bindingHandlers.initChecked = {
    init: function(element, valueAccessor) {
        var value = valueAccessor();
        if (!ko.isWriteableObservable(value)) {
            throw new Error('Knockout "initChecked" binding expects an observable.');
        }
        value(element.checked);
    }
};

ko.bindingHandlers.valueWithInit = {
    init: function(element, valueAccessor, allBindings, data, context) {
        ko.applyBindingsToNode(element, { initValue: valueAccessor() }, context);
        ko.applyBindingsToNode(element, { value: valueAccessor() }, context);
    }
};

ko.bindingHandlers.checkedWithInit = {
    init: function(element, valueAccessor, allBindings, data, context) {
        ko.applyBindingsToNode(element, { initChecked: valueAccessor() }, context);
        ko.applyBindingsToNode(element, { checked: valueAccessor() }, context);
    }
};

Notice valueWithInit simply uses initValue under the hood.

See it in action on jsfiddle.

Joe
  • 16,328
  • 12
  • 61
  • 75
  • **Warning**: the initValue binding **must be before** value (or any other value changing) binding – Gh61 Mar 24 '17 at 09:55
  • It's worth mentioning that isModified() will be set to true so if you don't want that to happen we need to set it to false after the value is set. Like so "valueAccessor().isModified(false)" – Andreas Apr 02 '17 at 16:24
21

One alternative you can use to keep your code a bit cleaner is to use a custom binding that wraps the value binding by initializing it with the element's current value.

You can even have it create observables on your view model, if they don't exist.

The binding might look something like:

ko.bindingHandlers.valueWithInit = {
    init: function(element, valueAccessor, allBindingsAccessor, data) {
        var property = valueAccessor(),
            value = element.value;

        //create the observable, if it doesn't exist 
        if (!ko.isWriteableObservable(data[property])) {
            data[property] = ko.observable();
        }

        data[property](value);

        ko.applyBindingsToNode(element, { value: data[property] });
    }
};

You would use it like:

<input value="someValue" data-bind="valueWithInit: 'firstName'" />

Note that the property name is in quotes, this allows the binding to create it, if it does not exist rather than error out from an undefined value.

Here is a sample: http://jsfiddle.net/rniemeyer/BnDh6

Kyeotic
  • 19,697
  • 10
  • 71
  • 128
RP Niemeyer
  • 114,592
  • 18
  • 291
  • 211
  • Actually it does not work if the property name is not in quotes. – Kassem Aug 27 '12 at 09:23
  • 2
    Yes, this version of the binding would require the name to be in quotes. If you knew that your observables would already exist, then you could bind directly against them and simply set the observable's value in the `init` function from the element's value. – RP Niemeyer Aug 27 '12 at 13:31
  • I've used this in the past and I'd like to add that it may be good to add that by changing the line `ko.applyBindingsToNode(element, { value: data[property] });` to `ko.applyBindingsToNode(element, { textInput: data[property] });` gets you real time two-way updates. http://knockoutjs.com/documentation/textinput-binding.html – edhedges Apr 20 '17 at 16:42
2

If a custom binding is too heavyweight then a simple solution is to initialize the observables from the DOM.

For example, given the following HTML form:

<form name="person">
  <input type="text" name="firstName" value="Joe" data-bind="value: firstName"/>
</form>

Then Knockout could be initialized as follows:

ko.applyBindings({
  firstName: ko.observable(document.forms['person']['firstName'].value)
});
Mark Hobson
  • 1,591
  • 1
  • 12
  • 11
1

You can simply use a custom binding and assign the values to the existing observables:

ko.bindingHandlers.yourBindingName = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        viewModel.dataSource($(".dataSource").val());
        viewModel.pageSize($(".pageSize").val());
    }
};

Then, just add a class, or an Id to the inputs and data-bind="yourBindingName" to the HTML element containing them.

Slim
  • 1,708
  • 5
  • 37
  • 60