JSideris / DOThtml

A human-friendly way to build highly-dynamic web pages in pure JavaScript.
2 stars 4 forks source link

Discussion on callbacks, namespaces, and parameter lists. #175

Open JSideris opened 5 months ago

JSideris commented 5 months ago

The decision on how to organize namespaces is a challenging one. Let's review where we came from, and what options we have.

History

The original alpha prototype DOThml v1 - which was more of a simple element-building library than a framework - used o really rudimentary syntax of callback functions.

// Generates <div><span>Nested Element!</span></div>
dot.div(function(){
    return dot.span("Nested Element!)
});

This was a good proof of concept, but really cumbersome to work with. One of the features it provided was the ability to build the DOM starting from root elements. So in this case, the <div> element can be created first and added to the DOM, then the <span> element can be created and added to the <div>.

I believed I could do better, so before my initial release of DOThtml, I created the goal of making the syntax simpler by removing the need for callbacks.

// Generates <div><span>Nested Element!</span></div>
dot.div(
    dot.span("Nested Element!)
);

This was the goal but in cerated some unique challenges that required rewriting a lot of stuff. Now, the <span> element actually gets created before the <div>. So elements that are created can't simply be added to the DOM. Instead, this works by creating a placeholder element that will contain the nested elements temporarily. This placeholder then gets passed into the parent function and when it's time to render the parent, the children of the placeholder are transferred into the parent. Obviously later versions of DOThtml are much more sophisticated than this (relying on virtual models of the DOM rather than placeholders), but this got the job done and provided us with a very compact syntax.

A few versions later in v4, we got our first integrated style builder, which was previously its own library: DOTcss. The style builder avoided using callbacks, but you had to use the dot.css object to access CSS-building functions. This prevented the dot namespace from getting cluttered with hundreds of CSS methods. That looked something like this:

// Sets the style property of the <span> to "font-size: 22px; color: rgb(0,0,255);"
dot.span("Nested Element!).style(
    dot.css.fontSize(22).color("blue")
)

At the time, there were a number of helper functions built in and the framework was fairly smart about how it interpreted values. 22 here was understood to mean pixels, which was the default unit for lengths. Passing 3 parameters into the color function would be interpreted as RGB values. Etc. But complex properties like filter and transform were still illusive, and could only accept string values.

In v5 (which was also when the framework was ported to TypeScript), more functionality was added to address that. Here's an example:

// Adds a CSS translation of +100px to the span's x position.
dot.span("Nested Element!).style(
    dot.css.transform(t=>t.translateX(100))
)

And that's what's led us to where we are now.

Challenges

One of the things that's really important to me is that if thousands of people are one day using DOThtml, I don't want to pull the rug out from under their feet and make dramatic changes to the interface. I'm fine suffering through that for myself, but not inflicting it onto others. That's one of the reasons it's taken me years of using DOThtml in my own projects before feeling comfortable encouraging others to use it. It needs to be perfect, and future proof, and I'm very meticulous about what that means.

There are a number of aspects of HTML, CSS, and even SVG building that require decisions on whether we'll be using callbacks (like transform builders), encapsulated objects (like dot.css), cluttering the global namespace (like we do with HTML attributes), or some other solution. Striking the right balance of high consistency, high functionality, and low salt is not easy.

Let's start by listing out the specific affected areas where decisions have either already been been made, or need to be made.

HTML Builder

The HTML builder is the dot object that contains a list of HTML functions in it. For nesting, you just pass that object into another element builder and start calling functions on it. I'm not willing to budge on this and go back to callbacks.

Attributes

Attributes are in the dot namespace along with elements. This admittedly feels weird to do - not because of namespace clutter, but because it feels inconsistent. Intuitively, attributes should really be children of elements, not siblings. To demonstrate, let's examine an example. Suppose we wanted to create the following HTML:

<div id="div-1">My first div.</div>
<div id="div-1">My second div.</div>

Right now in DOThtml, this looks like this:

dot
    .div("My first div.").id("div-2")
    .div("My second div.").id("div-2")

If we were to create a new XML-based alternative to HTML that was analogous to the above, it might look like this:

<div>My first div.</div><id>div-1</id>
<div>My second div.</div><id>div-2</id>

Something just feels off about that. But what alternative do we have in DOThtml's Dot Syntax?

Alternative 1: Moving attributes into elements.

This would look like this:

dot
    .div(dot.id("div-1").text("My first div."))
    .div(dot.id("div-2").text("My second div."))

But now the problem is that the attributes are siblings of the target's children, which is also kind of weird. Another problem is that we will lose the ability for the attribute builder to show attributes related to the tag. For example, <a> elements have an hRef function that doesn't appear on most other tags. Since here the attributes are being read from the dot object without any further context, we lose that feature.

Alternative 2: Attribute builder as a second argument.

Another option would be to accept a second parameter that provides an attribute builder.

dot
    .div("My first div.", d=>d.id("div-1"))
    .div("My second div.", d=>d.id("div-2"))

Whether or not to use a callback or a namespace (like dot.attributes) might be one for consideration. If we use a callback, the parameter can by typed to the correct tag. We could even make the first parameter optional. Further, this approach could be useful for component mounting, which we'll discuss next.

I have a couple concerns about this. One is that it obviously adds a bit of syntactic salt, since now we have to use these callback functions. It also mixes the encapsulated Dot Syntax with the callback syntax. So it feels a little mixy and matchy. Also, admittedly, I have a massive sunk cost bias since I've probably been using the combined namespace for dozens of projects. It's served me well so far. Switching will be immensely costly to me personally. I'm not saying it won't happen. I'm just saying it's going to be painful (for me). I'll do a follow-up blog post with my decision before the release of v6.

Component Mounting

// TODO

SVG Builder

// TODO

CSS Builder

// TODO

Complex CSS Properties

// TODO

Recursive CSS Properties

// TODO

Conclusion

// TODO