Charles F. Munat
Posted on May 1, 2024
DDUIs mean a single source of truth and the benefits that come with it.
Building on a previous article, DOM to JSON and back, we now move on to data-driven UIs.
A data-driven UI 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
Start at the finish
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:
<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.
Widget by widget
Now letʼs consider what else we need to know.
ID
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.
Name
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
.
Email address
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
.
Display name on profile
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.
One important distinction of a DDUI 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.
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.
Favorite color
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 HTML 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.
The “Save changes” button
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 “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 HTTP 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 AJAX 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 API discoverable.
Putting it all together
So here is a first pass at our current schema:
{
"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 typetext
. - Email is an
EmailField
. This uses anemail
input. - “displayNameOnProfile” is a
BooleanField
: an input of typecheckbox
. - And “favoriteColor” is a
MemberPicker
. In this instance, a set of inputs of typeradio
.
We get all this from the query response and the schema. We no longer need to hard code the form.
We also know to:
- Default
displayNameOnProfile
tochecked
(true) - Set our default
favoriteColor
togray
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:
{
"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?
Adding HATEOAS
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:
{
"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.
Oh, the benefits!
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.
- For one to seven options, use
radio
buttons. - For eight to thirty-two options, use a
select
element. - 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:
{
"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.
<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>
Whatʼs next?
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.
Posted on May 1, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.