WebDev Bites: Spacer Component Design

design of horizontal and vertical spacer elements

1rem

1) Structure

This view illustrates component structure. The JavaScript syntax:
(() => {
  /* code here */
})();
defines a function with no parameters. The outer () turns the anonymous function definition into an expression. The trialing () causes the code to be executed immediately.
The block of code starting at line 42 defines the Horizontal Spacer component. The block of code starting at line 95 defines the Vertical Spacer component. Code starting at line 141 defines the tag names associated with these components.

2) HSpace Definition

Horizontal component definition. The horizontal spacer definition begins at line 44. That defines a JavaScript class derived from HTMLElement. Line 45 defines public attributes used to modify a specific instance's properties. Lines 47-90 define component styles and structure: an element with no child elements in a constructor that runs as soon as the code is parsed. :host represents the root of the component element. slot defines a place to insert content, e.g., size of the space. Lines 71-85 define event handling for component. Lines 87-90 handle size and thickness attributes.

2) VSpace Definition

Vertical component definition. Vertical spacer definition begins at line 97. That defines a JavaScript class derived from HTMLElement. Line 98 defines public attributes used to modify a specific instance's properties. Lines 100-120 define component styles and structure: an element with no child elements in a constructor that runs as soon as the code is parsed. :host represents the root of the component element. slot defines a place to insert content, e.g., size of the space. Lines 125-133 define event handling for component. Lines 136 and 136 handle size attribute.

4) Helpers and Attributes

Lines 8-16 convert unitless specifier [v] to [v]rem; Lines 18-31 evaluate the length which may be presented as:
  • text node content
  • attribute specified in opening tag
  • style defined by host
and return as css value.
Lines 33-40 evaluate the vertical thickness which may be presented as:
  • attribute specified in opening tag
  • propertye value set by JavaScript
and return as css value.
Lines 142-145 define tag names associated with this component.
  1 /* SpacerComponent.js
  2   Horizontal and Vertical Spacer Components
  3   - content may be provided in any of the conventional space metrics.
  4   - space may be either declared as element content or as attribute.
  5 */
  6 (() => {
      /* code elided */
 42   function defineHSpace(tagName) {
 43     if (customElements.get(tagName)) return;
 44     class HSpace extends HTMLElement {
          /* code elided */
 91     }
 92     customElements.define(tagName, HSpace);
 93   }
 94
 95   function defineVSpace(tagName) {
 96     if (customElements.get(tagName)) return;
 97     class VSpace extends HTMLElement {
          /* code elided
137     }
138     customElements.define(tagName, VSpace);
139   }
140
141   // Define primary tags and short aliases
142   defineHSpace('h-space');
143   defineHSpace('h-s');
144   defineVSpace('v-space');
145   defineVSpace('v-s');
146 })();

          /* code elided */
 41
 42   function defineHSpace(tagName) {
 43     if (customElements.get(tagName)) return;
 44     class HSpace extends HTMLElement {
 45       static get observedAttributes() { return ['size', 'block', 'thickness']; }
 46       #mo;
 47       constructor() {
 48         super();
 49         this.attachShadow({ mode: 'open' });
 50         // Host paints the gap; optional thickness shows background/borders if desired.
 51         this.shadowRoot.innerHTML = `
 52           <style>
 53             :host {
 54               display: inline-block;
 55               width: var(--_h-size, 1rem);
 56               height: var(--_h-thickness, 0); /* default 0 to avoid layout shift */
 57               line-height: 0;
 58               color: inherit;
 59               background-color: inherit;
 60               font: inherit;
 61             }
 62             /* Hide any light-DOM children assigned to the default slot */
 63             slot { display: none !important; }
 64           </style>
 65           <slot></slot>
 66         `;
 67         this.setAttribute('aria-hidden', 'true');
 68         this.setAttribute('role', 'presentation');
 69         this.#mo = new MutationObserver(() => this.#update());
 70       }
 71       connectedCallback() {
 72         this.#mo.observe(this, { characterData: true, childList: true, subtree: true });
 73         this.#update();
 74       }
 75       disconnectedCallback() { this.#mo.disconnect(); }
 76       attributeChangedCallback() { this.#update(); }
 77       #update() {
 78         // Display mode toggle
 79         this.style.display = this.hasAttribute('block') ? 'block' : 'inline-block';
 80         // Size + thickness
 81         const size = pickSize(this, 'h');
 82         const thick = pickThickness(this);
 83         this.shadowRoot.host.style.setProperty('--_h-size', size);
 84         this.shadowRoot.host.style.setProperty('--_h-thickness', thick);
 85       }
 86       // JS property sugar
 87       get size() { return this.getAttribute('size'); }
 88       set size(v) { if (v == null) this.removeAttribute('size'); else this.setAttribute('size', v); }
 89       get thickness() { return this.getAttribute('thickness'); }
 90       set thickness(v) { if (v == null) this.removeAttribute('thickness'); else this.setAttribute('thickness', v); }
 91     }
 92     customElements.define(tagName, HSpace);
 93   }
 94     /* code elided */

146 })();
    /* code elided */
 94
 95   function defineVSpace(tagName) {
 96     if (customElements.get(tagName)) return;
 97     class VSpace extends HTMLElement {
 98       static get observedAttributes() { return ['size', 'inline']; }
 99       #mo;
100       constructor() {
101         super();
102         this.attachShadow({ mode: 'open' });
103         // Host carries the height directly.
104         this.shadowRoot.innerHTML = `
105           <style>
106             :host {
107               display: block;
108               height: var(--_v-size, 1rem);
109               color: inherit;
110               background-color: inherit;
111               font: inherit;
112             }
113             slot { display: none !important; }
114           </style>
115           <slot></slot>
116         `;
117         this.setAttribute('aria-hidden', 'true');
118         this.setAttribute('role', 'presentation');
119         this.#mo = new MutationObserver(() => this.#update());
120       }
121       connectedCallback() {
122         this.#mo.observe(this, { characterData: true, childList: true, subtree: true });
123         this.#update();
124       }
125       disconnectedCallback() { this.#mo.disconnect(); }
126       attributeChangedCallback() { this.#update(); }
127       #update() {
128         // Display mode toggle (inline vertical spacer)
129         this.style.display = this.hasAttribute('inline') ? 'inline-block' : 'block';
130         // Size
131         const size = pickSize(this, 'v');
132         this.shadowRoot.host.style.setProperty('--_v-size', size);
133       }
134       // JS property sugar
135       get size() { return this.getAttribute('size'); }
136       set size(v) { if (v == null) this.removeAttribute('size'); else this.setAttribute('size', v); }
137     }
138     customElements.define(tagName, VSpace);
139   }
140
        /* code elided */

        /* code elided */
  8   const numberRE = new RegExp('^\s*\d+(?:\.\d+)?\s*$'); // 12 or 12.5 (no unit)
  9
 10   function asCssLength(val, fallback = '1rem') {
 11     if (val == null) return fallback;
 12     const s = String(val).trim();
 13     if (s === '') return fallback;
 14     if (numberRE.test(s)) return `${s}rem`; // unitless -> rems
 15     return s; // assume valid CSS length or var()/calc()
 16   }
 17
 18   function pickSize(el, kind /* 'h'|'v' */) {
 19     // 1) attribute
 20     const attr = el.getAttribute('size');
 21     if (attr && attr.trim() !== '') return asCssLength(attr);
 22     // 2) inner text (do NOT clear; hidden via shadow <slot>)
 23     const inline = (el.textContent || '').trim();
 24     if (inline) return asCssLength(inline);
 25     // 3) CSS var on the host
 26     const varName = (kind === 'h') ? '--h-space-size' : '--v-space-size';
 27     const cssVar = el.style.getPropertyValue(varName) || getComputedStyle(el).getPropertyValue(varName);
 28     if (cssVar && cssVar.trim() !== '') return asCssLength(cssVar);
 29     // 4) default
 30     return '1rem';
 31   }
 32
 33   function pickThickness(el) {
 34     // attribute > CSS var > default 0
 35     const attr = el.getAttribute('thickness');
 36     if (attr && attr.trim() !== '') return asCssLength(attr, '0');
 37     const cssVar = el.style.getPropertyValue('--h-thickness') || getComputedStyle(el).getPropertyValue('--h-thickness');
 38     if (cssVar && cssVar.trim() !== '') return asCssLength(cssVar, '0');
 39     return '0';
 40   }
 41
        /* code elided /
140
141   // Define primary tags and short aliases
142   defineHSpace('h-space');
143   defineHSpace('h-s');
144   defineVSpace('v-space');
145   defineVSpace('v-s');
146 })();