Unique ids in knockout.js templates
Solution 1:
An alternative that does not rely on the order that the fields are bound is to have the binding set an id
property on the data itself, which would need to be an observable.
ko.bindingHandlers.uniqueId = {
init: function(element, valueAccessor) {
var value = valueAccessor();
value.id = value.id || ko.bindingHandlers.uniqueId.prefix + (++ko.bindingHandlers.uniqueId.counter);
element.id = value.id;
},
counter: 0,
prefix: "unique"
};
ko.bindingHandlers.uniqueFor = {
init: function(element, valueAccessor) {
var value = valueAccessor();
value.id = value.id || ko.bindingHandlers.uniqueId.prefix + (++ko.bindingHandlers.uniqueId.counter);
element.setAttribute("for", value.id);
}
};
You would use it like:
<ul data-bind="foreach: items">
<li>
<label data-bind="uniqueFor: name">Before</label>
<input data-bind="uniqueId: name, value: name" />
<label data-bind="uniqueFor: name">After</label>
</li>
</ul>
Sample: http://jsfiddle.net/rniemeyer/JjBhY/
The nice thing about adding a property to the observable function is that when you turn it into JSON to send back to the server, then it will just naturally disappear as the observable will just turn into its unwrapped value.
Solution 2:
I have done something like this in the past:
ko.bindingHandlers.uniqueId = {
init: function(element) {
element.id = ko.bindingHandlers.uniqueId.prefix + (++ko.bindingHandlers.uniqueId.counter);
},
counter: 0,
prefix: "unique"
};
ko.bindingHandlers.uniqueFor = {
init: function(element, valueAccessor) {
var after = ko.bindingHandlers.uniqueId.counter + (ko.utils.unwrapObservable(valueAccessor()) === "after" ? 0 : 1);
element.setAttribute("for", ko.bindingHandlers.uniqueId.prefix + after);
}
};
You would use them like:
<ul data-bind="foreach: items">
<li>
<label data-bind="uniqueFor: 'before'">Before</label>
<input data-bind="uniqueId: true, value: name" />
<label data-bind="uniqueFor: 'after'">After</label>
</li>
</ul>
So, it just keeps state on the binding itself incrementing ko.bindingHandlers.uniqueId.counter
. Then, the uniqueFor
binding just needs to know whether it is before or after the field to know how to get the correct id.
Sample here: http://jsfiddle.net/rniemeyer/8KJD3/
If your labels were not near their fields (multiple inputs bound before each label perhaps in separate rows of a table), then you would need to look at a different strategy.
Solution 3:
I can't reply to the selected answer, but I have an enhanced version of the code that supports multiple unique ids per input value. It's on my blog at http://drewp.quickwitretort.com/2012/09/18/0 and repeated here:
ko.bindingHandlers.uniqueId = {
/*
data-bind="uniqueId: $data" to stick a new id on $data and
use it as the html id of the element.
data-which="foo" (optional) adds foo to the id, to separate
it from other ids made from this same $data.
*/
counter: 0,
_ensureId: function (value, element) {
if (value.id === undefined) {
value.id = "elem" + (++ko.bindingHandlers.uniqueId.counter);
}
var id = value.id, which = element.getAttribute("data-which");
if (which) {
id += "-" + which;
}
return id;
},
init: function(element, valueAccessor) {
var value = valueAccessor();
element.id = ko.bindingHandlers.uniqueId._ensureId(value, element);
},
};
ko.bindingHandlers.uniqueFor = {
/*
data-bind="uniqueFor: $data" works like uniqueId above, and
adds a for="the-new-id" attr to this element.
data-which="foo" (optional) works like it does with uniqueId.
*/
init: function(element, valueAccessor) {
element.setAttribute(
"for", ko.bindingHandlers.uniqueId._ensureId(valueAccessor(), element));
}
};
Now you can have multiple labeled checkboxes for one record with automatic ids:
<li data-bind="foreach: channel">
<input type="checkbox" data-which="mute" data-bind="uniqueId: $data, checked: mute">
<label data-which="mute" data-bind="uniqueFor: $data">Mute</label>
<input type="checkbox" data-which="solo" data-bind="uniqueId: $data, checked: solo">
<label data-which="solo" data-bind="uniqueFor: $data">Solo</label>
</li>