JSONPairsWidget

Dynamic length json pairs widget.

by schinckel

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">Data</label>
    <div class="uk-form-controls">
      <input type="text" value='{"foo":"bar", "baz":true}' 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>
    <input class="uk-form-width-medium" data-key>
    <span class="uk-form-icon uk-form-icon-flip">
      <input class="uk-form-width-medium" data-value>
      <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 zip(first, second) {
	return Array.prototype.map.call(first, function(e, i){ return [e, second[i]]});
}

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.
      if (initialValue && initialValue[0] == '{') {
        var value = {};
        Array.apply(null, valuesList.children).filter(function(fieldset) {
          return !fieldset.disabled
        }).forEach(function(row, i) {
          // How do we know if we should be using the string value of this field, or interpret that
          // as a JSON value. For instance, if it's a number or boolean (or null)?
          var rowValue = row.querySelector('[data-value]').value;
          try {
            rowValue = JSON.parse(rowValue);
          } catch(e) {}
          value[row.querySelector('[data-key]').value] = rowValue;
        });
        element.value = JSON.stringify(value);
      } else {
        element.value = Array.apply(null,...