Knockoutjs 单选按钮组自定义绑定未根据选择更新

Knockoutjs Radio Button Group Custom Binding Not Updating on Selection

我正在尝试在 knockout 中创建自定义绑定,其功能类似于 options 默认绑定处理程序,但使用单选按钮而不是下拉列表。

将项目添加到数组会通知 update 但选择不同的单选按钮不会。

注意:我将自定义绑定处理程序剥离到最低限度。

绑定处理程序

// LOOK FOR MY COMMENT //<------------------------------ LOOK FOR IT
ko.bindingHandlers.radioButtons = {
    update: function (element, valueAccessor, allBindings) {
        var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
        var previousSelectedValue = ko.utils.unwrapObservable(allBindings().value);


        // Helper function
        var applyToObject = function (object, predicate, defaultValue) {
            var predicateType = typeof predicate;
            if (predicateType == "function")    // Given a function; run it against the data value
                return predicate(object);
            else if (predicateType == "string") // Given a string; treat it as a property name on the data value
                return object[predicate];
            else                                // Given no optionsText arg; use the data value itself
                return defaultValue;
        };

        // Map the array to a newly created label and input.
        var isFirstPass = true;
        var radioButtonForArrayItem = function (arrayEntry, index, oldOptions) {
            if (oldOptions.length) {
                var item = oldOptions[0];
                if ($(item).is(":checked"))
                    previousSelectedValue = item.value;

                isFirstPass = false;
            }

            var input = element.ownerDocument.createElement("input");
            input.type = "radio";

            var radioButtonGroupName = allBindings.get("groupName");
            input.name = radioButtonGroupName;

            if (isFirstPass) {
                var selectedValue = ko.utils.unwrapObservable(allBindings.get("value"));
                var itemValue = ko.utils.unwrapObservable(arrayEntry.value);
                if (selectedValue === itemValue) {
                    input.checked = true;
                }
            } else if ($(oldOptions[0].firstElementChild).is(":checked")) {
                input.checked = true;
            }

            var radioButtonValue = applyToObject(arrayEntry, allBindings.get("radioButtonValue"), arrayEntry);
            ko.selectExtensions.writeValue(input, ko.utils.unwrapObservable(radioButtonValue));

            var label = element.ownerDocument.createElement("label");
            label.appendChild(input);
            var radioButtonText = applyToObject(arrayEntry, allBindings.get("radioButtonText"), arrayEntry);
            label.append(" " + radioButtonText);

            return [label];
        };

        var setSelectionCallback = function (arrayEntry, newOptions) {
            var inputElement = newOptions[0].firstElementChild;
            if ($(inputElement).is(":checked")) {
                var newValue = inputElement.value;
                if (previousSelectedValue !== newValue) {
                    var value = allBindings.get("value");
                    value(newValue); //<------------------------- Get observable value and set here. Shouldn't this notify the update?
                }
            }
        };

        ko.utils.setDomNodeChildrenFromArrayMapping(element, unwrappedArray, radioButtonForArrayItem, {}, setSelectionCallback);
    },
};

使用方法...

// Html
<div data-bind="
    radioButtons: letters, 
    groupName: 'letters', 
    radioButtonText: 'text', 
    radioButtonValue: 'value',   
    value: value,      
"></div>

// Javascript
var vm = {
    letters: ko.observableArray([
        {
            text: "A",
            value: 1,
        },
        {
            text: "B",
            value: 2,
        },
    ]),
    value: ko.observable(2),
};

ko.applyBindings(vm);

因此,向 vm.letters({ text: "C", value: 2 }) 添加新项目会通知 update。但是,单击不同的单选按钮不会通知 update

我需要做什么才能让单击单选按钮通知 update

DEMO HERE

您的绑定似乎无法在选择更改时更新可观察对象。双向绑定对于自定义绑定不是自动的,除非您只是将参数传递给另一个现有绑定。像您的 "hack" 这样的事件处理程序正是自定义绑定的 "init" 函数通常用于的地方。

The “init” callback

Knockout will call your init function once for each DOM element that you use > the binding on. There are two main uses for init:

  • To set any initial state for the DOM element
  • To register any event handlers so that, for example, when the user clicks on or modifies the DOM element, you can change the state of the associated observable

尝试向您的 radioButtons 绑定添加类似于以下 init 函数的内容:

ko.bindingHandlers.radioButtons = {
  init: function(element, valueAccessor, allBindings){
    var unwrappedArray = ko.utils.unwrapObservable(valueAccessor());
    var value = allBindings().value;

    ko.utils.registerEventHandler(element, "click", function(event){
        var target = event.target;
        if(target.nodeName.toLowerCase() == "input"){
            value($(target).attr("value"));
        }
    });
  },
  update: function (element, valueAccessor, allBindings) {
      ...
  }

可能有比检查目标是否为 "input" 元素更安全的处理点击事件的方法;这只是一个简单的例子。如果您在单选按钮元素中嵌套了单选按钮或其他类型的输入元素作为子元素,则必须修改它。如果有疑问,您可以随时从敲除 "checked" 绑定 source code.

复制

编辑:一些需要修复的问题以及我是如何修复它们的。

  1. 更新视图模型的值属性。
  2. 以编程方式更改值 属性 时更新视图(双向绑定)。
  3. 如果选中的单选按钮被移除,我需要将值属性设置为undefined。

可能有更好的方法来实现所有这些...

ko.bindingHandlers.radioButtons = {
  init: function(element, valueAccessor, allBindings){
    //... error checks ... Remove all child elements to "element ... 

    var value = allBindings.get("value");

    // 1. Update viewmodel
    ko.utils.registerEventHandler(element, "click", function(event){
        var target = event.target;
        if(target.nodeName.toLowerCase() == "input"){
            value(target.value);
        }
    });

    // 2. Update view 
    value.subscribe(function (newValue) {
        var inputs = element.getElementsByTagName("input")
        $.each(inputs, function (i, input) {
            input.checked = input.value === newValue;
        });
    };
  },
  update: function (element, valueAccessor, allBindings) {
      ...

      var value = allBindings.get("value");

      // 3. Edge case: remove radio button of the value selected.
      var selectedRadioButton = unwrappedArray.find(function (item) { 
          return item.value === value(); 
      });
      if (selectedRadioButton == null) {
          value(undefined);
      }
  }
}