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!