Complex Custom Editor Fields with Umbraco Forms, Part I

I've recently started playing around with Umbraco Forms (formerly Contour) as an option on some of the work sites. The main reason for using custom forms rather than Contour/Umbraco Forms was the lack of custom editor types. In Forms, you have a few options, each of which basically maps to one of string, text (long string), date/time, boolean or, in the case of a file upload, maybe a blob.

I needed a way to essentially provide an extensible grid. We'll use an online food ordering option as an example, and we'll call our editor ItemGrid.

The Model

You'll need a C# model, as per the documentation. The data type should be FieldDataType.LongString since we'll be storing the data as JSON.

public ItemGrid() {  
    //Provider
    this.Id = new Guid("D6A2C406-CF89-11DE-B075-55B055D89593 ");
    this.Name = "ItemGrid";
    this.Description = "Renders a html input";
    this.Icon = "icon-table";
    this.DataType = FieldDataType.LongString;
    this.SortOrder = 10;
}

The Grid

Let's begin with some basic markup. We want the ability to add more rows to a tabular data field. We'll call our editor ItemGrid, which means this will be a partial in ~/Views/Partials/Forms/Fieldtypes/FieldType.ItemGrid.cshtml.

@model Umbraco.Forms.Mvc.Models.FieldViewModel
<table id="table_@Model.Id">  
  <thead>
    <tr><th>Item</th><th>Price</th><th>Quantity</th><th>Subtotal</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><input type="text" data-field="item" id="item_0"/></td>
      <td><input type="text" data-field="price" id="price_0"/></td>
      <td><input type="text" data-field="quantity" id="quantity_0"/></td>
      <td><input type="text" data-field="subtotal" id="subtotal_0"/></td>
    </tr>
  </tbody>
</table>  
<a href="#" id="addRow">Add Row</a>  
<input type="hidden" name="@Model.Name" id="@Model.Id" value="@Model.Value"/>  

Quick recap: we have a table with four columns: item, price (per unit), quantity and subtotal (= quantity * price); we have a link to add another row, and a hidden input which maps to the field in the C# model (this is where we'll actually store our data).

The JavaScript

We need to map the values from the table into the hidden field. While Umbraco Forms requires jQuery (which means you can use $ selectors and functions), we'll use some vanilla javascript, although I'm going to use some ES6 syntax for brevity. Just below the HTML:

// adds a row to the table.
function addRow() {  
  var body  = document.querySelector("#table_@Model.Id > tbody")
    , rows  = body.querySelectorAll("tr")
    , index = rows.length
    , row   = `<tr>
        <td><input type="text" data-field="item" id="item_${index}"/></td>
        <td><input type="text" data-field="price" id="price_${index}"/></td>
        <td><input type="text" data-field="quantity" id="quantity_${index}"/></td>
        <td><input type="text" data-field="subtotal" id="subtotal_${index}"/></td>
      </tr>`
    ;
  body.insertAdjacentHTML('beforeend', row);
  addEventListeners(); // re-add event listeners for the new row.
}

// updates the value of the submitted (hidden) field
function updateJson() {  
  var data = [];
  document.querySelectorAll("#table_@Model.Id > tbody > tr").forEach(row => {
    var item = {};
    row.querySelectorAll('input').forEach(input => {
      item[input.dataset['field']] = input.value;
    });
    data.push(item);
  });
  document.getElementById('@Model.Id').value = JSON.stringify(data);
}

// adds event listeners to the inputs.
function addEventListeners() {  
  document.querySelectorAll("#body_@Model.Id > tbody input").forEach(input => {
    input.addEventListener('blur', updateJson);
});
}

// add a click event listener for the link to add another row.
document.getElementById('addRow').addEventListener('click', addRow);  
// add the event listeners to the existing inputs.
addEventListeners();  

So there's a fair bit to unpack there. Let's go through it in more detail.

The addRow function adds a row to the table; the index for the ids of the fields is the current number of rows (since ids are 0-based).

The updateJson function loops through all the inputs in the table, and turns each row into an object, with the properties set by the data-field property on each input. This is then serialized and stored in the hidden input.

The addEventListeners function loops through the inputs in the table and adds a blur event listener to each; i.e., when the input loses focus, the data is re-serialized for the hidden input. You'll notice that this function is also called in the addRow function, since the newly created row won't have the listeners added by default.

Lastly, we add the addRow function to the click event on the link, and initialize the event handlers for the first row.

Conclusion

It's rough at the moment; there's no validation, and the Umbraco back office will just show a string representation of the array of data. But it's a fairly simple way to add more custom editors -- the overarching principle is that we only want to submit a single field with the editor, so we handle as much of the processing client-side as possible.

Future updates will (hopefully) look at pre-values (e.g. filtered lists for the item input), validations (which are basically just custom javascript), and improved display of the data in the admin interface and emails.

Matt Redmond

Musician, composer, actor, engineer, pirate.

Adelaide, Australia