PostHole
Compose Login
You are browsing eu.zone1 in read-only mode. Log in to participate.
rss-bridge 2025-05-02T00:00:00+00:00

Functional HTML

Tags on both sides.


Functional HTML

May 2, 2025

Pay what you like

Here’s a piece of HTML:

<html>
<body>
<p>Hello, world</p>
</body>
</html>

Imagine this was the only piece of HTML you’ve ever seen in your life. If you had complete freedom, which features would you add to HTML, and in what order?

Where would you start?


Server Tags

Personally, I’d like to start by adding a way to define my own HTML tags.

It doesn’t need to be complicated. We can just use JavaScript functions:

<html>
<body>
<Greeting />
</body>
</html>

function Greeting() {
return <p>Hello, world</p>

To make this work, let’s specify that when the HTML is sent over the network—that is, serialized—the server must replace such tags with whatever they return:

<html>
<body>
<p>Hello, world</p>
</body>
</html>

When there are no tag functions left to call, the HTML is ready to be sent.

Neat feature, huh?

Good thing we got it in early.

It might influence how we approach everything else.


Attributes

Let’s support passing attributes to tags and interpolating values into the markup.

<html>
<body>
<Greeting name="Alice" />
<Greeting name="Bob" />
</body>
</html>

function Greeting({ name }) {
return <p>Hello, {name}</p>

Of course, there’s no reason why those arguments have to be strings.

It might be nice to pass an object to Greeting instead:

<html>
<body>
<Greeting person={{ name: 'Alice', favoriteColor: 'purple' }} />
<Greeting person={{ name: 'Bob', favoriteColor: 'pink' }} />
</body>
</html>

function Greeting({ person }) {
return (
<p style={{ color: person.favoriteColor }}>
Hello, {person.name}
</p>

Objects let us group related stuff together.

According to our specification so far, serializing the HTML above would produce:

<html>
<body>
<p style={{ color: 'purple' }}>Hello, Alice</p>
<p style={{ color: 'pink' }}>Hello, Bob</p>
</body>
</html>

Still, we haven’t fully gotten rid of the objects.

Is sending those { color: '...' } objects okay?

What should we do with the objects?


Objects

The “real” HTML we know and love has no first-class notion of objects. If we wanted to output some “real” HTML, we’d have to format style as a string:

<html>
<body>
<p style="color: purple">Hello, Alice</p>
<p style="color: pink">Hello, Bob</p>
</body>
</html>

But if we’re reimagining HTML, we don’t have to abide by the same limitations. In fact, let’s specify that a server must serialize our imaginary HTML into a JSON tree:

["html", {
children: ["body", {
children: [
["p", {
children: "Hello, Alice",
style: { color: "purple" }
}],
["p", {
children: "Hello, Bob",
style: { color: "pink" }

Wait, what?

This strange JSON representation isn’t particularly interesting or useful yet. But going forward, we’ll consider this representation to be our primary output format (rather than the “real” HTML). This format is interesting to us because it is strictly richer and more expressive than the “real” HTML. It can express HTML and objects. This lets us keep the style objects—or any objects we might want to send!—intact.

Re-read it and convince yourself that we can easily turn it into “real” HTML later.

If we want to.


Async Server Tags

We’ve previously hardcoded some objects into our HTML:

<html>
<body>
<Greeting person={{ name: 'Alice', favoriteColor: 'purple' }} />
<Greeting person={{ name: 'Bob', favoriteColor: 'pink' }} />
</body>
</html>

function Greeting({ person }) {
// ...

But we could grab them from somewhere else.

Let’s read the data from the filesystem:

<html>
<body>
<Greeting person={JSON.parse(await readFile('./alice123.json', 'utf8'))} />
<Greeting person={JSON.parse(await readFile('./bob456.json', 'utf8'))} />
</body>
</html>

function Greeting({ person }) {
// ...

Note the await in there. Reading data tends to be asynchronous!

Actually, this looks a bit repetitive—let’s move that await into the Greeting:

<html>
<body>
<Greeting username="alice123" />
<Greeting username="bob456" />
</body>
</html>

async function Greeting({ username }) {
const filename = `./${username.replace(/\W/g, '')}.json`;
const person = JSON.parse(await readFile(filename, 'utf8'));
return (
<p style={{ color: person.favoriteColor }}>
Hello, {person.name}
</p>

We’ll have to slightly amend our specification. Now, whenever a server serializes our imaginary HTML, it’ll have to await any async function tags it encounters.

The end result is still the same JSON:

["html", {
children: ["body", {
children: [
["p", {
children: "Hello, Alice",
style: { color: "purple" }
}],
["p", {
children: "Hello, Bob",
style: { color: "pink" }

And we can still then turn that JSON into “real” HTML if we want to:

<html>
<body>
<p style="color: purple">Hello, Alice</p>
<p style="color: pink">Hello, Bob</p>
</body>
</html>

Still, notice how our “imaginary HTML” allows us to speak the user’s language:

<html>
<body>
<Greeting username="alice123" />
<Greeting username="bob456" />
</body>
</html>

async function Greeting({ username }) {
// ...

It’s a greeting for a specific username—not just a “paragraph”.

Where it loads its data or what it outputs is Greeting’s implementation detail.

Cool beans!


Events

What do we do when we encounter a function tag?

<html>
<body>
<Greeting />
</body>
</html>

function Greeting() {
return <p>Hello, world</p>

We call that function and substitute the tag with the output of the function.

<html>
<body>
<p>Hello, world</p>
</body>
</html>

This lets us get rid of all the functions while sending our HTML so that we don’t have to think about how to transfer them over the network.

But what if we don’t want a function to run now?

What if we want it to run later?

For example, on click?

<button>
Like
</button>

We’d need to somehow pass a function over the network.

We could pass a piece of code as a string:

<button onClick="addLike()">
Like
</button>

But that’s not very maintainable, is it?

Suppose we want to write onLike as a proper function:

<button onClick={onLike}>
Like
</button>

function onLike() {
addLike();

However, without the string quotes around the code it’s getting ambiguous. Does onLike execute in the same environment as our HTML—that is, on the server? Can I call writeFile there? Or does it execute in the browser? Can I call alert?

This ambiguity is also reflected in the fact that JSON.stringify will omit it:

["button", {
children: "Like"

By default, JSON.stringify doesn’t know what to do with functions. (In fact, let’s amend our specification to throw an error if our server encounters a function in any position other than a tag. That will force us to make an explicit choice.)

So what do we want it to do?


Client References

Suppose we want to send the onClick code to the client as a <script> tag.

For that to work, we’d need to know which <script> tag to send and which function inside of that file is the click handler. We could encode it like this:

["button", {
children: "Like",
onClick: "/src/bundle.js#onLike"

Let’s call '/src/bundle.js#onLike' a Client Reference—a way to refer to a piece of client code from the server; a client function’s “address” that uniquely locates it.

If browsers could understand this format directly, they would simply load the corresponding <script> tag and attach the onClick handler. But even if they don’t, the JSON above has enough information to be turned into “real” HTML:

<button id="btn">Like</button>
<script src="/src/bundle.js"></script>
<script>btn.onclick = onLike;</script>

How would we want to write this code though?

[...]


Original source

Reply