【问题标题】:Custom select list text binding not working with editor for first array item in Knockoutjs自定义选择列表文本绑定不适用于 Knockoutjs 中第一个数组项的编辑器
【发布时间】:2018-03-29 15:04:00
【问题描述】:

我有一个只读电话号码表。电话号码有一个号码和一个类型(例如移动电话或家庭电话)。每个电话号码都有一个“编辑”按钮,单击该按钮后,您可以编辑电话号码并在模式对话框中键入。我将 Knockoutjs 用于只读表和编辑器。该表绑定到 PhoneVMs 的 observableArray 并且编辑器在单个 PhoneVM 上工作。因为我希望用户在应用任何更改之前必须在模式上单击“确定”,所以该模式适用于所选PhoneVM 的副本,当他们单击“确定”时,它会替换 observableArray 中最初单击的PhoneVM该表是绑定的。这一切都很好。

现在我需要允许在与只读表相同的页面上编辑列表中的第一个电话(没有模式)。这个想法是为了在工作流程的早期更容易输入第一部电话。因此,您将在页面上输入您的手机,它会自动出现在下面的只读列表中,您也可以在模态中正常编辑它。我以为 Knockout 会让这件事变得简单,但我遇到了障碍。从这一点来看,只显示一个错误的例子会更容易。在这个小提琴中执行以下操作:https://jsfiddle.net/ph4mhsof/

  1. 编辑电话号码并在文本框中跳出标签。请注意“所有电话”列表中的第一部电话也会更新。
  2. 在下拉列表中更改电话类型。请注意所有电话表中的类型 ID 和类型名称都发生了相应的变化。
  3. 单击删除第一部电话。第二部手机成为新的第一部手机。
  4. 编辑电话号码并在文本框中跳出标签。请注意所有电话列表中的第一部电话按预期更新。
  5. 在下拉列表中更改电话类型。请注意所有电话列表中的类型 ID 更新。类型名称不会更新。

我正在使用自定义绑定将类型名称绑定到选择的文本。似乎该绑定的 init 函数中的 valueAccessor 必须专门指向原始的第一个 PhoneVM 的PhoneTypeName 属性,但我需要它做的是指向firstPhone 计算属性的PhoneTypeName。有没有办法解决这个问题?

原始jsfiddle的副本:

function phoneListVM() {
  var _self = this;
  this.phones = ko.observableArray([
    new phoneVM(1, "Mobile", "123-234-3456"),
    new phoneVM(2, "Home", "654-343-3211")
  ]);

  this.firstPhone = ko.computed(function() {
    return _self.phones()[0];
  });
}

function phoneVM(typeID, typeName, Number) {
  this.PhoneTypeID = ko.observable(typeID);
  this.PhoneTypeName = ko.observable(typeName);
  this.PhoneNumber1 = ko.observable(Number);
}

ko.bindingHandlers.selectedTextValue = {
  init: function(element, valueAccessor) {
    var value = valueAccessor();

    $(element).change(function() {
      value($("option:selected", this).text());
    });
  },
  update: function(element, valueAccessor) {}
};

$(document).ready(function() {
  var phoneList = new phoneListVM()
  ko.applyBindings(phoneList);
  $("button").click(function() {
    phoneList.phones.shift();
  });
});
.editor{
  background-color:rgba(200,200,250, 0.2);
  border: 1px solid rgba(0,0,0, 0.2);
  padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<h4>
Edit First Phone
</h4>
<div class="editor">
  <p>Phone Number:
    <input data-bind="value: firstPhone().PhoneNumber1" />
  </p>
  <p>Phone Type:
    <select data-bind="value:firstPhone().PhoneTypeID, selectedTextValue:firstPhone().PhoneTypeName">
      <option value="0"></option>
      <option value="1">Mobile</option>
      <option value="2">Home</option>
    </select>
  </p>
</div>

<h4>
All Phones
</h4>
<table>
  <thead>
    <tr>
      <th>Type ID</th>
      <th>Type Name</th>
      <th>Number</th>
    </tr>
  </thead>
  <tbody data-bind="foreach:phones">
    <tr>
      <td><span data-bind="text:PhoneTypeID"></span></td>
      <td><span data-bind="text:PhoneTypeName"></span></td>
      <td><span data-bind="text:PhoneNumber1"></span></td>
    </tr>
  </tbody>
</table>
<button type="button">
  Remove First Phone
</button>

【问题讨论】:

    标签: javascript knockout.js ko-custom-binding


    【解决方案1】:

    在我看来,自定义绑定太过分了。我已经用一些方法和一个专门的 selected observable 更新了你的代码,所以你总是知道选择了哪部手机。

    让我知道这是否是您要找的。​​p>

    function phoneListVM() {
      var _self = this;
      this.phones = ko.observableArray([
      	// Removed typeName
        new phoneVM(1, "123-234-3456"),
        new phoneVM(2, "654-343-3211")
      ]);
      
      // Observable to see which phone is currently selected
      this.SelectedPhone = ko.observable(_self.phones().length > 0 ? _self.phones()[0] : '');
      
      // Allow editing whichever phone they want
      this.EditPhone = function(obj) {
        _self.SelectedPhone(obj);
      };
      
      // Remove first phone and check if there are any more phones, if so add it to the selected phone
      this.RemoveFirstPhone = function() {
        var firstPhone = _self.phones()[0];
        if(firstPhone) {
            _self.phones.remove(firstPhone);
            _self.SelectedPhone(_self.phones().length > 0 ? _self.phones()[0] : '');
        }
      }
    }
    
    // Removed typeName and made it computed. Could be replaced with some lodash _.find if you are storing an array of types in the global space
    function phoneVM(typeID, Number) {
      var self = this;
      this.PhoneTypeID = ko.observable(typeID);
      this.PhoneNumber1 = ko.observable(Number);
      this.PhoneTypeName = ko.computed(function() {
        switch (self.PhoneTypeID().toString()) {
          case '1':
            return 'Mobile';
            break;
          case '2':
            return 'Home';
            break;
        }
      });
    }
    $(document).ready(function() {
      var phoneList = new phoneListVM()
      ko.applyBindings(phoneList);
    });
    .editor {
      background-color: rgba(255, 255, 255, 0.7);
      border: 1px solid rgba(0, 0, 0, 0.2);
      padding: 10px;
    }
    <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>
    <h4>
    Edit First Phone
    </h4>
    <div class="editor" data-bind="with: SelectedPhone, visible: SelectedPhone()">
      <p>Phone Number:
        <input data-bind="value: PhoneNumber1" />
      </p>
      <p>Phone Type:
        <select data-bind="value:PhoneTypeID">
          <option value="0"></option>
          <option value="1">Mobile</option>
          <option value="2">Home</option>
        </select>
      </p>
    </div>
    
    <h4>
    All Phones
    </h4>
    <table>
      <thead>
        <tr>
          <th>Type ID</th>
          <th>Type Name</th>
          <th>Number</th>
        </tr>
      </thead>
      <tbody data-bind="foreach:phones">
        <tr>
          <td><span data-bind="text:PhoneTypeID"></span></td>
          <td><span data-bind="text:PhoneTypeName()"></span></td>
          <td><span data-bind="text:PhoneNumber1"></span></td>
          <td>
            <button data-bind="click: $root.EditPhone">
              Edit
            </button>
          </td>
        </tr>
      </tbody>
    </table>
    <button type="button" data-bind="click: RemoveFirstPhone, visible: phones().length > 0">
      Remove First Phone
    </button>

    【讨论】:

      【解决方案2】:

      我同意 customBinding 过度工作的其他两个答案。 但正如你所说,你不能轻易更改代码,我将向你展示你的问题。

      在您的自定义绑定声明中,您刚刚定义了 init 函数,但将 update 函数留空。那就是问题所在。当您在选择框上进行更改时,会触发 change 事件,但没有事件处理程序,所以什么也没发生。

      事实上,您的自定义绑定已成功将change 事件处理程序添加到选择框。这就是为什么在删除第一个电话号码之前,一切正常。但是当您删除第一个电话号码时,事件处理程序会被删除,因为您只能添加一个事件处理程序change 事件的选择框。

      解决方法是:init 函数留空,并将当前init 函数的所有内容移至update 函数,如下所示。

      ko.bindingHandlers.selectedTextValue = {
        init: function(element, valueAccessor) {
      
        },
        update: function(element, valueAccessor) {
          var value = valueAccessor();
      
          $(element).change(function() {
            value($("option:selected", this).text());
          });
        }
      };
      

      【讨论】:

        【解决方案3】:

        你可能想多了。无需创建自定义活页夹。你可以使用knockout的options binding

        // create an array
        var phoneTypes = [{
          text: "Mobile",
          value: 1
        }, {
          text: "Home",
          value: 2
        }];
        
        function phoneListVM() {
          var _self = this;
          // this will be bound to the dropdown
          _self.phoneTypes = ko.observableArray(phoneTypes)
          this.phones = ko.observableArray([
            new phoneVM(1, "Mobile", "123-234-3456"),
            new phoneVM(2, "Home", "654-343-3211")
          ]);
        
          this.firstPhone = ko.computed(function() {
            return _self.phones()[0];
          });
        }
        
        function phoneVM(typeID, typeName, Number) {
          var self = this;
        
          this.PhoneTypeID = ko.observable(typeID);
          this.PhoneNumber1 = ko.observable(Number);
        
          // get the value from the phonetypes array using the PhoneTypeID
          self.PhoneTypeName = ko.computed(function() {
            var type = phoneTypes.filter(function(a) {
              return a.value === self.PhoneTypeID()
            });
            return type.length > 0 ? type[0].text : undefined;
          })
        }
        

        并将 HTML 更改为:

        <select data-bind="options: phoneTypes,
                           optionsText: 'text',
                           optionsValue: 'value',
                           optionsCaption: 'Choose',
                           value: firstPhone().PhoneTypeID">
        </select>
        

        您可以将复杂对象作为knockout 中的选定值。所以你可以在phoneVM 中有一个PhoneType 属性并将PhoneType 的2 个属性绑定到text

        Here's an updated fiddle


        我不太明白为什么您只允许对第一个选项进行编辑,或者用户将如何编辑第二个选项。但是,您可以查看this fiddle,了解如何使项目列表中的每个项目都可编辑。


        cmets 后更新:

        即使select 不是由淘汰赛绑定创建的,您仍然不需要自定义绑定。您可以像我之前建议的那样将PhoneTypeName 设为computed 属性,并根据PhoneTypeIDoptions 获取text

        function phoneVM(typeID, typeName, Number) {
          var self = this;
          this.PhoneTypeID = ko.observable(typeID);
          this.PhoneNumber1 = ko.observable(Number);
        
          self.PhoneTypeName = ko.computed(function() {
            var type = $('#select option[value='+self.PhoneTypeID() +']');
            return type.length > 0 ? type.text() : undefined;
          });
        }
        

        Here's an updated fiddle

        您的更改事件没有被触发的原因可能是因为您添加事件的 element 现在已从 DOM 中删除。

        【讨论】:

        • 谢谢!这是一个不错的解决方案,但我的下拉列表当前是在服务器端(asp.net mvc)生成的,并且没有使用淘汰赛。我试图避免将敲除与同样绑定在服务器端的另一个输入一起使用的复杂性。在这里尝试澄清是我拥有的类似基本功能的一个示例(我说的部分很好用):demos.telerik.com/aspnet-mvc/grid/editing-popup。除此之外,我正在尝试添加直接在页面上输入第一项的能力(它没有删除按钮,仅供参考)。我的小提琴只显示问题,而不是我的功能
        • @xr280xr 我已经相应地更新了答案。测试一下,然后告诉我。
        猜你喜欢
        • 2023-03-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-07
        • 2012-08-23
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多