This project is some sort of answer to these major trends:
* believing you cannot have tiny APIs which are able to compete with most famous frameworks * believing custom elements are not cool enough to compete with such frameworks * believing the built-in extends of custom elements are unnecessary or not useful at all
Borrowing concepts and patterns from various libraries, _heresy_ enables custom elements as you've never seen before:
* declarative UI (i.e. ) without needing JSX transformations or tooling at all * locally scoped custom elements to avoid name clashing and make components reusable in any context, similarly to what you can do with React components * automatic component name definition passed through the optional Component.style(...selectors) to inject related styles only once per definition * automatic handleEvent pattern so that you can forget the unnecessary overhead of this.method = this.method.bind(this) * out of the box lifecycle events, such as oninit(event), onconnected(event), ondisconnected(event) or onattributechanged(event), so that you can skip the ugly attributeChangedCallback and other unintuitive callbacks right away (but still use them if you like) * out of the box observedAttributes and booleanAttributes behavior, borrowed directly from HyperHTMLElement Class * automatic, smart component initializer via Component.new() that avoids all the quirks related to the initialization of custom elements and built-ins * an ever available comp.is string (you won't believe it's not always an attribute if created procedurally via a registered class) * automatic, lazy this.html and this.svg template literal tags, to populate a component's content within its optionally, locally scoped defined elements * provides a simplified way to target rendered nodes through the React-like ref() utility * hooks implemented via render({useState, ...}) definition. If a render has an argument, it will contain all hooks exported from augmentor. Import createContext from _heresy_, to be able to use render({useContext}). Import defineHook to create custom hooks.
$3
It is possible to define your own hooks through the defineHook(name, fn) utility.
const Comp = { extends: 'p', render({[uso]: useStateObject}) { const [state, update] = useStateObject(); // do something with the state } }; `
$3
A component can be defined through both classes or raw object literals.
`js //
// as object literal const literal = { name: 'Item', extends: 'li', // will extends li constructor render() { this.htmlmy name is ${this.props.name}; } };
// as class class Item extends HTMLLiElement { static name = 'Item'; // necessary if code gets transpiled static tagName = 'li'; // necessary to indicate the kind render() { this.htmlmy name is ${this.props.name}; } } `
While both the name and tag it represents, can be defined within the class or object, it's rather suggested to pre-define at least the tag it's going to represent, but not the name.
`js const literal = { extends: 'li', render() { this.htmlmy name is ${this.props.name}; } };
// in this way it's possible to define the name only via define('Item', literal); `
Alternatively, it is possible to not include name and tag, defining these via the Comp:tag or Comp convention.
`js class Item extends HTMLLiElement { render() { this.htmlmy name is ${this.props.name}; } }
define('MyItem
', Item); // OR define('MyItem:li', Item); `
#### Which tag ?
The beauty and power of the built-in extends of custom elements is that you can literally represent any tag you want/need.
However, if you'd like to simply extend a non-standard tag, you can always fall back to the
element tag kind, which will extend HTMLElement, and represent the component through its retrieved name.`js // either as object const Component = { extends: 'element', onconnected() { console.log(this.outerHTML); } };
// or as class class Component extends HTMLElement { static get tagName() { return 'element'; } onconnected() { console.log(this.outerHTML); } };
const MyElement = heresy.define('MyElement', Component); document.body.appendChild(MyElement.new()); // in console:
`
$3
While
define(...) will use the global registry to define the specific declarative name, making it a good practice to namespace it (i.e. FWDatePicker, StencilForm etc.), it is possible to define local components through the usage of includes, also aliased as contains.
Such a list will still pass through the registry, so that local components are fully valid custom elements that never name-clash with anything else, so that it's easier to split complex components into various sub-modules and only define their main container globally.
The following example has been rewritten with extra details and is live on codepen.
`js import {define, ref, render, html} from 'heresy';
import {User, Pass} from './form/ui.js'; import {validate, switchPage} from './form/utils.js';
const Form = { extends: 'form', includes: {User, Pass}, oninit() { // refs can be declared upfront or inline (see render) this.user = ref(); this.addEventListener('submit', this); }, onsubmit(event) { event.preventDefault(); if (validate(this.user.current, this.pass.current)) fetch('/log-in').then(switchPage).catch(console.error); }, // render is invoked automatically on connected // if no connected, or callback is explicitly defined render() { this.html
; } };
define('SiteLogin', Form); render(document.body, html
); `
The
includes or contains property, if present, must be a map of "Name": Component pairs, where the name could also define the tag type, like it does with define(...).
In the previous example both
User and Pass are components extending input, so that the tag name is not necessary, but {"User