Knockoutjs自定义按钮单击动作以指数方式触发

时间:2017-08-31 21:50:36

标签: typescript knockout.js

我设置了一个具有自定义绑定处理程序的按钮,效果很好!但是,我注意到如果我第一次按下按钮,它会调用一次动作,但是如果我再次按下它,它会调用动作4次,然后是8次,依此类推,直到我刷新页面。

我已经阅读了一些关于knockoutjs按钮点击事件多次触发的其他SO文章,但我认为我的问题可能会略有不同。

以下是我完整设置的要点: https://gist.github.com/fischgeek/dcd6cad07bce920cbd03aa6d6dc1e125

TL; DR这里的自定义绑定处理程序脱离了上下文;也许有一个明显的问题,我没有看到

ko.bindingHandlers.actionButton = {
        init: function (element, valueAccessor, allBindingsAccessor, data, context) {
            var value = valueAccessor();
            if (typeof value === 'object') {
                throw (`${value.Title()} binding must be a string.`);
            }
            var options = allBindingsAccessor().abOptions || {};
            $(element).attr('type', 'submit');
            $(element).addClass('btn');
            $(element).append(`<span data-bind="text: ${value}.Title()"></span>&nbsp;`);
            $(element).append(`<span class="glyphicon" data-bind="css: ${value}.Glyph()"></span>`);
            data[value].Title(options.Title || data[value].Title());
            ko.applyBindingsToNode(element, { css: data[value].State(), click: data[value].WorkMethod() });
        },
        update: function (element, valueAccessor, allBindingsAccessor, data, context) {
            var value = valueAccessor();
            ko.applyBindingsToNode(element, { css: data[value].State(), click: data[value].WorkMethod() });
        }
    };

HTML用法:

<button data-bind="actionButton: 'abSaveSchedule', abOptions: {Title: 'Save Schedule'}"></button>

2 个答案:

答案 0 :(得分:1)

在init方法中,添加一个按钮并对其应用绑定。但是在更新方法中(任何observable随时更改第一次调用),您再次将绑定应用于该元素。

这就是为什么在每次单击时调用操作的次数会增加更新的可观察数量乘以已应用于这些可观察量的绑定数量(标题,字形,状态??)。

我的猜测是,通过完全删除更新处理程序,这将完好无损。

答案 1 :(得分:0)

这可能是非常罕见的场合之一,你需要淘汰赛cleanNode ......

initupdate

首先,您必须知道,在致电init后,淘汰赛也会始终致电update。这意味着您的示例将applyBindings发送到element 两次,从而导致WorkMethod在点击时被调用两次。

逻辑修补程序(如answer提出的Philip建议)是删除applyBindings方法中的update调用。这将解决您的大多数问题,但您仍然缺少其他内容:更改TitleStateWorkMethod后自动更新。如果这不是问题,我建议不要制作这些属性observable。但是因为他们 observable,我会假设他们要更新......

确保视图保持最新

让我解释为什么你的绑定缺乏“更新支持”:

init方法中,在应用内部绑定之前,您unwrap TitleStateWorkMethod 。这意味着您的自定义绑定将获得所有update调用,这反过来意味着您必须处理重新应用绑定到内部元素。 (查看ifwith绑定'来源以供参考)

对于textcss绑定,解决方案是将引用传递给observable而不是内部值。这使得默认绑定可以在自己的update方法中处理更新。

$(element).append(`<span data-bind="text: ${value}.Title"></span>&nbsp;`);
// ...
ko.applyBindingsToNode(element, {
  css: data[valueAccessor()].State
// ...

对于click绑定,我们将不得不求助于“重新应用”绑定,因为doesn't unwrap its valueAccessor...(其他人觉得应该更改?)

要防止附加多个onClick,您需要在重新应用绑定之前清理节点。

“只是修复”的例子

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

    if (typeof value === 'object') {
      throw (`${value.Title()} binding must be a string.`);
    }
    var options = allBindingsAccessor().abOptions || {};
    $(element).attr('type', 'submit');
    $(element).addClass('btn');

    // Keep the observable, but allow for it to be set from
    // the binding's options
    if (options.Title) {
      data[value].Title(options.Title);
    }
    
    $(element).append(`<span data-bind="text: ${value}.Title"></span>&nbsp;`);
    $(element).append(`<span class="glyphicon" data-bind="css: ${value}.Glyph"></span>`);

    
    
  }, update:  function(element, valueAccessor, allBindingsAccessor, data, context) {
    ko.cleanNode(element);
    ko.applyBindingsToNode(element, {
      css: data[valueAccessor()].State,
      click: data[valueAccessor()].WorkMethod()
    });
  }
};


var vm = {

  Title: ko.observable("title"),
  State: ko.observable(""),
  WorkMethod: ko.observable(
    function() {
      console.log("click");
    }
  ),
  Glyph: ko.observable("")
};

ko.applyBindings({
  "abSaveSchedule": vm,
  changeMethod: () => vm.WorkMethod(() => console.log("click2")),
  changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>

<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>

现在,尽管这“有效”,但它有点混乱。

重构

这是一个相同的重写,在我看来,更容易阅读:

ko.bindingHandlers.actionButton = {
  init: function(element, valueAccessor, allBindingsAccessor, data, context) {
    var propName = valueAccessor();
    var options = allBindingsAccessor().abOptions || {};
    var ctx = data[propName];

    if (options.Title) {
      data[propName].Title(options.Title);
    }

    ko.applyBindingsToNode(element, {
      attr: {
        "type": "submit",
        "class": "btn"
      },
      css: ctx.State,
      click: (...args) => ctx.WorkMethod()(...args),
      template: {
        data: ctx,
        nodes: $(`<span data-bind="text:Title"></span>
          <span class="glyphicon" data-bind="css: Glyph"></span>`)
      }
    });
    return {
      controlsDescendantBindings: true
    };
  }
};


var vm = {

  Title: ko.observable("title"),
  State: ko.observable(""),
  WorkMethod: ko.observable(
    function() {
      console.log("click");
    }
  ),
  Glyph: ko.observable("")
};

ko.applyBindings({
  "abSaveSchedule": vm,
  changeMethod: () => vm.WorkMethod(() => console.log("click2")),
  changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>

<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>