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&amp;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();
   ...