Knockout.js模板中的唯一标识符

23

假设我有一个像这样的 knockout.js 模板:

<script type="text/html" id="mytemplate">
    <label for="inputId">Label for input</label>
    <input type="text" id="inputId" data-bind="value: inputValue"/>
</script>

如果我在页面的多个位置渲染此模板,最终会得到几个具有相同id的输入框(以及几个具有相同for值的标签),这会产生不良后果。特别是,所有依赖于id的代码可能无法正常工作(在我的情况下,我使用jquery.infieldlabel插件,它会因为多个具有相同id的输入框而感到困惑)。我现在解决这个问题的方法是向我绑定到模板的模型添加唯一的id属性:

<script type="text/html" id="mytemplate">
    <label data-bind="attr: {for: id}>Label for input</label>
    <input type="text" data-bind="attr: {id: id}, value: inputValue"/>
</script>

这个方案可行,但并不太优雅,因为我必须在我的模型中有这个人工id属性,而该属性用于其他任何事情。我想知道是否有更好的解决方案。

3个回答

64

另一种不依赖于字段绑定顺序的替代方案是在数据本身上设置一个id属性,该属性需要是可观察的。

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);
    } 
};
你可以像这样使用它:
<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>

示例:http://jsfiddle.net/rniemeyer/JjBhY/

向观察函数添加属性的好处在于,当您将其转换为JSON以发送回服务器时,由于可观察对象将变成其未包装的值,因此该属性自然会消失。


4
我赞同这个想法。但我可以问一个比较复杂的问题来寻求建议,如果你有一对电台,例如标签为“Yes”和“No”,每个标签都与单个布尔观测值“IsActive”绑定。目前,这4个元素——2个电台和2个标签——都具有相同的ID。这些电台对自身出现多次,因此我正在使用Knockout绑定为每一对生成唯一的名称属性,因此我需要能够在ID前缀中加入当前对的特定名称。 - Tom W Hall
你也可以使用 data-bind="attr: { for: 'status_' + $index }" 和 data-bind="attr: { id: 'status_' + $index }" 来生成唯一的 ID。 - viperguynaz
你还可以使用data-bind="attr: { for: 'status_' + $index }"和data-bind="attr: { id: 'status_' + $index }"来生成唯一的ID。$index指的是当前数组项的从零开始的索引。$index是一个可观察对象,当数组项的索引发生变化时(例如,如果向数组中添加或删除了项),它会被更新。 - viperguynaz
1
当这个问题被提出时,$index 不存在,但如果您不需要 id 始终是唯一的(如果您删除最后一个项目并添加一个新项目,则它们将具有相同的 id),则可以使用它。此外,在您的绑定中,如果您在表达式中使用 $index,则需要将其作为函数调用,例如:'status_' + $index() - RP Niemeyer
顺便提一下,element.setAttribute("for", value.id); 的替代方法是 element.htmlFor = value.id; - Tomalak
1
我发现一个令人困扰的问题是,元素添加到DOM后ID会改变。考虑到许多库/软件认为一旦元素在DOM中,ID就是不可变属性,我想知道有多少库/软件可以接受这种情况。例如,屏幕阅读器是否可以监听元素ID的更改并适应它?这在IE6+中是否有效?目前,我正在使用Handlebars在处理之前填充ID和“for”标签,然后将其交给ko,但不确定是否过度设计。 - Shital Shah

7

我以前做过类似于这样的事情:

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);
    } 
};

您可以像这样使用它们:
<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>

因此,它只是在绑定本身上保持状态,递增ko.bindingHandlers.uniqueId.counter。 然后,uniqueFor绑定只需要知道它是在字段之前还是之后,以了解如何获取正确的ID。

示例: http://jsfiddle.net/rniemeyer/8KJD3/

如果您的标签不在其字段附近(可能在表的不同行中绑定多个输入项),那么您需要查看其他策略。


我也在考虑类似的解决方案,这对我的情况肯定有效,但我不喜欢这个解决方案的原因是它依赖于标签呈现的顺序。不过还是谢谢你提供的代码,这绝对是其中之一的选择。 - Roman Bataev
如果顺序是一个问题,那么这里有另一个选项:http://jsfiddle.net/rniemeyer/JjBhY/。 这个选项类似,但会在可能是可观察对象的内容上设置一个"id"属性,绑定中先命中的绑定会更新该id。 在可观察对象函数上设置"id"属性的好处是,当你将其转换为JSON时,它将消失,因为只剩下可观察对象的未包装值。 - RP Niemeyer
谢谢,这正是我在寻找的!你介意把它发布为答案,这样我就可以接受它了吗? - Roman Bataev

3

我无法回复所选答案,但我有一个增强版的代码,支持每个输入值的多个唯一ID。 它在我的博客上 http://drewp.quickwitretort.com/2012/09/18/0 上,这里也重复一遍:

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));
    } 
};

现在您可以为一个记录拥有多个带有自动ID的标记复选框:
<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>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接