I. Preramble - bahrus/trans-render GitHub Wiki

שhen, in the Course of web development, it becomes necessary to migrate to a new way of building and connecting components together, and to dissolve the tight coupling which has heretofore made this far more difficult than what developers should be entitled to, a decent respect for the excellent, opinionated Web Component Libraries that already exist, impels a lengthy explanation of the Separation of Concern approach we adopt to solve this well, and why this requires the introduction of yet another web component helper library, and so we declaratively describe the Nature of this Separation.

ڜe hold these truths to be self-evident...

...after bumbling around for months and months:

     

All UI Libraries Are Created Equal

The great thing about web components is that little web components built with tagged template literals can connect with little web components built with Elm, and web components will be judged by the content they provide, rather than superficial internal technical library choices.

For example, an interesting debate that has existed for a number of years has been between OOP vs functional programming. Efforts to "embrace the duality paradox" like Scala and F# always appealed to me. In the realm of UI development, this has been an interesting dichotomy to follow. Traditionally, JavaScript was a unique (?) "function first" language, which seemingly inspired some envy / second guessing from the everything is a class class of developers. The introduction of classes into JavaScript has been met with some healthy skepticism. The "hooks" initiative adds an interesting twist to the debate, and might strike the right balance for some types of components. Evidently, the result has been less boilerplate code, which can only be good. Perhaps the learning curve is lower as well, and that's great.

We take the view that classes are a great addition to the JavaScript language, even if they don't solve every issue perfectly. Some points raised by the React team do hit home with me regarding classes.

My personal journey with classes

Speaking personally, I came from an academic (mathematical) background, and functions felt much more natural to me. Yes, I saw the need for namespaced functions, and having the ability to hold data structures with nested sub-structures. But the way people gushed about combining these two things into one entity simply left me scratching my head. The examples I would read were c++ books that would start with Giraffes and Dogs, and then jump into describing how to create a Windows window, and I would get lost about 5 pages in. Visual Basic (originally codenamed "Thunder", maybe because of its emphasis on making it easy to respond to events?), in contrast, simply required an animated gif to explain, and it didn't even use classes originally! I simply didn't see the appeal of classes, until the day I joined an actual software company, and worked with problems centered around database tables, with customers, employees, transactions. Finally, the lightbulb lit in my mind. I can certainly see why a new developer would also question the need to learn the subtleties of classes just to wire a button to a textbox. Add to that the subtleties of "this" and the syntax is a little clunkier (new class()).doFunction()... )

Yes, I did think quite a bit about the question, and playing around a bit, before landing on the current approach that this library uses / encourages.

I think one factor that needs to be considered when weighing the pro's and con's between classes and functions for defining components, is another duality paradox: the "à la carte vs. buffet duality paradox."

Are we:

  1. Creating, with tender loving care, a component meant to have a minimum footprint, while being highly reusable, leverageable in multiple frameworks / no frameworks, server-side-rendered / not-server-side-rendered, loaded synchronously / asynchronously, bundled / not bundled, ShadowDOM / noShadowDOM, etc?
  2. Engaging in RAD-style creation of a local component only to be used in a specific way by one application or one component?

This package will be sure to cater first and foremost to the "built with tender loving care" shops, but will strive not to sacrifice the second goal as much as possible. Judge for yourself, I guess.

So we encourage the use of classes in a way that might avoid some of the pitfalls, while benefitting from the really nice features of classes, namely:

  1. Support for easily tweaking one custom element with another (method overriding).
  2. Taking advantage of the nice way classes can help organize data and functionality together.
     

Looks aren't everything

This package provides a core web component helper, "O", that does not concern itself with rendering content. There are numerous scenarios where we want to build a component and not impose any rendering library performance penalty. They generally fall into one of these four scenarios and counting:

  1. "Web Components as a Service": Providing a timer component or some other non visual functionality.
  2. Providing a wrapper around a third-party client-side library that does its own rendering. Like a charting library.
  3. Providing a wrapper around server-rendered content.
  4. "Web Components as an Organism". Providing a viewModel from raw data that serves as a non-visual "brain" component that handles all the difficult JavaScript logic, and that is all. Other non-visual components transmit updates from the 'brain" component to peripheral visual components, and dispatch updates back up to the "brain" component. All contained within a large single web component (the body). Kind of the same as 1, but with a little more context.
     

The pursuit of happiness...

...is achieved when Web Components can be opened directly as an HTML page.

This package contains the DNA for another package, xtal-element, that enables building web components as HTML files, that can be opened up directly as a web page, often providing complete functionality as is. It can still be embedded in a web stream / page as a standard web component. Demo'ing such a web component couldn't be easier.

     

Server rendered content is entitled to be...

...displayed, free from client-side JavaScript meddling, as long as it best represents what the user wants to view.

This is a tricky one. What is absolutely clear is we want to keep the number of renders low (and changes made during a render to be as minimal as possible).

This package provides a web component base library, Mount, that extends the underlying web component organism, O. Mount focuses on providing first-class support for "upgrading" server-side-rendered or server-side-generated content or click-clackity human-typing-generated content.

Mount breaks down the task of "booting up" into the following steps:

  1. If needed, create ShadowDOM.
  2. If needed, clone the main template.
  3. If needed, attach event handlers to the cloned template. This is done via an optional user-defined "xform" transform.
  4. If needed, before appending the cloned template into the live ShadowDOM tree (or directly in the element if forgoing shadow DOM), perform the first pass at transforming the content, where the props are passed in.
  5. If needed, append the cloned template into the shadowDOM or element itself.
  6. Reactively (re)perform the transform as props change.

Many of the "if needed"'s are there because we insist on supporting server-side rendering, so not all those steps are really needed in that case.

Mount infers what it needs to do as much as possible by looking at the circumstances in which it finds itself (during the constructor) -- whether shadowDOM is already present, for example.

But mount does ask the end user to provide hints via two attributes: "defer-hydration", which is part of the web community protocol, and which mount honors. More significantly, if ShadowDOM is not used, and the initial rendering needs to be done by the client, the developer needs to indicate this by adding attribute "csr".

     

JSON and HTML Modules will land on Planet Earth someday.

This package subscribes to the rule of least power philosophy. It is designed as a natural segue into declarative custom elements. As much logic as possible is made truly declarative with JSON configuration. It even encourages developers to apply a little extra ceremony to demonstrate commitment to true declarative syntax, separating settings that are JSON serializable from those that are not (such as function / class references). While the developer can still use the easier to edit typescript / javascript when configuring web components, the trans-render approach encourages us to utilize JSON imports, and gain from lower parsing times, once the platform finally gets there. Hopefully, sometime this century HTML modules/imports will follow, w3c willing, which could, if w3c shows some HTML (and end user) love, allow us to render as content streams, and also benefit, perhaps from more low-risk / ui-driven development.

     

Just follow your nose.

This package embraces the duality paradox between Functional and OOP by following a pattern we shall refer to as FROOP: Functional reactive object-oriented preening. It does so via a library called roundabout, and alternative approach to signals.

Properties are entirely defined and configured via JSON-serializable configurations. The properties are there on the custom element prototype, but they are created dynamically by the core O library, based on the configurations provided by the developer.

This configuration is extended by the roundabout "FROOP Orchestrator," to provide a kind of "service bus" that can easily integrate lots of tiny, loosely coupled "action methods". Action methods of a class (or mixin) are functions -- methods and/or property arrow functions, which impose one tiny restriction: Such methods should expect that the first (and really only) parameter passed in will be an instance of the class (or custom element) it acts on. In other words, the "inputs" of the method will be already set property changes. The orchestrator allows the developer to pinpoint which action methods to call when properties change. Ideally, the signatures of such ideal action methods would all either look like:

class MyCustomElement extends HTMLElement{
    myActionMethod({myProp1, myProp2}: this){
        ...
        return {
            myProp3,
            myProp4
        } as Partial<this>;
    }

    async myAsyncActionMethod({myProp1, myProp2}: this){
        ...
        return {
            myProp3,
            myProp4
        } as Partial<this>;
    }
}

together with

/**
 * Out of class Action Methods are shared across all instances, so better performance
 * *
 **/

const myOutOfBodyDeclarativeActionMethod = ({self, myProp1, myProp2}: MyCustomElement) => ({
    myProp3,
    myProp4
} as Partial<MyCustomElement>);

/**
 * Some heavy lifting out of body, non declarative action method (which is really a dynamic property of a class instance)
 * that could be dynamically imported and progressively enhance the custom element
 */
const myAsyncOutOfBodyActionMethod = async ({self, myProp1, myProp2}: MyCustomElement) => {
    //lots and lots of heavy code, combined with lots of dependencies...
    return {
        myProp3,
        myProp4
    } as Partial<MyCustomElement>

};

class MyCustomElement extends HTMLElement{
    self = this;
    myInternalActionMethod = ({self, myProp1, myProp2}: this) => {
        self.#myPrivateMethod(); //works!
        return {
            myProp3,
            myProp4
        } as Partial<MyCustomElement>;
    }
    myOutOfBodyDeclarativeActionMethod = myOutOfBodyDeclarativeActionMethod; 
    myAsyncOutOfBodyActionMethod = myAsyncOutOfBodyActionMethod; // this is a property, and could utilize the FROOP orchestrator to activate content progressively
}

What all these action methods have in common, is they don't directly have any side effects. Ideally, they would generally all be self contained "nano-methods". The FROOP orchestrator centralizes the pain and blame for causing side effects.

However, that's just the ideal. As mentioned initially, the only hard rule for action methods is they should be able to take as the first argument an instance of the class (custom element).

Further reading that is useful:

https://javascriptweblog.wordpress.com/2015/11/02/of-classes-and-arrow-functions-a-cautionary-tale/
https://www.charpeni.com/blog/arrow-functions-in-class-properties-might-not-be-as-great-as-we-think
https://web.dev/javascript-this/
     

To withdraw into obscurity is the way of Heaven

The mount web component layer strives to impose as little custom syntax as possible, providing an avenue for extending / replacing the HTML vocabulary as immediately as possible, and to be able to work with server-rendered content "as is" without having to weed out obtrusive formatting instructions.

This way the syntax can evolve, piece by piece, over time, based on usage, with no central authority in charge of it.

Almost all aspects of the rendering can be opt-in, with replacing syntax allowed, and significantly, because each aspect is downloaded within an overridable method only if specified, overriding the method and establishing alternative syntax has no penalty imposed by the original syntax, as it won't be downloaded.

The core rendering logic is also kept quite minimal. The core transform syntax trans-render relies on, trans-rendering, does its job well, but is limited to providing property value distributing, and adding event listeners, and common use case conditional logic. But it stays true to our definition of declarative -- where no side effects can be introduced, and can execute anywhere html can execute, just like web components.

It does provide minimal support for loops, and conditional logic, like with most other UI libraries provide.

But the clear preference is for developers to pick and choose from a potentially infinite variety of custom attribute element decorators/attributes/enhancements that provide such features, some specializing in certain scenarios, others focused on other use cases.

<= Home => Signals vs Roundabouts

⚠️ **GitHub.com Fallback** ⚠️