Custom elements and shadow DOM are the browser's own components. PAD writes them with their shadow root already in place - declarative shadow DOM - and a small module adds behaviour.
A .pad file in _tags/ is a tag of the application. It writes the custom element, and
{shadow} writes its shadow root as a <template shadowrootmode="open"> - with the
stylesheet inlined, since a shadow root takes no styles from the page and a <style> in a
template would have its braces read as PAD tags:
{parms title, tone='info'}
<pad-card tone="{#tone}">
{shadow css='www:elements/card.css'}
<header part="head"><slot name="title"></slot></header>
<div class="body"><slot></slot></div>
<footer part="foot"><slot name="footer"></slot></footer>
{/shadow}
<span slot="title">{#title}</span>
@content@
</pad-card>
Parameters for what the tag writes, content for the light DOM the browser slots in -
slot="footer" goes to the <slot name="footer"> of the root:
{card title='Revenue', tone='good'}
<p class="stat">{$revenue}</p>
<a slot="footer" href="?report">Details</a>
{/card}
customElements.define in a module of www/. The element finds the root PAD rendered in
this.shadowRoot and only adds behaviour; before the module ran, the element already showed:
// <pad-tabs>: the shadow root is the one PAD rendered - this only adds behaviour. A click
// or an arrow key picks a tab; its panel shows, the others get hidden.
class PadTabs extends HTMLElement {
connectedCallback() {
this.tabs = [...this.querySelectorAll('[slot="tab"]')];
this.panels = [...this.querySelectorAll('[slot="panel"]')];
this.addEventListener('click', (event) => {
const tab = event.target.closest('[slot="tab"]');
if (tab) this.pick(this.tabs.indexOf(tab));
});
this.addEventListener('keydown', (event) => {
const at = this.tabs.indexOf(document.activeElement);
if (at < 0) return;
const step = { ArrowRight: 1, ArrowLeft: -1 }[event.key];
if (!step) return;
event.preventDefault();
this.pick((at + step + this.tabs.length) % this.tabs.length, true);
});
this.pick(0);
}
pick(at, focus) {
this.tabs.forEach((tab, index) => {
tab.setAttribute('aria-selected', String(index === at));
tab.tabIndex = index === at ? 0 : -1;
});
this.panels.forEach((panel, index) => { panel.hidden = index !== at; });
if (focus) this.tabs[at].focus();
}
}
customElements.define('pad-tabs', PadTabs);value="{$mine}") and JSON (items="{^items}");
CSS can style by an attribute before any script runs: :host([value="3"]).X-CSRF-Token header, read from
<meta name="csrf-token">; the page answers the JSON of $padExpose.static formAssociated = true and
ElementInternals.setFormValue - posts its value with a {form} like an
<input>, and padValidate checks it on the server.