Forms as state containers Part 2: managing complex data

This is a small series on HTML forms. The previous post was Forms as state containers Part 1: forms as a single source of truth

JSON?

The value of a form can be seen as a key/value map, a plain object in JSON. From this follows that the value of a subform naturally becomes an object in the parent form. In HTML5 subforms can be expressed as a fieldset element. However, the javascript interface of a fieldset is not a natural fit as a subform, since it only groups elements in the parent form: the elements remain accessible in the parent form. Wrapping the fieldset interface slightly in a thin javascript layer, we can treat fieldsets as a true subform, and we can address its "value" property as an object. In the unlikely event that the form doesn't have access to javascript, we may have have another solution, as we'll see later on.

Repeating values obviously fit naturally to javascript arrays. The "value" property of, for example, a multi-select gets (or sets) the array value of the form component. This is all quite obvious, you might think, and with the advent of JSON you just send your form data as a JSON document to the server with an Ajax request. However, this does not adhere to the age-old adagium of unobtrusive javascript. And although graceful degradation and adherence to standards seems a thing of the past, we may still need to send complex form data over the wire using the built-in mechanisms. Unfortunately, this is not so trivial.

The internet mime type of form data traditionally is by default application/x-www-form-urlencoded, the only other flavor is multipart/form-data. Only these formats are available if you want to send form data to the server using the built-in HTTP methods (AKA "verbs") GET or POST that are available on the HTML form element. There is a solution, however, and it comes from an unlikely place...

Look ma, no JSON!

Originally, PHP was a form processing language for the web, and was even briefly named FI (Form Interpreter). From early on it had a way of handling hierarchical form data on the server. When a field name in a URL-encoded piece of data ends with a matching pair of square brackets, the value is interpreted as an array:

myarray[]=1;myarray[]=2;myarray[]=3

In the receiving PHP script, myarray will contain the following array:

[1,2,3]

If the order is important, indices may be specified:

myarray[2]=1;myarray[1]=2;myarray[0]=3

Translates to:

[3,2,1]

You can even encode key/value maps in post data you send to PHP, by supplying a string instead of an integer within the enclosing square brackets:

mymap[a]=1;mymap[b]=2;mymap[c]=3

On the server, mymap will contain:

{"a": 1, "b": 2, "c": 3}

You've guessed it, you can't encode key/value maps with integers as keys, but that doesn't matter, because you also can't use integers as names for form components.

HTML imports

We successfully resolved the issue of sending complex data to the server, but we didn't even have a use case yet! Let's take the largest challenge I can currently think of head-on: creating an Excel-like tabular data grid for editing rows and columns with arbitrary information. From a form perspective, we have an array of repeatable, yet identical, subform instances, where the values in each subform can be edited by the user. For the moment, I'll ignore properties for individual rows or columns, like custom formatting or data types.

Can we create such a beast using just HTML forms? Well, we could start by introducing a fieldset element, and stating that it should be interpreted as an array:

<form name="datagrid">
  <fieldset name="row[]">
    <!-- this will contain subform components -->
  </fieldset>
</form>

Column names are initially sequential letters starting with A. So for starters, let's just take A to G to create the default subform that will be imported:

<fieldset name="row[]">
  <input name="A" type="text" />
  <input name="B" type="text" />
  <input name="C" type="text" />
  <input name="D" type="text" />
  <input name="E" type="text" />
  <input name="F" type="text" />
  <input name="G" type="text" />
</fieldset>


Eventually we will want to repeat both rows and columns, so we can add them more generically, but to just to drive our point home, I present to you my pure HTML form for editing tabular data:



I didn't even have to use tables. So, how can we repeat subforms without all this copy-pasting I just did? Well, we could just write a simple javascript to repeat rows and columns, but it wouldn't be pure HTML anymore. What if I could at least import the fieldset, as a snippet of code, and insert it into the main form without the aid of custom scripts? At first I thought HTML imports could provide a standard way of doing this, but alas... HTML imports aren't about importing HTML at all! They're for bundling functionality, which requires a lot of javascript. Yikes! However, the idea isn't bad, so I'll just use it as I think it should work. To cut it short, the above snippet is packed as separate form piece, and can be considered the default row.

Once the pieces are put together, we obviously need a way to store actual data, or retrieve what we've already stored before, and populate the form with it. We need some formal way of expressing that we want to retrieve multiple rows, which in terms of data means an array. I usually use the following convention: if the URL contains a query, the result will be an array, and if it doesn't, the URL should contain an identifier, in which case the result will be a key/value map.

So, to insert the data located at /row, we create a query to request an array from the server, and import in into the form:

/row?limit=100

The above URL will just return a hundred rows of data. We could use JSON, as the common conviction is that markup contains a lot more data. But since our intent is to insert subform instances, we should still use plain old HTML, as it's much more predictable. And how much bigger is properly compressed markup really?

Below is a working prototype, provided the links are resolved directly at their current location. In case there's no data saved yet, there's only the empty default row. Once the form is submitted, the data is stored, and in that case there will be one row of data, and again an empty row at the bottom.

<form>
  <link href="/header.html" rel="import"></link>
  <link href="/rows?limit=100" rel="import"></link>
  <link href="/default-row.html" rel="import"></link>
</form>


This is just a very basic prototype, that can be enhanced in many ways, for example with sorting and filtering, operations on single cells, you name it. But before you start hacking right away using your favorite javascript framework of the month, consider starting from traditional HTML forms.

Next up: Forms as state containers Part 3: validation

Comments

Popular posts from this blog

Abandoning hope... and XForms

JS Journey into outer space - Day 5

JS Journey into outer space - Day 4