ArrayWidget
Dynamic length array widget.
HTML
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/uikit/2.27.2/css/uikit.almost-flat.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.5.1/Sortable.js"></script>
<form class="uk-form uk-form-horizontal">
<fieldset class="uk-form-row">
<label class="uk-form-label">Name</label>
<div class="uk-form-controls">
<input name="name" value="Alice" class="uk-form-width-medium">
</div>
</fieldset>
<fieldset class="uk-form-row">
<label class="uk-form-label">Tags</label>
<div class="uk-form-controls">
<input type="text" value="foo,bar,baz" name="values">
</div>
</fieldset>
<fieldset class="uk-form-row">
<div class="uk-form-controls">
<button type="submit" class="uk-button">
<i class="uk-icon-check"></i> submit
</button>
<button type="reset" class="uk-button">
<i class="uk-icon-times"></i> reset
</button>
</div>
</fieldset>
</form>
<template data-array-widget-row>
<fieldset data-array-row>
<span class="uk-form-icon uk-form-icon-flip">
<input class="uk-form-width-medium">
<i class="uk-icon-ban" data-array-remove-row></i>
</span>
</fieldset>
</template>
<template data-array-widget-add>
<button class="uk-button" type="button">
<i class="uk-icon-plus"></i> Add new
</button>
</template>
<pre>name=Alice&values=foo,bar,baz</pre>
<!-- spacer to make output larger, since we dynamically add elements -->
<div style="height: 200px"></div>
CSS
form {
padding: 12px;
}
input[name=values] {
opacity: 0.5;
float: right;
}
[data-array-row] [data-array-remove-row] {
pointer-events: initial;
cursor: pointer;
}
input[disabled] + [data-array-remove-row]::before {
content: "\f0e2";
}
fieldset[data-array-row] {
margin-bottom: 2px;
}
JavaScript
function ArrayWidget(element) {
// We need to be able to reset the value to the initial, if our form
// gets told to reset.
var initialValue = element.value;
var rowTemplate = document.querySelector('[data-array-widget-row]');
var buttonTemplate = document.querySelector('[data-array-widget-add]');
// Wrap the whole widget inside a fieldset, as it enables us to have event
// listeners here, that we can use for delegation.
var widget = document.createElement('fieldset');
element.replaceWith(widget);
widget.appendChild(element);
// Also use a seperate container for just the input elements.
var valuesList = document.createElement('fieldset');
widget.appendChild(valuesList);
var newValueButton = document.importNode(buttonTemplate.content, true);
newValueButton.querySelector('button').addEventListener('click', addValue);
widget.appendChild(newValueButton);
function recalculateValue(event) {
// Set the element (which in reality would be a hidden field) to have a value
// that is the list of values, but only from widgets that are not disabled.
element.value = Array.from(valuesList.querySelectorAll('input'))
.filter((input) => !input.disabled)
.map((input) => input.value)
.join(',');
element.dispatchEvent(new Event('change', {bubbles: true}));
}
valuesList.addEventListener('change', recalculateValue);
// When the remove button is clicked and we are an existing (loaded from server)
// value, we just want to toggle disabled: this means it's easy for the user to
// revert that change. Perhaps all deletions should just trigger this?
// We also trigger a recalculateValue() whenever we have this.
function toggleValue(event) {
var input = event.target.previousElementSibling;
input.disabled = !input.disabled;
recalculateValue();
}
function removeValue(event) {
event.target.parentElement.remove();
...