PAD + web components

Web components on a PAD page

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.

1. A PAD custom tag writes the element

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>

2. The page uses it like any tag

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}

3. A module upgrades it

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);

4. Data, posts and forms