Theory: Extending HAL to support simple forms

Monday, May 1, 2017

Theory: Extending HAL to support simple forms

HAL is a format created by Mike Kelly used to make APIs more explorables. It can also be used for automatically generating parts of the UI via its links. But what if it could be pushed a bit further?

Motivation

Most forms can be made generic in many Enterprise Applications (Web and Mobile). Their inputs are usually simple. They don’t usually have much in terms of prettyness. They are temporary holders of information that would eventually be sent over to the backend. Therefore, if you have a way to describe what your form should look like, it should be possible render them generically on any client platform, be it Web or Mobile.

HAL already provides a way to incorporate some metadata within the resource being returned.

{
    "_links": {
        "self": { "href": "/orders" },
        "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
        "next": { "href": "/orders?page=2" },
        "ea:find": {
            "href": "/orders{?id}",
            "templated": true
        },
        "ea:admin": [{
            "href": "/admins/2",
            "title": "Fred"
        }, {
            "href": "/admins/5",
            "title": "Kate"
        }]
    },
    "currentlyProcessing": 14,
    "shippedToday": 20,
    "_embedded": {
        "ea:order": [{
            "_links": {
                "self": { "href": "/orders/123" },
                "ea:basket": { "href": "/baskets/98712" },
                "ea:customer": { "href": "/customers/7809" }
            },
            "total": 30.00,
            "currency": "USD",
            "status": "shipped"
        }, {
            "_links": {
                "self": { "href": "/orders/124" },
                "ea:basket": { "href": "/baskets/97213" },
                "ea:customer": { "href": "/customers/12369" }
            },
            "total": 20.00,
            "currency": "USD",
            "status": "processing"
        }]
    }
}

I believe it could be possible to extend it to incorporate metadata that could guide the implementation of a UI form.

{
  "firstName": "04c48da7-5b2d-4eb4-a93e-50a22979394b",
  "lastName": "99e7d915-471a-481c-8e13-57523af39cad",
  "age": 77,
  "description": "6b51e2fa-a1f9-47bf-8ac1-944a7e60bdd4",
  "_links": {
    "self.Save": [
      {
        "href": "/Example/SaveExample1",
        "kind": "Save",
        "name": "Save"
      }
    ]
  },
  "_presentable": {
    "fields": [
      {
        "restrictions": [
          "Required"
        ],
        "boundTo": "firstName",
        "editable": true,
        "name": "First Name"
      },
      {
        "restrictions": [
          "Required"
        ],
        "boundTo": "lastName",
        "editable": true,
        "name": "Last Name"
      },
      {
        "restrictions": [
          "Required"
        ],
        "boundTo": "description",
        "editable": true,
        "name": "Description"
      },
      {
        "restrictions": [
          "Optional"
        ],
        "boundTo": "age",
        "editable": false,
        "name": "Age"
      }
    ],
    "actionBoundTo": "self.Save"
  },
  "_metadata": {
    "objectName": "Example1",
    "description": "Example 1"
  }
}

The json above is a variant of HAL that includes 2 new properties: _metadata and _presentable.

The _metadata field could be used to provide additional info on the form. In theory, it could even be used to infer the name of the javascript function to be called on form submit.

The _presentable field contains a description of the fields that can be presented as well as their input restrictions. It also contains the name of the link/action that can be called to send the form to the backend.

Here’s how it could be coded up in C#.

function buildForm(containerId,obj) {
    if (!obj["_presentable"]) {
        return;
    }

    var presentable = obj["_presentable"];
    var links = obj["_links"];

    var form = document.createElement("form");
    var formLink = links[presentable.actionBoundTo][0].href
    form.setAttribute("method", "POST");
    form.setAttribute('action', formLink);

    presentable.fields.forEach(function(element) {
      var elemContainer = document.createElement("div");
      elemContainer.setAttribute("class", "form-group");

      var label = document.createElement("label");
      label.innerText=element.name;
      
      var input = document.createElement("input");
      input.setAttribute("type", "text");
      input.setAttribute("name", element.boundTo);

      if (!element.editable){
          input.setAttribute("readonly","true");
      }

      if(element.restrictions.indexOf('Required') > -1){
          input.setAttribute("required","true");
      }

      var bindings = element.boundTo.split('.');
      var inputVal = obj[bindings[0]];

      if (inputVal){
        for (i = 1; i < bindings.length; i++) { 
            inputVal = inputVal[bindings[i]]; 
        }

        input.setAttribute("value",inputVal);
      }
      
      elemContainer.appendChild(label);
      elemContainer.appendChild(input);

      form.appendChild(elemContainer);
    });

    var submit = document.createElement("button");
    submit.setAttribute("class", "btn btn-success");
    submit.setAttribute("type", "submit");
    submit.innerText = links[presentable.actionBoundTo][0].name;

    form.appendChild(submit);

    var container = document.getElementById(containerId);
    container.innerHTML = '';

    container.appendChild(form);

}

And here’s how it could be rendered in Javascript.

  public enum LinkKind
    {
        Fetch,
        Save,
        Update
    }

    public enum Restrictions
    {
        Required,
        Optional
    }

    public class ActionableLink
    {
        public string Href { get; set; }

        [JsonConverter(typeof(StringEnumConverter))]
        public LinkKind Kind { get; set; }
        public string Name { get; set; }
    }

    public class PresentableField
    {
        public PresentableField()
        {
            Restrictions = new Restrictions[0];
        }

        [JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
        public IEnumerable<Restrictions> Restrictions { get; set; }
        public string BoundTo { get; set; }
        public bool Editable { get; set; }
        public string Name { get; set; }
    }

    public class Presentable
    {
        public Presentable()
        {
            Fields = new PresentableField[0];
        }

        public IEnumerable<PresentableField> Fields { get; set; }
        public string ActionBoundTo { get; set; }
    }

    public class Metadata
    {
        public string ObjectName { get; set; }
        public string Description { get; set; }
    }

    public abstract class ExtendedModel
    {
        public ExtendedModel()
        {
            Links = new Dictionary<string,IEnumerable<ActionableLink>>();
        }

        [JsonPropertyAttribute("_links")]
        public Dictionary<string,IEnumerable<ActionableLink>> Links { get; set; }

        [JsonPropertyAttribute("_presentable")]
        public Presentable Presentable { get; set; }

        [JsonPropertyAttribute("_metadata")]
        public Metadata Metadata { get; set; }
    }

UPDATE: Turns out there was already such an extension to the format called HAL-Forms.