Here we will create two simple but powerful JavaScript functions.
The first, jsToDom, will take a JavaScript (or JSON) object and turn it into a
DOM
object that we can insert into our page. The second, domToJs,
does the reverse. It takes a DOM object and converts it to
JS.
Now we can stringify it and persist it in our database. And rehydrate it
at will.
As you may know, JSON does not recognize functions. So we will need a way
to deal with our event listeners. Donʼt worry! Weʼve got it
covered.
This means that we donʼt rush to load up dozens of frameworks,
libraries, and other dependencies. Instead, we start with nothing:
zero dependencies.
We build the site structure with semantically-correct and accessible
HTML. Then we add just enough CSS to make it attractive, user-friendly, and responsive.
Finally, we add JavaScript to progressively enhance the user experience. But only if we need it.
This vanilla approach works very well. One goal of the Craft Code effort
is to see how far we can go before we have to add a dependency on
someone elseʼs code.
The code
For the impatient, letʼs take a look at the final code. Then weʼll explain it. The next two functions are the totality of the module.
The rest are specific to this example.
Note: this is about concepts, not production-ready code. This is a
first pass and might could use some refactoring. YMMV.
We grab the tagName from the JSON and create a DOM element of
that type. Then we add the attributes to that element. Then we apply jsToDom recursively on the children, appending them to the
element.
The events object is pretty clever, IOHO. The keys are the names of the events (e.g., click) and
the values are the names of the handler functions. We will import those
functions only when needed. See an example below.
We also create a string representation of the events object. We could
have simply stringified it, but we wanted to make it human readable, so
we wrote our own (lines #39 to #52).
This is a bit tricky as nothing in the DOM is simple. Go figure.
We explain below.
This extracts the attributes, the childNodes,
and tagName from the passed DOM element. Then we use these to
create a simple JS/JSON object, recursing through the child nodes.
We pull the data-events attribute out and treat it separately.
We parse the value back into an actual object and add it at the events key.
The output is JS, but we can stringify it to JSON as required. The JSON
shown below is a typical example. It creates our test form.
We import this JSON and pass it to jsToDom in our
index.js file below.
Pretty self-explanatory. Now, how do we handle our events?
Simple. We take our events object from the passed JSON/JS. Then
we loop through the keys, which are the event types. The values are the names of the handler functions.
We add an event listener for each type and assign it the default
function from the module with that name. For example, our output div gets a click handler called “log”. This
function is in ./modules/log.js.
We import the handler: (await import("./log.js")).default.
We assign it to handler.
Then we add it like this:
addEventListener("click", handler).
Drop dead simple. And we only import the modules that we need. See the
actual log handler below.
Kinda dumb, but it is merely an example. We add this log function
as a click handler on our output strong element
and as a focusin handler on our form.
The submit handler for the form is a bit more exciting:
We canʼt store functions in JSON, so we put them into modules. Then
we will import them as needed when we rehydrate the DOM elements.
We attach parseSubmission as the submit handler
for our form element.
What can we do with this?
Ooo. All sorts of cool things.
Easy element creation
Instead of messing around with createElement, setAttribute, etc., we can use jsToDom. We pass it a JS or JSON object
representing the DOM elements we want.
We create handler functions ahead of time in modules. When we need an
event listener, jsToDom imports it just in time and assigns
it to the element.
This works like Reactʼs createElement function. Or a library
such as hyperscript. Sure, weʼd prefer JSX for its much reduced cognitive load. But our alternative here is the DOM methods such as createElement. Unless we want to load up
a bulky library such as React, that is.
We donʼt.
Suppose I wanted to inject a password field with a show/hide button.
First, we create a toggle handler such as this:
Now I can call jsToDom with the following JSON and it will create
my password input. Try pasting it into the example form. Remember that the click event handler is already available at
./modules/toggle-visibility.js.
We hope that it is straightforward how all this works.
Easy element persistance
What if I want to save current UI state? We can use domToJs
to do just that.
We took the example page (linked above) and passed the html
element to domToJs. Then we stringified it. Now we have
preserved both the head and body elements.
So we can take a blank HTML document like this:
And we can use our persisted JSON head and body elements to create the page on the fly. We can store the JSON in a database
or load it from an API.
Below is the code minus the JSON. Or view the actual code.
Then see it in action. View the source on that page to see what we mean.
A whole page generated from simple JSON!
Data-driven user interfaces
One idea that we have been promoting for several years now is that of a
data-driven interface. Weʼve got an article in the pipeline on that
coming soon. But we can give a quick overview here.
The idea is quite simple. The easiest example to visualize is automated
form rendering.
Forms collect data. Typically, that data is then persisted
in a database.
Databases have schemas. That means that the database already
knows the types it expects. If we have defined our schema well, then it knows
those types precisely.
Particular data use particular interface widgets. For example,
an email address would use an input element of type
“email”. An integer might use an input
element of type
“number” with its step attribute set to “1”.
An enum might use a select or radio buttons or
a checkbox group.
From this schema we should be able to determine how to both display and validate that data. After all, there is only a small number of widgets.
So what if the response to our database query for the data included the
schema? Some GraphQL queries already make this possible. From that
schema, we can generate validation functions. And we know which
widgets to display.
So we can generate our form and our validators automatically.
Best of all, we have a single source of truth: our database schema.
In a coming article, we will explain how easy it is to achieve this.
(There is an advanced version of this that uses a
SHACL ontology and a triple store
such as Fuseki. We then use
SPARQL queries to generate the HTML, CSS, and JS straight from the database. Wee
ha! Weʼll get to that sometime soon, too. Promise.)
If anyone requests it, weʼll give a detailed explanation of the
above code in a separate article. This one is long enough.
Links to related pages
Get notified form
Carbon emissions for this page
Cleaner than 98% of pages tested
0.019g →
on first visit; then on return visits
0.012g
Beacon: A+
QR Code
Scan this code to open this page on another device.