Preramble
When, 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 xtal-element assumes 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.
We hold these truths to be self-evident, after bumbling around for months and months:
1. 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.
xtal-element takes 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:
- 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?
- Engaging in RAD-style creation of a local component only to be used in a specific way by one application or one component?
xtal-element is a bit more biased towards the former, but strives not to sacrifice the second goal as much as possible. Judge for yourself, I guess.
So xtal-element encourages use of classes in a way that might avoid some of the pitfalls, while benefitting from the really nice features of classes, namely:
- Support for easily tweaking one custom element with another (method overriding).
- Taking advantage of the nice way classes can help organize data and functionality together.
2. Looks aren't everything
The core functionality of xtal-element is not centered around 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 three scenarios:
- Providing a timer component or some other non visual functionality. "Component as a service".
- Providing a wrapper around a third-party client-side library that does its own rendering. Like a charting library.
- Providing a wrapper around server-rendered content.
3. The pursuit of happiness is achieved when Web Components can be opened directly as an HTML page.
Web components built with xtal-element provide an HTML output option that allows the web component to provide its own demo directly by opening the HTML file in a browser. 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.
4. Content coming from the server 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).
As mentioned earlier, the core functionality of a xtal-element doesn't address rendering. However, there are some core mixins xtal-element provides, that do provide rendering capabilities.
The functionality those mixins provide can be broken down into the following steps:
- If needed, create ShadowDOM.
- If needed, clone the main template.
- If needed, attach event handlers to the cloned template. This is done via an optional user-defined "initTransform".
- If needed, before appending the cloned template into the live ShadowDOM tree (or directly in the element if forgoing shadow DOM), perform the first "updateTransform" where the props are passed in.
- If needed, append the cloned template into the shadowDOM or element itself.
- Reactively (re)perform the updateTransform as props change.
Many of the "if needed"'s are there because xtal-element supports server-side rendering, so not all those steps are really needed in that case.
xtal-element is fully committed to providing support for server-side rendering. It specifically targets SSR that is based on optionally weaving dynamic data into static html files via Cloudflare's HTMLRewrite API, but is also compatible with client-side rendering using DOM API's. So this raises a number of scenarios an xtal element needs to consider.
Some of the scenarios listed below can happen in combination, some are mutually exclusive. It would make for a complex Venn diagram:
- Minimal server-side rendering. Server only creates an instance of the tag, and sets some attributes, and the light children.
- Limited Shadow DOM server-side rendering, limited to pasting in the Shadow DOM defined in the static html file, without any attempt to do any of the binding defined within, of which there are some beyond slot mapping.
- Limited Shadow DOM server-side rendering, but the Shadow DOM requires no dynamic adjustments.
- A full-blown server-side rendering solution of only one initial instance, complete with applying the binding instructions.
- A full-blown server-side rendering solution of all instances of the component.
- The full state needed for rendering is provided as a combination of JSON-serialized attributes and light children.
- Less than the full state is defined within the geographical boundaries of the element. Instead, some separate elements (sibling or parent) are used to integrate part of the state, including non-JSON serializable settings.
Only scenarios 3, 4 (first instance) and 5 do not require a first pass update render on the client-side. We need a way for the server to indicate this clearly to the client side instance.
Scenario 7 makes things complicated, as it becomes difficult to know when to do the first update render. The safe thing would be rerender each time pieces of the state are passed in. But that isn't optimal. This is the use-case that is central to the defer-hydration proposal (I think).
xtal-element creates a clear division between main template cloning, initial rendering, which involves adding event handlers, pulling in templates, vs update handling, reacting to prop changes.
IndicationsScenario | Server-side attributes | Actions performed |
---|
No server-side rendering, no planning-ahead defer-hydration hints | None | Do main template cloning, Do Init Render, Update Render |
Server-side rendering, copy-paste, no binding | defer-hydration=['' if 1 external setter, number of external setters if > 1, no attribute if none] | Only do Init Render, update transform after defer-hydration attribute removed. |
Server-side rendering, copy-paste, with binding | defer-hydration=[Same as above], defer-rendering | Only do Init Render after defer-hydration attribute removed, skip update transform but remove defer-rendering the first time. |
5. JSON and HTML Modules will land on Planet Earth someday
xtal-element 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. 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 xtal-element approach encourages us to utilize JSON imports, and gain from lower parsing times, and HTML modules/imports, w3c willing, which allows us to render as content streams, and also benefit, perhaps from more low-risk / ui-driven development.
6. This is FROOP
xtal-element embraces the duality paradox between Functional and OOP by following a pattern we shall refer to as FROOP: Functional reactive object-oriented preening.
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 trans-render / xtal-element library from the configurations provided by the developer.
Should decorators ever reach stage 3, they will also be supported.
This configuration is extended by trans-render's/xtal-element's "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
const myOutOfBodyDeclarativeActionMethod = ({self, myProp1, myProp2}: MyCustomElement) => ({
myProp3,
myProp4
} as Partial<MyCustomElement>);
const myAsyncOutOfBodyActionMethod = async ({self, myProp1, myProp2}: MyCustomElement) => {
return {
myProp3,
myProp4
} as Partial<MyCustomElement>
};
class MyCustomElement extends HTMLElement{
self = this;
myInternalActionMethod = ({self, myProp1, myProp2}: this) => {
self.#myPrivateMethod();
return {
myProp3,
myProp4
} as Partial<MyCustomElement>;
}
myOutOfBodyDeclarativeActionMethod = myOutOfBodyDeclarativeActionMethod;
myAsyncOutOfBodyActionMethod = myAsyncOutOfBodyActionMethod;
}
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/
7. To withdraw into obscurity is the way of Heaven
xtal-element strives to impose as little custom syntax as possible, providing an avenue for extending / replacing the HTML vocabulary as immediately as possible.
This way the syntax can evolve, piece by piece, over time, based on usage, with no central authority in charge of it.
The core transform syntax xtal-element relies on, DTR, does its job well, but is limited to providing property value distributing, and adding event listeners.
It stops there, and doesn't even provide support for conditional or looping, which most other UI libraries provide.
Instead, developers can pick and choose from a potentially infinite variety of custom attribute element decorators that provide such features, some specializing in certain scenarios, others focused on other use cases.
For non visual components, it makes sense that the definition for the component should be JS-first.
Let's take a look at the xtal-element way to define a "component as a service" such as this timer component.