🔥 Heresy: mappedAttributes, fragment, and define-less components
I’ve recently added tiny features to heresy, that unlock a lot of useful patterns with both its front-end and back-end stack, so I’ve thought it’d be great to talk a bit about current status and v1 direction.
mappedAttributes
Beside observedAttributes
, that acts like regular Custom Elements definition, and booleanAttributes
, that simplify cases where an attribute should be present or not in a component, similar to what native button.disable
would do, the mappedAttributes
are a new entry that allow the following features:
- each attribute is defined as accessor, and any kind of value can be passed in as is, without undesired
string
conversion, as it would be for regular attributes landing on the DOM - these attributes are not reflected on the DOM, but these are passed directly to the component
- if the component defines one or more
on${mappedAttribute}(event)
method, this will be invoked each time the attribute is passed along, during any lighterhtml rendering cycle.
const List = {
extends: 'ul',
mappedAttributes: ['items'],
onitems({detail}) {
console.log(`items: ${detail}`);
this.render();
},
render() {
this.html`${this.items.map(
item => html`<li>${item.text}</li>`
)}`;
}
};define('List', List);
render(container, () => html`<List .items=${db.result}/>`);
The usage of .attribute=...
instead of just attribute=...
uses a recent feature introduced in lighterhtml, where properties are directly assigned, instead of stringified through the DOM.
Not only the syntax is self-explanatory, mappedAttributes
don’t reflect on the page/layout, so that having a visual indication of what’s being set as property helps understanding the component behavior and its final layout, so that using style to address ul[items]
, as example, wouldn’t make sense.
fragment
This is a whole new primitive that doesn’t exist in the Custom Elements world, but it does in heresy: a way to define a component container that could have one or more nodes in its content.
const FUserPass = {
extends: 'fragment',
includes: {User, Pass},
render() {
this.html`<User/><Pass/>`;
}
};const Login = {
extends: 'form',
includes: {FUserPass},
render() {
this.html`
<FUserPass/>
<input type=submit>
`;
}
};render(container, Login);
As you would expect, a fragment doesn’t really exist on the DOM, it’s just a way to pass along one or more elements, without polluting the tree with unnecessary wrappers.
Because of this very peculiar behavior, whenever you need to deal with a fragment, remember the following:
- its
childNodes
won’t be reachable after its first render (basically never) - if you need to address any of its children, use the
ref(...)
utility exported by heresy - styles associated with a fragment are basically irrelevant, as these won’t ever be able to address a real node, so that you could use the
style
feature, but you’ll likely need to skip its first argument, as it’s irrelevant - because fragments don’t exist in the layout, if you need to pass data or properties for its children while rendering, use
mappedAttributes
const FUserPass = {
extends: 'fragment',
includes: {User, Pass},
mappedAttributes: ['data'],
ondata({detail}){
const {current: user} = ref(this, 'user');
const {current: pass} = ref(this, 'pass');
if (user || pass) {
user.value = detail.user;
pass.value = detail.pass;
}
},
render() {
this.html`
<User ref=${ref(this, 'user')}/>
<Pass ref=${ref(this, 'pass')/>
`;
}
};const Login = {
extends: 'form',
includes: {FUserPass},
render() {
this.html`
<FUserPass data=${{user: 'test', pass: '1234'}}/>
<input type=submit>
`;
}
};
Adding events to a fragment would also not produce the desired behavior so just stick with the fact it’s simply a way to group other components together.
define-less components
If you look closer, you’d realize I’ve already sneaked in this feature in the previous example: you can now avoid global definitions as a whole, and simply pass your component to be rendered, considering right now each render will use a new instance of such component.
render(container, Login);
This is specially useful to define macro containers, such as a body, a main section, or a menu, without needing to steal a Custom Element valid name from the global registry, which is super handy when all your components are confined within their container, and the chances that such container changes in the life-cycle of your Web app is rather rare.
This also simplifies prototyping, as performances and DOM changes in lighterhtml are always fast enough, no matter the circumstances.
Toward v1
The more I “play” with heresy, the more I am convinced using classes with it makes little sense, ’cause these are more verbose, sometimes less expressive, but these also require an environment understanding of what is a HTMLAnchorElement
or any other kind of DOM node you’d like to represent.
In few words, extending global classes is almost meaningless in Node.js or other back-end centric environment, while the literal object definition would work, and play super well, in isolation, in every env, or even in native environments, thanks to its DOM-less expressive, and declarative, definition.
The code size might also see some benefit in ignoring classes based components, and composition via Object.assign
or modern {...Comp1, ...}
literal “extend” wins in simplicity and clarity of the intents, allowing multiple components composition with ease, bypassing the single inheritance limit provided by classes.
Despite these last considerations, I hope you’ll enjoy the new features, and also the fact heresy is close to be features complete 🎉