Web dev & architect
at Antreem
class MyBox
{ extends HTMLElement
constructor() { super(); // ... }
connectedCallback() {} disconnectedCallback() {} attributeChangedCallback() {} adoptedCallback() {}
} static get observedAttributes() { return ['foo']; }
>>>
operator
my-form.no-btn >>> button {...}
@apply
function
:root {
--my-mixin: { color: red; }
}
:host > div {
@apply(--my-mixin);
}
>>>
operator would defy the purpose of style encapsulation@apply
function needs separate declarations for pseudo-classes/elements and
isn't clear about some more complex uses.
#shadow-root
<header part="box-header">...</header>
<div part="box-content">...</div>
/* styles.css */
::part(box-header) {
font: bold 2em system-ui, sans-serif;
}
part
attribute with a name of our choice
constructor() {
...
const styleEl = document.createElement('style');
styleEl.textContent = 'div { color: red; }';
this.shadowRoot.appendChild(styleEl);
}
constructor() {
...
const linkEl = document.createElement('link');
linkEl.rel = 'stylesheet';
linkEl.href = 'component.css';
this.shadowRoot.appendChild(linkEl);
}
const styles = new CSSStyleSheet(); styles.replaceSync('div { color: red; }');
styles.replace('div { color: red; }') .then(sheet => { ... });
.replaceSync
isn't really different than creating a <style>
element
and defining textContent
, just nicer… maybe.
.replace
returns a promise that resolves when the stylesheet is fully
loaded, so we have more control over it.
CSSStyleSheet
objects? Let's see.import { styles } from './styles.js';
class MyComponent extends HTMLElement {
constructor() {
...
this.shadowRoot.adoptedStyleSheets = [ styles ];
}
}
.adoptedStyleSheets
property of the .shadowRoot
as an array
containing our styles, and we're done: our Web Component is now styled.
.adoptedStyleSheets
?Array
Object.freeze
document.adoptedStyleSheets = [
...document.adoptedStyleSheets,
sheet
];
add
or
define
, push
or whatever to add new "things" to collections, while we're
just creating new plain arrays. But it works…
import sheet from './styles.css';
sheet
is a CSSStyleSheet
import sheet from './styles.css';
class MyComponent extends HTMLElement {
constructor() {
...
this.shadowRoot.adoptedStyleSheets = [ sheet ];
}
}
<link rel="import" href="/template.html">
import templateDoc from './template.html';
<blockquote>
640K is more memory than anyone will ever need.
</blockquote>
import quoteDoc from './module.html';
console.log(quoteDoc.constructor.name); // HTMLDocument
const xhr = new XMLHttpRequest();
xhr.responseType = 'document';
xhr.open('GET', './module.html');
xhr.send();
xhr.onload = () => console.log(xhr.response);
script
s are executed
main.js<template>Today is <time></time></template> <script type="module">
const doc = import.meta.document; export const content = doc.querySelector('template').content;
</script>
import { content } from './module.html';
type="module"
work<template>Today is <time></time></template>
<script type="module">
import sheet from './today-date.css'; export class TodayDate extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.shadowRoow.adoptedStyleSheets = [ sheet ]; this.shadowRoot.appendChild(import.meta.document .querySelector('template').content.cloneNode(true) ); } }
</script>
What about templates?
<template>
s<template>
s are static<template> Today is <time></time> </template> <script>
const { content } = document.querySelector('template'); const copy = content.cloneNode(true); copy.querySelector('time').textContent = new Date();
</script>
<template>
s are not "string" templates, but rather DOM structure templates,
useful to create complex structures fast.
import { html } from 'lit-html'; function MyDate() { return html`
<div> Today is <time>
${ new Date() }
</time> </div>
`; }
<template>
Today is
<time datetime="{{ iso }}">
{{ date }}
</time>
</template>
const date = new Date();
const instance =
template.createInstance({
date: date.toString(),
iso: date.toISOString()
});
<template>
elements and a way to
replace the placeholders with the data provided. So we wouldn't have to clone the tree, target
the nodes and so on.
<p>Today is <time></time></p>
const todayPart = new ChildNodePart(timeEl);
todayPart.value = new Date().toDateString();
todayPart.commit();
<p>Today is <time>Wed Nov 25 2020</time></p>
role
, aria-*
…better-button { box-sizing: border-box; min-width: 5.14em; margin: 0 0.29em; font: inherit; text-transform: uppercase; outline-width: 0; border-radius: 3px; user-select: none; cursor: pointer; padding: 0.7em 0.57em; transition: box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); display: inline-block; overflow: hidden; position: relative; contain: content; } better-button[raised]:not([disabled]), better-button:not([disabled]):hover { box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); } better-button[disabled] { background: #eaeaea; color: #a8a8a8; cursor: auto; pointer-events: none; box-shadow: none; } better-button:not([disabled]):focus { font-weight: 500; box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.4); } better-button .ripple { position: absolute; transform: scale3d(0,0,0); opacity: 0.6; transition: all 800ms cubic-bezier(0.4, 0, 0.2, 1); border-radius: 50%; width: 150px; height: 150px; will-change: opacity, transform; pointer-events: none; z-index: -1; } better-button .ripple.run { opacity: 0; transform: none; } better-button:not(:defined) { color: red; }
class BetterButton extends HTMLElement { static get observedAttributes() { return ['disabled']; } get disabled() { return this.hasAttribute('disabled'); } set disabled(val) { if (val) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } } constructor() { super(); this.addEventListener('keydown', e => { if (e.keyCode === 32 || e.keyCode === 13) { this.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); } }); this.addEventListener('click', e => { if (this.disabled) { e.preventDefault(); e.stopPropagation(); } this.drawRipple(e.offsetX, e.offsetY); }); } connectedCallback() { this.setAttribute('role', 'button'); this.setAttribute('tabindex', '0'); } attributeChangedCallback(name, oldValue, newValue) { if (this.disabled) { this.setAttribute('tabindex', '-1'); this.setAttribute('aria-disabled', 'true'); } else { this.setAttribute('tabindex', '0'); this.setAttribute('aria-disabled', 'false'); } } drawRipple(x, y) { let div = document.createElement('div'); div.classList.add('ripple'); this.appendChild(div); div.style.top = `${y - div.clientHeight/2}px`; div.style.left = `${x - div.clientWidth/2}px`; div.style.backgroundColor = window .getComputedStyle(this).color; div.classList.add('run'); div.addEventListener('transitionend', e => div.remove()); } } window.customElements.define('better-button', BetterButton);
class MyButton extends HTMLButtonElement {…}
customElements.define('my-button', MyButton, { extends: 'button' });
<button is="my-button">…</button>
is=
attribute referencing our Custom Element. Kinda awkward, but there's even worse news.
is
attribute is a hack
and it's a sentiment shared by the proponents themselves.
element.ariaLabel = 'This is the result'; element.ariaLive = 'polite';
<output aria-label="This is the result" aria-live="polite">...</output>
class AlertModal extends HTMLElement { constructor() { super();
this.setAttribute('role', 'alertdialog'); this.setAttribute('aria-expanded', 'false');
} } #internals = this.attachInternals(); #internals.role = 'alertdialog'; #internals.ariaExpanded = false;
.setAttribute
to define the custom element's accessible properties,
and that's good because those attributes could be removed or changed from outside. Instead, we can
define an ElementInternals
object with this new method .attachInternals
.
This allows us to define accessible semantics internally, so they can't be touched externally.
And it gets better.
element.ariaDescribedBy = 'boxTitle';
// ... implies element.ariaDescribedByElements = [ boxTitleElement ];
Element(s)
suffix, that gets filled with the elements pointed by those id's.
Element(s)
to
get the same result, whether or not they have an id. Let's see why it's so important
for Web Components#shadow-root (custom-combobox)
<input aria-owns="optList" aria-activedescendant="opt1">
<slot></slot>
<custom-combobox>
<custom-optionlist id="optList">
<custom-option id="opt1">Option 1</custom-option>
...
</custom-optionlist>
</custom-combobox>
connectedCallback() {
const input = this.shadowRoot.querySelector('input');
const list = this.querySelector('custom-optionlist');
input.ariaOwnsElements = [ list ];
input.ariaActiveDescendantElement = list.firstChild;
}
ElementInternals
for custom elementsElementInternals
yields other nice things too, as it was actually conceived
to make Custom Elements as form elements. So we wouldn't actually need to extend
the HTMLInputElement
or similar classes.
<daily-question>
<template shadowroot="open">
<h2>Question of the day</h2>
<slot></slot>
</template>
<p>Is the cat inside?</p>
</daily-question>
<template>
element with this new shadowroot
attribute, that takes "open" or "closed" as
values like the possible modes of .attachShadow
. So this could be done by a server!
class DailyQuestion extends HTMLElement {
constructor() {
super();
// A custom element now *might* have
// a shadow root already attached
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoow.innerHTML = '...';
}
...
}
...
}
.attachShadow()
will just remove the one created by
the Declarative Shadow DOM, letting old Web Components still work.
<retro-banner>
<template shadowroot="open">
<link rel="stylesheet" href="/eigthies.css">
<h1><slot></slot></h1>
</template>
Bring the `80s back!
</retro-banner>
<link>
element inside the template, as we used to do with JavaScript.
The stylesheet gets cached too, so it won't be loaded again for all the other instances of the same
component.
const registry = new CustomElementRegistry(); registry.define('daily-question', DailyQuestion);
const root = document.querySelector('#app'); root.attachShadow({ mode: 'open', registry });
root.shadowRoot.innerHTML = '<daily-question>Tabs or spaces?</daily-question>';
CustomElementRegistry
, use it to define our custom elements
and attach it to a shadow root.
:state(foo)
)ElementInternals
was actually conceived with the initial intent of making
Custom Elements as form controls.
ElementInternals
, allows to
add, toggle and remove custom states and use them as pseudo-classes in CSS.
<slot>
elements aren't enough, we sometimes need more control
in placing projected content inside the Shadow DOM.
@customElement('simple-greeting')
export class SimpleGreeting extends LitElement {
@property() name = 'World';
render() {
return html`Hello, ${this.name}!
`;
}
}
@Component({ tag: 'my-first-component' })
export class MyComponent {
@Prop() name: string;
render() {
return <p>
My name is {this.name}
</p>;
}
}
A good ol' library directory
for (const question of questions) {
await answer(question);
}