I've been writing web backends and frontends since the 90s. CGI, ISAPI, AJAX - you name it, there's not a TLA I've not used to write production quality, real-time, dynamic services presented in browsers.
CGI works, but delivering a whole page or frame was a lot of work for servers and session managers and lacked the interactivity users expect. React brought a consistent definition for modular, re-usable web elements, but once you've gone beyond simple components, the sheer size and complexity of its implementation makes it a bit painful. This has been addressed by libraries like raw.js, but they fundamentally don't manage the UI interactivity tree, they merely provide alternative ways create markup.
Whether front-end (React & similar frameworks) or backend (CGI, templating systems), the basic requirement is always to declare some HTML with dynamic substitutions:
<html>
<body>
<h1>Hello ${name}!</h1>
<div>Here is the news...</div>
${newsItems}
</body>
</html>
It doesn't matter whether the server fills in the substitutions on the fly Γ la CGI, or the client does it via a virtual DOM, the goal is to layout the content, allowing for some magical process to update it automatically.
Modern browsers provide some key technologies to do this natively without the need for a build process so complex and a dependency tree so dense you'd be lucky to ever escape the forest to the sunlit uplands of actual project delivery!
AI-UI "Async Iterator UI" does this in a tiny, focused client-side module, with no dependancies, using native JavaScript constructs.
The problem with templates
The essential problem with most templating tools is that the underlying variables are themselves fixed values at the time the template is processed. The tool or framework has to somehow re-read or track changes to the ${variables}
and ${expressions}
within your layout (or, worse still, you have to use some arcane syntax or function to set and get "state"), and then work out what that means for the final markup.
In AI-UI, your variables and expressions can themselves be "live" - they can update themselves, directly updating the DOM with no diffing, naturally implementing minimal updates.
You can think of them like cell references in spreadsheet: you update one cell and all the others recalculate and redraw themselves.
This is achieved by simply allowing the substitutions in AI-UI layout to be JavaScript async iterators or promises (of course, they can just be normal static expressions too).
Iterators, events, components
To make it as easy to use as possible, AI-UI provides some key features:
- a library of key functions for handling async iterators (
map
,filter
,consume
,merge
,combine
...) - presenting DOM events as async iterators, so you can quickly and easily link DOM elements together, without having to manage a whole tangle of event-listeners and their removal when the DOM is modified
- encapsulating DOM elements together in a type-safe construct, so consumers of your components know exactly what attributes can be set to control your components
- allowing you to define new DOM element properties as
iterable
, so you can easily read, write and subscribe to changes with basic JavaScript syntax likeweatherChart.location = "London";
- a handy Chrome Devtools extension for exploring your DOM components and their hierarchy, and helpful logging during develiopment.
All elements created by AI-UI are standard DOM elements, and support the full, standard DOM API so you can integrate them with any existing web site or framework.
Specifying your layout
Because AI-UI is a JavaScript module, you specify the layout as a series of function calls. However, it also fully supports JSX and htm, so you can use a more familiar markup at the cost of the loss of some type safety. There's more about these choices in the AI-UI guide here.
In this, article, I'll use the functional notation as it's the most type-safe. It really simple: a DOM element is created by calling the function named after the element:
// AI-UI uses normal functions to create fully typed elements
const elt = div("Hello ",
span({style: 'color: green'},
" there ", name
)
);
// elt is derived from HTMLDivElement
...which generates exactly the same result as:
// HTM uses tagged template strings to create one or more Nodes
const elt = html`
<div>Hello
<span style="color: green">
there ${name}
</span>
</div>`;
// elt is a Node or Node[]
// JSX uses a transpiler to change the markup into function calls that return an unknown type
const elt =
<div>Hello
<span style="color: green">
there {name}
</span>
</div>;
// elt is any
The element creation functions all take an optional object specifying any attributes, and zero or more child nodes, which can be strings, numbers, DOM Nodes, collections of these or promises or async iterators for any of the above.
The magic here is that name
, can be not only a value like "Mat"
or 123
, but also an async iterator that generates these values. When the async iterator generates a new value, AI-UI will update the DOM to reflect the change directly.
Tick tock!
Here is the complete code for a simple clock:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- There are also CommonJS and ESM builds -->
<script src="https://unpkg.com/@matatbread/ai-ui/dist/ai-ui.js"></script>
</head>
<body>
</body>
<script>
/* Specify what base tags you reference in your UI */
const { h2, div } = AIUI.tag();
/* Define a _new_ tag type, called `App`, based on the standard "<div>" tag,
that is composed of an h2 and div elements. It will generate markup like:
<div>
<h2>Hello World</h2>
<div>{some content goes here}</div>
</div>
*/
const App = div.extended({
constructed() {
// When constructed, this "div" tag contains some other tags
return [
// h2(...) is "tag function". It generates an "h2" DOM element with the specified children
h2("Hello World"),
// div(...) is also "tag function". It generates an "div" DOM element with the specified children
div(clock())
]
}
});
/* Add add it to the document so the user can see it! */
document.body.appendChild(
// App(...) is also a "tag function", just like div()
// and h2(), created by extended() which generates a "div"
// containing the DOM tree returned by constructed().
App({
style:{
color: 'blue'
}
},
'Tick Tock')
);
/* A simple async "sleep" function */
function sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
}
/* The async generator that yields the time once a second */
async function *clock() {
while (true) {
yield new Date().toString();
await sleep(1);
}
}
</script>
You can specify attributes, content, children - in fact anything you can insert into a DOM - using native JavaScript async iterators, generators or promises.
Where next?
The above example just demonstrates the first step unleashed by the power of async iterators in UI.
The AI-UI when(...)
function creates iterators from other DOM elements, so you can make one element control another, and the iterable
member of an extended
component allows you to implement "hot" properties in your own components, which can be used to update your components with a simple assignment.
There's a guide on GitHub together with some examples, like this weather chart (open dev tools to see how it works).
The underlying concepts in AI-UI have been used in projects for over a decade, both public and private, with hundreds of thousands of users. After 10 years of refinement, the time is right to share it.
Given AI-UI is new to open source, it'd be great to get your feedback and I'm actively looking for collaborators to help hone the API and set the direction for future developments.
I look forward to seeing your comments or questions here, or on the Github repo.
Top comments (2)
Just for fun, here's the classic Sheep game written using AI-UI!
Github Source
Live demo
Enjoy!
More about iterable properties in my new article here