A base class for custom elements.
Define and register your element:
import XElement from 'https://deno.land/x/element/x-element.js';
class HelloWorld extends XElement {
static template(html) {
return () => html`<span>Hello World!</span>`;
}
}
customElements.define('hello-world', HelloWorld);
And use it in your markup:
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World</title>
<script type="module" src="./hello-world.js"></script>
</head>
<body>
<hello-world></hello-world>
</body>
</html>
XElement has a built-in templating engine to efficiently manage DOM generation and data binding using native template literals.
It is also possible to use third party rendering engines. Check out the (template guide)[./TEMPLATES.md] to learn more.
Properties and their related attributes are watched. When a property or related attribute is updated, a render is queued.
Property definitions have the following options:
type
[Function]: associate properties with types.attribute
[String]: override default attribute for properties.input
[StringArray]: declare names of watched properties for a computed property.compute
[Function]: compute property value when input changes.reflect
[Boolean]: reflect properties back to attributes.observe
[Function]: react when properties change.initial
[Function|Any]: provide initial, default values for nullish properties.default
[Function|Any]: provide recurring, default values for nullish properties.readOnly
[Boolean]: prevent setting properties on the host.internal
[Boolean]: prevent getting / setting properties on the host.
class RightTriangle extends XElement {
static get properties() {
return {
base: {
type: Number,
initial: 3,
},
height: {
type: Number,
initial: 4,
},
hypotenuse: {
type: Number,
input: ['base', 'height'],
compute: Math.hypot,
},
};
}
static template(html) {
return ({ base, height, hypotenuse }) => html`
<code>Math.hypot(${base}, ${height}) = ${hypotenuse}<code>
`;
}
}
By default, property attributes are synced — i.e., updating the attribute will
update the property. Bi-directional syncing only happens if you reflect the
property by setting reflect: true
.
Sometimes you want a property to be part of the public interface (either for
attribute or property introspection), but you want to manage the value of that
property internally. You can set readOnly: true
to achieve this. Such a
property can only be written to using host.internal
.
Other times, you don't want a property to be part of the public interface
(common for computed properties). You can set internal: true
to achieve this.
Such a property can only be read from or written to using host.internal
.
class MyElement extends XElement {
static get properties() {
return {
date: {
type: String,
readOnly: true,
initial: () => new Date().toISOString(),
reflect: true,
},
interval: {
internal: true,
},
};
}
connectedCallback() {
super.connectedCallback();
clearInterval(this.internal.interval);
this.internal.interval = setInterval(() => this.tick(), 100);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.internal.interval);
}
tick() {
this.internal.date = new Date().toISOString();
}
static template(html) {
return ({ date }) => html`<span>${date}<span>`;
}
}
Create a computed property by providing a list other property names as input
and a compute
callback to be invoked when the input changes. The compute
callbacks are memoized and are expected to be pure.
If the given function is defined on the constructor, the context (this
) is
guaranteed to be the constructor when called later.
Computed properties are lazily evaluated. If a computed property is not rendered in a template function or accessed programmatically, its code will not run.
Create an observed property by providing an observe
function. Whenever an
observable change occurs, your function will be called with
host, value, oldValue
.
If the given function is defined on the constructor, the context (this
) is
guaranteed to be the constructor when called later.
XElement supports declarative, delegated event handlers via a listeners
block. Listeners are added during the connectedCallback
and are removed during
the disconnectedCallback
in the element's lifecycle. Listeners defined in
the listeners
block are added to the render root.
Because it's so common to want to introspect the event
and call a function on
the host
, the provided arguments are host, event
.
If the given function is defined on the constructor, the context (this
) is
guaranteed to be the constructor when called later.
class MyElement extends XElement {
static get properties() {
return {
clicks: {
type: Number,
readOnly: true,
default: 0,
},
};
}
static get listeners() {
return { click: this.onClick };
}
static onClick(host, event) {
host.internal.clicks += event.detail;
}
static template(html) {
return ({ clicks }) => {
return html`<span>Clicks: ${clicks}</span>`;
}
}
}
If you need more fine-grain control over when listeners are added or removed,
you can use the listen
and unlisten
functions.
Because it's so common to want to introspect the event
and call a function on
the host
, the provided arguments will always be host, event
.
If the given function is defined on the constructor, the context (this
) is
guaranteed to be the constructor when called later.
class MyElement extends XElement {
static get properties() {
return {
clicks: {
type: Number,
readOnly: true,
default: 0,
},
};
}
static onClick(host, event) {
host.internal.clicks += event.detail;
}
connectedCallback() {
super.connectedCallback();
this.listen(this.shadowRoot, 'click', this.constructor.onClick);
}
disconnectedCallback() {
super.disconnectedCallback();
this.unlisten(this.shadowRoot, 'click', this.constructor.onClick);
}
static template(html) {
return ({ clicks }) => {
return html`<span>Clicks: ${clicks}</span>`;
}
}
}
The recommended way to add styles to your shadow root is to author a separate
.css
file, import it as a CSSStyleSheet
and declare it in your .styles
.
For more control, you can alternatively use createRenderRoot
(see below).
import styleSheet from './my-element-style.css' with { type: 'css' };
class MyElement extends XElement {
static get styles() {
return [styleSheet];
}
}
By default, XElement will create an open shadow root and use that as your render root. However, you may want to customize or not attach a shadow root at all.
Control special behavior like “focus delegation” by overriding the default shadow root configuration.
class MyElement extends XElement {
static createRenderRoot(host) {
return host.attachShadow({ mode: 'open', delegatesFocus: true });
}
}
Sometimes, you don’t want encapsulation. No problem — just return the host
.
class MyElement extends XElement {
static createRenderRoot(host) {
return host;
}
}
Analysis takes place once per class. This allows all future instances to share common setup work. Halting errors are thrown here to assist in development.
Each instance undergoes one-time setup work in the constructor
callback.
Each instance is initialized once during the first connectedCallback
.
When properties update on an initialized element, the following should occur:
- await a queued microtask (prevents unnecessary, synchronous work)
- compute properties (this is implied and happens lazily)
- reflect properties
- render result
- observe properties
In certain cases, you may want to observe multiple properties at once. One way to achieve this is to compute a new object and observe that object.
class MyElement extends XElement {
static get properties() {
return {
tag: {
type: String,
default: 'div',
},
text: {
type: String,
default: '',
},
element: {
type: HTMLElement,
internal: true,
input: ['tag'],
compute: tag => document.createElement(tag),
observe: (host, element) => {
const container = host.shadowRoot.getElementById('container');
container.innerHTML = '';
container.append(element);
}
},
update: {
type: Object,
internal: true,
input: ['element', 'text'],
compute: (element, text) => ({ element, text }),
observe: (host, { element, text }) => {
element.textContent = text;
},
},
};
}
static template(html) {
return () => {
return html`<div id="container"></span>`;
}
}
}