为什么抛出“无法读取未定义的属性'length'”?

时间:2020-01-26 21:25:11

标签: javascript jquery

我有两列的表单,其中包含左右下拉菜单。

根据用户在左侧下拉列表中选择的内容,右侧下拉列表将呈现为纯下拉列表(<select>或多选(<select multiple="multiple">)并填充值。

大多数情况下都有效,但是在添加一行然后更改新添加的行中的左侧下拉选择时,控制台出现Uncaught TypeError: Cannot read property 'length' of undefined错误。该代码按预期工作(它会更新正确的下拉列表),但是我不确定为什么首先将错误引发该错误。

示例:

const regex = /^(.+?)(\d+)$/i;
let cloneIndex = $(".operation").length;

bindToSelector($('.operation-keys'));

function setIndex(elements) {
  elements.each(function(index, element) {
    $(element).find("select.operation-keys").attr("name", "operation-keys[" + index + "]");
    $(element).find("select.operation-values").attr("name", "operation-values[" + index + "]");
  });
}

function clone() {

  let $removeButton = $(this).closest(".actions").find(".remove-operation");
  let $closestOperation = $(this).closest(".actions").prev(".operation").first();

  $removeButton.show();

  let selector = $closestOperation.clone()
    .insertAfter($closestOperation).attr("id", "operation-" + cloneIndex)
    .find("*")
    .each(function() {
      let id = this.id || "";
      let match = id.match(regex) || [];
      if (match.length == 3) {
        this.id = match[1] + '-' + (cloneIndex);
      }
    });
  cloneIndex++;

  let $Operations = $(this).closest(".operation-parent").find(".operation");

  setIndex($Operations);

  bindToSelector(selector);
}

function remove() {
  $(this).closest(".actions").prev(".operation").remove();

  let $Operations = $(this).closest(".operation-parent").find(".operation");
  setIndex($Operations);
  let totalElements = $(this).closest(".operation-parent").find(".operation").length;
  if (totalElements === 1) {
    $(this).hide();
  }
  cloneIndex--;
}

$(".add-operation").on("click", clone);
$(".remove-operation").on("click", remove);

// select logic

function buildSelect(operationKey) {
  let result = {};
  switch (operationKey) {
    case 'continents':
      result['operationValues'] = ['North America', 'South America', 'Europe'];
      result['type'] = 'multiple';
      break;
    case 'languages':
      result['operationValues'] = ['English', 'French', 'Spanish'];
      result['type'] = 'multiple';
      break;
    case 'eye_color':
      result['operationValues'] = ['Brown', 'Blue', 'Green', 'Hazel'];
      result['type'] = 'single';
      break;
    case 'age':
      result['operationValues'] = ['18-24', '25-32', '33-40', '>41'];
      result['type'] = 'single';
      break;
  }
  return result;
}

function bindToSelector(selector) {
  $(selector).on('change', function() {
    let operationKey = $(this).val()
    let valueSelect = $(this).parent().next().find('.operation-values');
    
    // get the values + type of select we should populate
    let result = buildSelect(operationKey);
    // set the select type, if needed
    if (result['type'] == 'multiple') {
      valueSelect.attr("multiple", "multiple");
    }
    else
    {
      valueSelect.removeAttr("multiple", "multiple");
    }

    // populate the values
    let options = result['operationValues'];
    console.log(options);
    
    // empty the dropdown in the cloned row (not working - empties all value dropdowns)
    valueSelect.empty();
    for (var i = 0; i < options.length; i++) {
      valueSelect.append(`<option value="${options[i].toLowerCase()}" >${options[i]}</option>`);
    }

  });

}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous"><script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/css/bootstrap-multiselect.css" type="text/css"/>
<script type="text/javascript" src="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/js/bootstrap-multiselect.js"></script>
<script type="text/javascript" src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<div class="form-group row">
  <label class="col-4">Operation Key</label>
  <label class="col-6">Operation Value</label>
</div>
<div class="form-group">

  <div class="operation-parent row">
    <div class="col-8 operation">
      <div class="row">
        <div class="col-6">
          <select class="form-control form-control-lg operation-keys" name="operation_keys[]">
            <option value="" selected disabled>Select One</option>
            <option value="continents">Continents Visited</option>
            <option value="languages">Languages Spoken</option>
            <option value="eye_color">Eye Color</option>
            <option value="age">Age</option>
          </select>
        </div>

        <div class="col-6">
          <select class="form-control form-control-lg mb-30 operation-values" name="operation_values[]">

          </select>
        </div>

      </div>
    </div>
    <div class="col-2 actions">
      <a class="btn btn-alt-success add-operation">+</a>
      <a class="btn btn-alt-danger remove-operation" style="display:none;">-</a>
    </div>
  </div>

违规行是:

for (var i = 0; i < options.length; i++) {

我只是不明白为什么它首先会引发错误。

复制步骤:

  1. 运行代码段,在左侧的下拉列表中进行选择。
  2. 单击+添加新行。
  3. 在新行的左侧下拉列表中进行选择。观察控制台中的错误。

奖励点:添加新行(上面的步骤2)时,如何清除新行右侧下拉列表中的值(因此它与克隆行不同) ?我知道我需要在.empty();末尾某个地方的选择器上调用clone(),但不确定应该使用哪个选择器。

1 个答案:

答案 0 :(得分:0)

我相信问题出在您的克隆函数中:

let selector = $closestOperation.clone()
.insertAfter($closestOperation).attr("id", "operation-" + cloneIndex)
.find("*")
.each(function() {
  let id = this.id || "";
  let match = id.match(regex) || [];
  if (match.length == 3) {
    this.id = match[1] + '-' + (cloneIndex);
  }
});
// ...
bindToSelector(selector);

运行.find(“ *”)时,选择器将变成由10个项目组成的大型数组。然后,每个这些都将发送到选择器函数并绑定到事件处理程序。

更改值时,这些项通过buildSelect函数运行,但不匹配任何内容,因为您只希望通过它发送select元素。因此,将返回默认值{}。该空对象没有尝试使用result['operationValues']访问的值“ operationValues”,因此它是未定义的,因此没有长度。