Bring Your Own DOM – Part 2 – TUIs

Mise en place for making pasta, from catceeq.

In part 1 we talked about some basic setup for building portals. That was cool and useful and … there’s not much to do with it. What if we want to go further? What if we wanted to write Elm to build textual user interfaces (TUIs), something like Ink or Bubble Tea.

What is a DOM?

Most people would say “That’s impossible! Elm is only for web apps.” Well, that’s kind of true. Elm is designed for writing to a DOM, and browsers use a DOM. Let’s look at MDN’s definition

The DOM represents a document with a logical tree. Each branch of the tree ends in a node, and each node contains objects. DOM methods allow programmatic access to the tree. With them, you can change the document’s structure, style, or content.

Nodes can also have event handlers attached to them. Once an event is triggered, the event handlers get executed.

https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model

I’ve chosen to add emphasis to the key points here: the DOM is a tree, which we can change, and add event listeners too. There’s nothing here that says it can only exist in a browser, and back in part 1 we figured out a way to setup a faux node that defines the 7 or so DOM methods that Elm uses. Could we write other kinds of faux nodes?

The ingredients of our DOM

For those that like to jump ahead, a link to the repo I’ve been playing around in.

For those that like to be guided, lets begin. For our DOM we’ll need a few ingredients. First our portal web component from part 1 with all of it’s basics like childNodes and setAttribute. Next we need a “document”. Finally we need some helpers to tie it all together.

Starting with the simplest ingredient first, the “document”. Elm expects the context its created in to have access to context.document. This document needs 2 functions and 2 properties. The first function is createTextNode, which does as it says by creating a new text node in its tree. The second is createElement, which creates a non-text node in its tree. The properties are then .body which returns the root or “body” node of the tree and .title which is used for getting & setting the title of your document.

Now that we have a document, we need to work with the nodes, aka elements, aka the tree. This is where that portal comes in. We can largely copy the skeleton of it and then update the body of each function to match our new goal. Before we were forwarding messages to another browser DOM node, but now we’re responsible for creating, updating, viewing, and destroying our own nodes!

Gluing it all together

“But wait!” you say, “How can we glue it all together if we don’t have nodes yet?” We’ll get to that, saving the best hardest part for last. The easiest way of gluing it all together is to define our own Elm Program. In my package this is done with Ink.program, which looks strikingly similar to Browser.document only instead of body : List (Html msg) it expects body : Ink msg. Doing this allows us to control what view code we allow to be written and what it means in the context of an Ink app.

So what then is Ink msg and where do I buy one? Well similar to Html.text and Html.div I provided Ink.text and Ink.column. Instead of div : List (Html.Attribute msg) -> List (Html msg) -> Html msg you get column : List (Ink.Style msg) -> List (Ink msg) -> Ink msg. Nearly identical, only scoped to our new context.

Finally this is all tied together with Html.node "elm-ink-node-name". Which leaves us with one final question.

What is a Node!?

The basics of a node can be straight forward. A text node only needs to print a string to the terminal which could be process.stdout.write("your text"). They don’t have children, or styles, or events so we’re done!

That’s boring though. We want color! We want interaction! We want layout!

This is where things get complicated, and where I’m still struggling to build something of high enough quality that I feel like publishing this as a package. It’s why there aren’t docs. It’s why I’ve taken so long to even write this blog post.

I have an example in the repo of wrapping the Node package blessed, which is a JS package for building TUIs, but it never felt quite right. Layout felt wrong and the docs were difficult to work through. I have another example using Facebook’s Yoga. This works well for layout, if you can manage to stumble your way through the (lack of) docs, but it makes events more complicated. It’s also difficult because it doesn’t account for borders in a way that’s easy to work with.

Where to go from here?

I do plan on continuing this work and I think it’ll be useful. It’s the reason I wrote wolfadex/elm-ansi, and I think the community could benefit from this work. Whether you’re writing a script using elm-pages or if we wanted to move tooling like elm-review’s CLI or Elm Land’s CLI to Elm, this would let us do so with ease. If others are interested, I’m open to PRs and other input. I also hope that someone out there will feel inspired to write their own DOM for another platform. Maybe next year we’ll get elm-mobile or elm-iot or elm-arduino!

I also realize that this may be a little open-ended. If you have questions please feel free to reach our on Slack, Discord, Mastodon, or wherever you can find me as wolfadex and I’ll do what I can do answer your questions.

Leave a Comment