Warm tip: This article is reproduced from serverfault.com, please click

Knockout value binding weird behaviour on input types

发布于 2016-02-21 12:37:28

I have two sibling inputs inside a KO foreach:

<input type="text" name="background_color"  data-bind="value: $data.background_color">
<input type="hidden" name="background_color"  data-bind="value: $data.background_color">

and the generated DOM looks like:

<input type="text" name="background_color" data-bind="value: $data.background_color">
<input type="hidden" name="background_color" data-bind="value: $data.background_color" value="#c9311b">

I'm getting that via jQuery's html function and then using that HTML elsewhere, but when I use it elsewhere, the text input's value isn't set.

My fix is to use the attr binding instead, which works

data-bind="attr: {'value':$data.background_color}"

but that's painful. Why is the value is only set for input[type="hidden"]?

Using KO v3.3.0.

Questioner
john Smith
Viewed
0
T.J. Crowder 2016-02-21 21:19:52

The current value of a text input isn't reflected in the DOM at the element/attribute level at all, and so innerHTML or jQuery's html() won't see it.

Solution below, but first let's just look at that behavior of value and why it's different for type="text" vs. type="hidden".

value property vs. value attribute

The value attribute is not the value of a text input, it's the default value of a text input. Setting a text input's value does not set the attribute. In contrast, it doesn't make sense for a hidden input to have the differentiation between a "default value" and a "current value," and so setting the value property does set the attribute, and so you see that attribute in the generated HTML.

This behavior of value is not related to KO, it happens just with raw DOM manipulation:

document.getElementById("the-text-field").value = "text field value";
document.getElementById("the-hidden-field").value = "hidden field value";
<input id="the-text-field" type="text" name="background_color" >
<input id="the-hidden-field" type="hidden" name="background_color">

This is covered by the HTML5 specification here where it talks about the value property and the different modes it has:

The value IDL attribute allows scripts to manipulate the value of an input element. The attribute is in one of the following modes, which define its behavior:

value

On getting, it must return the current value of the element. On setting, it must set the element's value to the new value, set the element's dirty value flag to true, invoke the value sanitization algorithm, if the element's type attribute's current state defines one, and then, if the element has a text entry cursor position, should move the text entry cursor position to the end of the text field, unselecting any selected text and resetting the selection direction to none.

default

On getting, if the element has a value attribute, it must return that attribute's value; otherwise, it must return the empty string. On setting, it must set the element's value attribute to the new value.

...(there are a couple of other modes)

...and here, where it says that input type="hidden"'s value uses the "default" mode but input type="text" uses the "value" mode (you have to scroll down a bit to the "IDL attributes and methods" table; value is the third one listed).

Solution

The solution, as you found, is to ensure that the value attribute is set. You can do that with the attr binding, as you mentioned, but it's a bit painful as you said. Instead, you can give yourself your own custom binding that sets both the value and the default value; since the default value is reflected in the DOM as the value attribute, you'll see it in html():

// The binding handler
ko.bindingHandlers.valueWithAttr = {
  update: function(element, valueAccessor) {
    element.value = element.defaultValue = ko.unwrap(valueAccessor());
    // Or we could do:
    // var value = ko.unwrap(valueAccessor());
    // element.value = value;
    // element.setAttribute("value", value);
    // ...but since that's what setting `defaultValue` does...
  }
};

// Using it
var vm = {
  background_color: "blue"
};
ko.applyBindings(vm, document.body);

$("<pre>").text(
  "html of the inputs: " +
  $("#container").html()
).appendTo(document.body);
<div id="container">
  <input type="text" name="background_color"  data-bind="valueWithAttr: $data.background_color">
  <input type="hidden" name="background_color"  data-bind="valueWithAttr: $data.background_color">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.3.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>