1

I found questions that discuss using a multiselect based on another multiselect, cloning a form row, and dynamically updating select options. But, there are no questions that answer how to do all three together.

In my specific example, I have two dropdowns side by side - an operation_key and operation_value. Based on the user's selection for operation_key, they should see either a regular (single) select or a multiselect and have the appropriate options dynamically populated in the respective operation_value dropdown.

Here's a mapping for how it should work:

+---------------+---------------------------------+---------------------------------------+
| Operation Key | Operation Value - Dropdown Type |  Operation Value - Dropdown Options   |
+---------------+---------------------------------+---------------------------------------+
| continents    | multiple                        | North  America, South America, Europe |
| languages     | multiple                        | English, French, Spanish              |
| eye_color     | single                          | Brown, Blue, Green, Hazel             |
| age           | single                          | 18-24, 25-32, 33-40, >41              |
+---------------+---------------------------------+---------------------------------------+

I combined the code from the various examples and came up with the following:

// clone functionality

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

function setIndex(elements) {
  elements.each(function(index, element) {
    // set the appropriate index on the cloned element (not working)
    $(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();

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

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

$('.operation-keys').on('change', function() {
  let operationKey = $(this).val()
  // get the nearest value dropdown element (not working)
  let valueSelect = $(this).closest('.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")
  }
  // populate the values  (not working)
  let options = result['operationValues'];
  for (var i = 0; i < options.length; i++) {
    valueSelect.options.add(new Option(option.toLowerCase(), option));
  }


});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<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">
<link rel="stylesheet" href="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/css/bootstrap-multiselect.css" type="text/css" />


<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="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>

  <script type="text/javascript" src="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/js/bootstrap-multiselect.js"></script>

JSFiddle: https://jsfiddle.net/vs6oLugy/

There are a few problems:

1) The setIndex() function is supposed to dynamically update the names of the cloned .operation_keys and .operation_values fields. For example, operation_keys[1], operation_values[1], operation_keys[2], operation_values[2], etc. For some reason, it's not working. I presume it's because $(element).find("select.operation_keys") isn't targeting the element correctly, but I can't figure out why it's not.

2) The rest of the logic which finds and updates the respective .operation_values dropdown with the appropriate type (regular select or multiselect) and sets the available choices/options is not working. I think the problem is the line which targets the dropdown to be modified:

let valueSelect = $(this).closest('.operation-values');

Each of the selects is inside a parent .col-6 div, so maybe $(this).closest() isn't able to traverse upwards through the DOM tree? Though, I'm not sure what should change to get this part working either. I've tried looking into siblings and getting the parent element first, but neither has worked.

3) When a row is cloned, I'm not sure how to hide the second (right--most) dropdown (operation_value) to encourage the user to make a selection on the left dropdown (operation_key) first. When a row is cloned, the row that it was cloned from presumably may have had both dropdowns visible, so is it safe enough to just call .hide() on the new right-most dropdown created by the clone() function?

Any help is much appreciated!

tzazo
  • 323
  • 3
  • 13

2 Answers2

0

There are a few things to consider in this code:

  1. You are using jQuery. But you mix the code with vanilla JS.
  2. A few steps were forgotten, like an else to remove the multiple attribute and emptying the select when new options are added.
  3. options returns a readonly list.
$('.operation-keys').on('change', function() {
  let operationKey = $(this).val()
  let valueSelect = $('.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'];
  //make the list empty again
  valueSelect.empty();
  for (var i = 0; i < options.length; i++) {
    //use jQuery's append and add options through string!
    valueSelect.append(`<option value="${options[i].toLowerCase()}" >${options[i]}</option>`);
  }


});

// clone functionality

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

function setIndex(elements) {
  elements.each(function(index, element) {
    // set the appropriate index on the cloned element (not working)
    $(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();

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

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

$('.operation-keys').on('change', function() {
  let operationKey = $(this).val()
  let valueSelect = $('.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'];
  valueSelect.empty();
  for (var i = 0; i < options.length; i++) {
    valueSelect.append(`<option value="${options[i].toLowerCase()}" >${options[i]}</option>`);
  }


});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<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">
<link rel="stylesheet" href="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/css/bootstrap-multiselect.css" type="text/css" />


<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="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>

  <script type="text/javascript" src="https://github.com/davidstutz/bootstrap-multiselect/blob/master/dist/js/bootstrap-multiselect.js"></script>
Mouser
  • 13,132
  • 3
  • 28
  • 54
  • I was able to get it almost there, but try adding a row on this version (jsfiddle.net/y9kqdxt8) and then make a selection using the left dropdown. valueSelect.empty(); seems to be wiping out all of the options in prior dropdown rows instead of just the row you're currently on. – tzazo Jan 26 '20 at 21:49
0

There are few changes needed in your code

  // set the select type, if needed
  if (result['type'] == 'multiple') {
      $('#operation-values').attr("multiple", "multiple");
  } else {
      $('#operation-values').removeAttr("multiple");
  }

  // populate the values  (not working)
  let options = result['operationValues'];

  $('#operation-values').html('');
  for(var i = 0; i < options.length; i++) {
     $('#operation-values').append('<option value="'+options[i].toLowerCase()+'">'+options[i]+'</option>');
  }
sarfraaz talat
  • 635
  • 4
  • 10