Data-driven UIs
Tags: ddui, nocode, vanilla, craftcode
Published: 2024-05-01 01:37:44

**DDUIs mean a single source of truth and the benefits that come with it.**

Building on a previous article, [DOM to JSON and back](https://dev.to/craft-code/dom-to-json-and-back-3mgc), we now move on to **data-driven UIs.**

A data-driven <abbr title="user interface">UI</abbr> is a user interface generated dynamically from a query response. In short, the database _schema_ determines the interface to that data.

This gives us a **single source of truth.** Our interface (e.g., forms) _cannot_ get out of synch with our data source. If we update the database, then the interface updates itself instantly.

## Key takeaway
>
> **Here is how things are usually done:** we build a database with a schema.
>
> Then we build an API that sits in front of it. Then we build an interface to that API. All hard-coded.
>
> Have fun keeping them in synch.
>
> What we propose here is to **extend the database schema.** We add enough information to it to allow us to generate the API and UI dynamically. Then we generate them _on the fly._
>
> This means we have **one** source of truth. When that source changes, the interface updates itself instantly to match it.
>
> We call this a **data-driven UI.** It rocks.

## On this page

1. [Start at the finish](#start-at-the-finish)
2. [Widget by widget](#widget-by-widget)
    3. [ID](#id)
    4. [Name](#name)
    5. [Email address](#email-address)
    6. [Display name on profile](#display-name-on-profile)
    7. [The “Save changes” button](#the-save-changes-button)
8. [Putting it all together](#putting-it-all-together)
    9. [Adding HATEOAS](#adding-hateoas)
10. [Oh, the benefits!](#oh-the-benefits)
    11. [Whatʼs next?](#whats-next)

<h2 id="start-at-the-finish">Start at the finish</h2>

To determine what we need to add to our database schema and query response, letʼs start with the **interface.** Thatʼs where the rubber meets the road, right? Weʼll keep it simple.

How about a form used to update a userʼs profile? Something like this:

```html
<form action="#" id="edit-profile" method="POST">
  <input type="hidden" name="_charset_">
  <input type="hidden" name="id" value="255c08b2-6606-424b-a339-d3f9ebe50a21">
  <div class="text-field">
    <label for="name">Name</label>
    <input id="name" name="name" required type="text">
  </div>
  <div class="email-field">
    <label for="email">Email address</label>
    <input id="email" name="email" required type="email">
  </div>
  <div class="boolean-field">
    <label for="display-name-on-profile">
      <input
        id="display-name-on-profile"
        name="displayNameOnProfile"
        type="checkbox"
        value="true"
      >
      Display email on profile
    </label>
  </div>
  <fieldset class="member-picker">
    <legend>Favorite color</legend>
    <label for="gray">
      <input
        checked
        id="gray"
        name="favoriteColor"
        type="radio"
        value="#999"
      >
      gray
    </label>
    <label for="red">
      <input
        id="red"
        name="favoriteColor"
        type="radio"
        value="#f00"
      >
      red
    </label>
    <label for="green">
      <input
        id="green"
        name="favoriteColor"
        type="radio"
        value="#0f0"
      >
      green
    </label>
    <label for="blue">
      <input
        id="blue"
        name="favoriteColor"
        type="radio"
        value="#00f"
      >
      blue
    </label>
  </fieldset>
  <div class="button-bar">
    <button type="submit">Save changes</button>
  </div>
</form>
```

What did we need to know to code this form?

First, we need to know what data it will include and the datatype of each:

- The userʼs **ID,** which is of type `UUID`.
- The userʼs **name,** which is a character `string`. This might have some limitations, such as characters allowed or limits on length.
- The userʼs **email address,** which is of type `EmailAddress`. This type does not yet exist, so we will create it.
- A `boolean` flag to decide whether to display the userʼs email address on their profile or leave it off.

<h2 id="widget-by-widget">Widget by widget</h2>

Now letʼs consider what else we need to know.

<h3 id="id">ID</h3>

We know that the user cannot update their **ID,** so this is readonly. Should the user see this? It would only add to the clutter. So letʼs make it `hidden`.

We donʼt care what type the ID is because we are going to return it unchanged. That said, we know that it is a `UUID`.

From the above, we can see that the proper widget for the ID is an `input` of type `hidden`. We can treat the UUID as a string.

<h3 id="name">Name</h3>

We know that the user can update their **name,** so this field is `mutable`. And `visible`. And itʼs a `string`.

A short string, so that means an `input` of type `text` rather than a `textarea`.

<h3 id="email-address">Email address</h3>

We also know that the user can update their **email address**. And `verify` it by clicking on a link in an email sent to that address. It should be `mutable` and `visible` and of type `EmailAddress`, hence an EmailField.

That gives us a **semantic** advantage. The browser can do validation for us. Or we can add our own, but use the browserʼs validation as a fall back.

We also need to set the **name** and **email address** to `required`.

<h3 id="display-name-on-profile">Display name on profile</h3>

This field requires a simple yes or no. Does the user want to display their email address on their profile?

The type, then, is `boolean`, and the best way to collect this data point is an `input` of type `checkbox`.

If we do this correctly in the database, then the type is a `BOOLEAN` type. So the value is either `TRUE` or `FALSE`. We can build our component so that it works with this type.

Does it matter that the specific widget is a checkbox? No. So we call this widget a **BooleanField**.

<mark>One important distinction of a <abbr title="data-driven user interface">DDUI</abbr> is that it is **schema-centric.** We donʼt care what _widget_ we use. Thatʼs up to the interface. We care about the _datatype_.</mark>

That is why we call this a **BooleanField** instead of a “CheckboxField“. It may seem trivial, but precise language is very important. It helps to re-orient oneʼs brain toward a DDUI approach.

<h3 id="favorite-color">Favorite color</h3>

Now we are asking the user to make a choice from a **set** of options. The key word here is `set`. In short, we are asking the user to choose one `member` from that set.

The widget for this is then a **MemberPicker** because it permits one to choose a single **member** from a set. If we were allowing multiple selections, then weʼd have a **SubsetPicker** (or Chooser or whatever).

Items in a set are also called _elements._ But “ElementPicker“ might be confusing as we are also working with <abbr title="HyperText Markup Language">HTML</abbr> elements.

In this instance, our set has four members, each with a label and a value:

- gray: `#999`
- red: `#f00`
- green: `#0f0`
- blue: `#00f`

Besides the `favoriteColor`, we will need this full set of color options. The back end will have to provide these, but theyʼd be in the HTML anyway.

What widget should we choose? Well, we have options. We could use a `select` element. But for only four choices, it would be nice to see all at once. That means inputs of type `radio`.

But see below.

<h3 id="the-save-changes-button">The “Save changes” button</h3>

We need some way to trigger the **action** that saves our changes. There are several possibilities, but easiest is to wrap our inputs in a `form`. And add a submit `button`.

And here comes [Roy Fielding's](https://en.wikipedia.org/wiki/Roy_Fielding) “Hypertext As The Engine Of Application State”: HATEAOS. Our server needs to tell us which actions are available for Profile.

When we load our example profile page, the user agent (browser) makes an <abbr title="HyperText Transfer Protocol">HTTP</abbr> request to the server. The server sends an HTML document. This links to various other documents: images, stylesheets, scripts, etc.

In our scenario, this page then does an <abbr title="Asynchronous JavaScript and XML">AJAX</abbr> request to retrieve the data.

On most pages, the HTML form is already in place. We use JavaScript to enter into the form using JavaScript. But in our case, with a DDUI app, the form does not exist until the AJAX requests returns. _Then_ JavaScript both builds the form and fills it with the data.

As the form is not hard coded, we donʼt know where to submit it. What is the formʼs `action`? Is it a `GET` or a `POST`? So we need this metadata in our AJAX response.

This has the added benefit of making the <abbr title="Application Programming Interface">API</abbr> discoverable.

<h2 id="putting-it-all-together">Putting it all together</h2>

So here is a first pass at our current schema:

```json
{
  "type": "Profile",
  "properties": [
    {
      "name": "id",
      "type": "uuid",
      "readonly": true,
      "hidden": true
    },
    {
      "name": "name",
      "type": "string",
      "required": true
    },
    {
      "name": "email",
      "type": "EmailAddress",
      "required": true
    },
    {
      "name": "displayNameOnProfile",
      "type": "boolean",
      "default": true
    },
    {
      "name": "favoriteColor",
      "type": "Member",
      "default": "gray",
      "options": [
        {
          "label": "gray",
          "value": "#999"
        },
        {
          "label": "red",
          "value": "#f00"
        },
        {
          "label": "green",
          "value": "#0f0"
        },
        {
          "label": "blue",
          "value": "#00f"
        },
      ]
    }
  ]
}
```

Easy peasy, right?

Our ID is not visible, so we know to use a `hidden` field for it. So:

- Name is a `StringField`. We use an input of type `text`.
- Email is an `EmailField`. This uses an `email` input.
- “displayNameOnProfile” is a `BooleanField`: an input of type `checkbox`.
- And “favoriteColor” is a `MemberPicker`. In this instance, a set of inputs of type `radio`.

We get all this from the **query response** and the **schema.** We no longer need to _hard code_ the form.

We also know to:

1. Default `displayNameOnProfile` to `checked` (true)
2. Set our default `favoriteColor` to `gray`

And we know that the whole thing is the `Profile` type.

We could return this schema _parallel_ to the query response containing our values. But why not insert the current values and return them with our schema?

Our Profile schema with current values:

```json
{
  "type": "Profile",
  "properties": [
    {
      "name": "id",
      "type": "uuid",
      "readonly": true,
      "hidden": true,
      "value": "255c08b2-6606-424b-a339-d3f9ebe50a21"
    },
    {
      "name": "name",
      "type": "string",
      "required": true,
      "value": "Bob Dobbs"
    },
    {
      "name": "email",
      "type": "EmailAddress",
      "required": true,
      "value": "bob@dobbs.guru",
      "verified": true
    },
    {
      "name": "displayNameOnProfile",
      "type": "boolean",
      "default": true,
      "value": false,
    },
    {
      "name": "favoriteColor",
      "type": "Member",
      "default": "gray",
      "options": [
        {
          "label": "gray",
          "value": "#999"
        },
        {
          "label": "red",
          "value": "#f00"
        },
        {
          "label": "green",
          "value": "#0f0"
        },
        {
          "label": "blue",
          "value": "#00f"
        },
      ],
      "value": "blue"
    }
  ]
}
```

OK, now what can we do with this?

<h3 id="adding-hateoas">Adding HATEOAS</h3>

As mentioned above, we need to include a set of available **actions.** For our Profile, these are **CRUDL:** create, retrieve, update, delete, and list. Letʼs put them in an `actions` property and add them to our `Profile` schema:

```json
{
  "actions": {
    "create": {
      "method": "PUT",
      "url": "/profiles/{id}"
    },
    "retrieve": {
      "method": "GET",
      "url": "/profiles/{id}"
    },
    "update": {
      "method": "PATCH",
      "url": "/profiles/{id}"
    },
    "delete": {
      "method": "DELETE",
      "url": "/profiles/{id}"
    },
    "list": {
      "method": "GET",
      "url": "/profiles"
    }
  }
}
```

Of course, we may not permit all these actions all the time. We have to consider **authorization** as well. But then we simply leave them off the `actions` list.

There is much more that we can do with this! For example, we can un-camelCase the `name` property to get the field label. Better, letʼs default to that, but use a `label` property to override the `name` where needed.

We can also add more data to our actions. For example, a label for the button (defaulting to “create”, etc.). Or a description of what the action does. Or limits on the action, such as a `Duration` that disables it except during that duration. (Or the reverse.)

The possibilities are endless.

<h2 id="oh-the-benefits">Oh, the benefits!</h2>

The most important benefit of a DDUI is the one mentioned at the top of this essay: **a single source of truth.** But there is so much more.

This approach requires **much less effort.** Devs donʼt hard code the same set of components over and over again. Instead, they maintain the `jsonToDom` mapping. Once we build this renderer, we need only to add occasional new options.

Your database schema then represents two things:

- Your domain
- Metadata to help configure the interface

Your database already knows the datatype of everything you persist. Why not clue in the front end? We can also include `metadata` to permit us to configure the front end.

For example: we used `radio` buttons for our `MemberPicker`. But what if there are fifty options? Then weʼd likely want a `select` element instead. Or some kind of “typeahead” widget. Our `MemberPicker` widget will output any of these, but how does it know which one to use?

One way is to set the type of widget in the query response, but thatʼs a bad idea. The schema should have no knowledge or interest in _how_ we present these data.

1. For one to seven options, use `radio` buttons.
2. For eight to thirty-two options, use a `select` element.
3. For thirty-three or more options, use a **lookahead** field.

We could hard code this into the widget, but why not load a configuration when the app starts up?

We can also run this on the server side if we prefer. Or generate partial HTML to create a static site and then do the rest on the client side. Kind of how React used to work.

Now, when we need to change the interface, we donʼt have to create the race condition we usually get. Thatʼs where the back and and front end devs block each other.

We also donʼt have to make changes in three different places. _With the virtual certainty that they will get out of synch and create problems._ Instead, we update the database once and presto! The front end updates itself instantly.

Here is our final simple DDUI schema for Profile:

```json
{
  "type": "Profile",
  "properties": [
    {
      "hidden": true,
      "name": "id",
      "readonly": true,
      "type": "uuid",
      "value": "255c08b2-6606-424b-a339-d3f9ebe50a21"
    },
    {
      "name": "name",
      "required": true,
      "type": "string",
      "value": "Bob Dobbs"
    },
    {
      "label": "Email address",
      "name": "email",
      "required": true,
      "type": "EmailAddress",
      "value": "bob@dobbs.guru",
      "verified": true
    },
    {
      "default": true,
      "name": "displayNameOnProfile",
      "type": "boolean",
      "value": true,
    },
    {
      "default": "gray",
      "name": "favoriteColor",
      "options": [
        {
          "label": "gray",
          "value": "#999"
        },
        {
          "label": "red",
          "value": "#f00"
        },
        {
          "label": "green",
          "value": "#0f0"
        },
        {
          "label": "blue",
          "value": "#00f"
        },
      ],
      "type": "Member",
      "value": "blue"
    }
  ],
  "actions": {
    "create": {
      "method": "PUT",
      "url": "/profiles/{id}"
    },
    "retrieve": {
      "method": "GET",
      "url": "/profiles/{id}"
    },
    "update": {
      "method": "PATCH",
      "url": "/profiles/{id}"
    },
    "delete": {
      "method": "DELETE",
      "url": "/profiles/{id}"
    },
    "list": {
      "method": "GET",
      "url": "/profiles"
    }
  }
}
```

The above JSON, when run through our renderer function, will generate the HTML below. This is an **update form.**

```html
<form
  action="/profiles/255c08b2-6606-424b-a339-d3f9ebe50a21"
  data-method="PATCH"
  id="edit-profile"
  method="POST"
>
  <input type="hidden" name="_charset_">
  <input
    type="hidden"
    name="id"
    value="255c08b2-6606-424b-a339-d3f9ebe50a21"
  >
  <div class="text-field">
    <label for="name">Name</label>
    <input
      id="name"
      name="name"
      required
      type="text"
      value="Bob"
    >
  </div>
  <div class="email-field">
    <label for="email">Email address</label>
    <input
      id="email"
      name="email"
      required
      type="email"
      value="bob@dobbs.com"
    >
  </div>
  <div class="boolean-field">
    <label for="display-name-on-profile">
      <input
        checked
        id="display-name-on-profile"
        name="displayNameOnProfile"
        type="checkbox"
        value="true"
      >
      Display email on profile
    </label>
  </div>
  <fieldset class="member-picker">
    <legend>Favorite color</legend>
    <label for="gray">
      <input
        id="gray"
        name="favoriteColor"
        type="radio"
        value="#999"
      >
      gray
    </label>
    <label for="red">
      <input
        id="red"
         name="favoriteColor"
        type="radio"
        value="#f00"
      >
      red
    </label>
    <label for="green">
      <input
        id="green"
        name="favoriteColor"
        type="radio"
        value="#0f0"
      >
      green
    </label>
    <label for="blue">
      <input
        checked
        id="blue"
        name="favoriteColor"
        type="radio"
        value="#00f"
      >
      blue
    </label>
  </fieldset>
  <div class="button-bar">
    <button type="submit">Save changes</button>
  </div>
</form>
```

<h3 id="whats-next">Whatʼs next?</h3>

Look for more detailed articles on this topic in the future. With plenty of code examples. Even some npm (or JSR) libraries.

But the most exciting part of this is the core of the whole system: the database. We need a database that will permit us to provide infinite detail about our schema. And keep the schema strict. And permit us to update it _on-the-fly_ whenever we want to.

And we know just the right type of database for this purpose.

More soon.