🔥 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 🎉

Web, Mobile, IoT, and all JS things since 00's. Formerly JS engineer at @nokia, @facebook, @twitter.