@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
666 lines (639 loc) • 51.1 kB
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Data Flow – UIElement Docs</title>
<meta name="description" content="Passing state, events, context" />
<link rel="stylesheet" href="./assets/main.css" />
<script type="module" src="./assets/main.js"></script>
</head>
<body class="">
<context-router>
<header class="content-grid">
<h1 class="content">
UIElement Docs <small>Version 0.13.3</small>
</h1>
<section-menu>
<nav>
<h2 class="visually-hidden">Main Menu</h2>
<ol>
<li>
<a href="index.html">
<span class="icon">📖</span>
<strong>Introduction</strong>
<small>Overview and key benefits of UIElement</small>
</a>
</li>
<li>
<a href="getting-started.html">
<span class="icon">🚀</span>
<strong>Getting Started</strong>
<small>Installation, setup, and first steps</small>
</a>
</li>
<li>
<a href="components.html">
<span class="icon">🏗️</span>
<strong>Components</strong>
<small>Anatomy, lifecycle, signals, effects</small>
</a>
</li>
<li>
<a href="styling.html">
<span class="icon">🎨</span>
<strong>Styling</strong>
<small>Scoped styles, CSS custom properties</small>
</a>
</li>
<li>
<a href="data-flow.html">
<span class="icon">🔄</span>
<strong>Data Flow</strong>
<small>Passing state, events, context</small>
</a>
</li>
<li>
<a href="examples.html">
<span class="icon">🍽️</span>
<strong>Examples</strong>
<small>Common use cases and demos</small>
</a>
</li>
<li>
<a href="blog.html">
<span class="icon">📜</span>
<strong>Blog</strong>
<small>Latest articles and updates</small>
</a>
</li>
<li>
<a href="api.html">
<span class="icon">📚</span>
<strong>API Reference</strong>
<small>Functions, types, and constants</small>
</a>
</li>
<li>
<a href="about.html">
<span class="icon">🤝</span>
<strong>About</strong>
<small>License, versioning, getting involved</small>
</a>
</li>
</ol>
</nav>
</section-menu>
<card-callout class="content danger" hidden>
<p class="error" role="alert" aria-live="polite"></p>
</card-callout>
</header>
<main class="content-grid"><section-hero>
<h1 id="data-flow">
<a name="data-flow" class="anchor" href="#data-flow">
<span class="permalink">🔗</span>
</a>
🔄 Data Flow
</h1>
<div>
<p class="lead"><strong>Learn how UIElement components can work together seamlessly.</strong> Start with simple parent-child relationships, then explore advanced patterns like custom events and shared state. Build modular, loosely coupled components that communicate efficiently.</p>
<module-toc>
<nav>
<h2>In this Page</h2>
<ol>
<li><a href="#component-coordination">Component Coordination</a></li>
<li><a href="#custom-events">Custom Events</a></li>
<li><a href="#providing-context">Providing Context</a></li>
<li><a href="#consuming-context">Consuming Context</a></li>
<li><a href="#next-steps">Next Steps</a></li>
</ol>
</nav>
</module-toc>
</div>
</section-hero>
<section>
<h2 id="component-coordination">
<a name="component-coordination" class="anchor" href="#component-coordination">
<span class="permalink">🔗</span>
</a>
Component Coordination
</h2>
<p>Let's consider a <strong>product catalog</strong> where users can add items to a shopping cart. We have <strong>three independent components</strong> that work together:</p>
<ul>
<li><code>ModuleCatalog</code> <strong>(Parent)</strong>:<ul>
<li><strong>Tracks all <code>SpinButton</code> components</strong> in its subtree and calculates the total count of items in the shopping cart.</li>
<li><strong>Passes that total</strong> to a <code>BasicButton</code>.</li>
</ul>
</li>
<li><code>BasicButton</code> <strong>(Child)</strong>:<ul>
<li>Displays a <strong>badge</strong> in the top-right corner when the <code>badge</code> property is set.</li>
<li><strong>Does not track any state</strong> – it simply renders whatever value is passed to it.</li>
</ul>
</li>
<li><code>FormSpinbutton</code> <strong>(Child)</strong>:<ul>
<li>Displays an <strong>Add to Cart</strong> button initially.</li>
<li>When an item is added, it transforms into a <strong>stepper</strong> (increment/decrement buttons).</li>
</ul>
</li>
</ul>
<p>Although <code>BasicButton</code> and <code>FormSpinbutton</code> are completely independent, they need to work together. So <code>ModuleCatalog</code> <strong>coordinates the data flow between them</strong>.</p>
<h3 id="parent-component-modulecatalog">
<a name="parent-component-modulecatalog" class="anchor" href="#parent-component-modulecatalog">
<span class="permalink">🔗</span>
</a>
Parent Component: ModuleCatalog
</h3>
<p>The <strong>parent component (<code>ModuleCatalog</code>) knows about its children</strong>, meaning it can <strong>read state from and pass state to</strong> them.</p>
<p>First, we need to observe the quantities of all <code>FormSpinbutton</code> components. For this, we create a signal of all children matching the <code>form-spinbutton</code> selector:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'module-catalog'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> total: </span><span style="color:#A6E22E">fromDescendants</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'form-spinbutton'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">sum</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">item</span><span style="color:#F8F8F2">) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> sum </span><span style="color:#F92672">+</span><span style="color:#F8F8F2"> item.value,</span></span>
<span class="line"><span style="color:#AE81FF"> 0</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>The <code>fromDescendants()</code> function returns a signal of the reduced array of all matching elements. In contrast to a static <code>querySelectorAll()</code> call, the <code>fromDescendants()</code> function is reactive and updates whenever new elements are added or removed from the DOM.</p>
<p>Then, we need to convert the total of all product quantities to a string and pass it on to the <code>BasicButton</code> component. In UIElement we use the <code>pass()</code> function to share state across components:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'module-catalog'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> total: </span><span style="color:#A6E22E">fromDescendants</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'form-spinbutton'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">sum</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">item</span><span style="color:#F8F8F2">) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> sum </span><span style="color:#F92672">+</span><span style="color:#F8F8F2"> item.value,</span></span>
<span class="line"><span style="color:#AE81FF"> 0</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'basic-button'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> pass</span><span style="color:#F8F8F2">({</span></span>
<span class="line"><span style="color:#A6E22E"> badge</span><span style="color:#F8F8F2">: () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> (el.total </span><span style="color:#F92672">></span><span style="color:#AE81FF"> 0</span><span style="color:#F92672"> ?</span><span style="color:#A6E22E"> String</span><span style="color:#F8F8F2">(el.total) </span><span style="color:#F92672">:</span><span style="color:#E6DB74"> ''</span><span style="color:#F8F8F2">),</span></span>
<span class="line"><span style="color:#A6E22E"> disabled</span><span style="color:#F8F8F2">: () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> !</span><span style="color:#F8F8F2">el.total,</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>Allright, that's it!</p>
<ul>
<li>Whenever one of the <code>value</code> signals of a <code><form-spinbutton></code> updates, the total in the badge of <code><basic-button></code> automatically updates.</li>
<li>No need for event listeners or manual updates!</li>
</ul>
<h3 id="child-component-basicbutton">
<a name="child-component-basicbutton" class="anchor" href="#child-component-basicbutton">
<span class="permalink">🔗</span>
</a>
Child Component: BasicButton
</h3>
<p>The <code>BasicButton</code> component <strong>displays a badge when needed</strong> – it does not know about any other component nor track state itself. It just exposes a reactive properties <code>badge</code> of type <code>string</code> and <code>disabled</code> of type <code>boolean</code> and has effects to react to state changes that updates the DOM subtree.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'basic-button'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> disabled: </span><span style="color:#A6E22E">asBoolean</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> badge: </span><span style="color:#A6E22E">asString</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">_</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'button'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setProperty</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'disabled'</span><span style="color:#F8F8F2">)),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'.badge'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'badge'</span><span style="color:#F8F8F2">)),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<ul>
<li>Whenever the <code>disabled</code> property is updated by a parent component, the button is disabled or enabled.</li>
<li>Whenever the <code>badge</code> property is updated by a parent component, the badge text updates.</li>
<li>If <code>badge</code> is an empty string, the badge indicator is hidden (via CSS).</li>
</ul>
<h3 id="child-component-formspinbutton">
<a name="child-component-formspinbutton" class="anchor" href="#child-component-formspinbutton">
<span class="permalink">🔗</span>
</a>
Child Component: FormSpinbutton
</h3>
<p>The <code>FormSpinbutton</code> component reacts to user interactions and exposes a reactive property <code>value</code> of type <code>number</code>. It updates its own internal DOM subtree, but doesn't know about any other component nor where the value is used.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'form-spinbutton'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> value: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">all</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> zeroLabel </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> el.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'zero-label'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">||</span><span style="color:#E6DB74"> 'Add to Cart'</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> incrementLabel </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> el.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'increment-label'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">||</span><span style="color:#E6DB74"> 'Increment'</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> max </span><span style="color:#F92672">=</span><span style="color:#A6E22E"> asInteger</span><span style="color:#F8F8F2">(</span><span style="color:#AE81FF">9</span><span style="color:#F8F8F2">)(el, el.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'max'</span><span style="color:#F8F8F2">))</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#A6E22E"> nonZero</span><span style="color:#F92672"> =</span><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> el.value </span><span style="color:#F92672">!==</span><span style="color:#AE81FF"> 0</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'.value'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'value'</span><span style="color:#F8F8F2">), </span><span style="color:#A6E22E">show</span><span style="color:#F8F8F2">(nonZero)),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.decrement'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> show</span><span style="color:#F8F8F2">(nonZero),</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> el.value</span><span style="color:#F92672">--</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.increment'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> setText</span><span style="color:#F8F8F2">(() </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> (</span><span style="color:#A6E22E">nonZero</span><span style="color:#F8F8F2">() </span><span style="color:#F92672">?</span><span style="color:#E6DB74"> '+'</span><span style="color:#F92672"> :</span><span style="color:#F8F8F2"> zeroLabel)),</span></span>
<span class="line"><span style="color:#A6E22E"> setProperty</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'ariaLabel'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span></span>
<span class="line"><span style="color:#A6E22E"> nonZero</span><span style="color:#F8F8F2">() </span><span style="color:#F92672">?</span><span style="color:#F8F8F2"> incrementLabel </span><span style="color:#F92672">:</span><span style="color:#F8F8F2"> zeroLabel,</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> setProperty</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'disabled'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> el.value </span><span style="color:#F92672">>=</span><span style="color:#F8F8F2"> max),</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> el.value</span><span style="color:#F92672">++</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> all</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'button'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'keydown'</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">e</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> { key } </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> e</span></span>
<span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> ([</span><span style="color:#E6DB74">'ArrowUp'</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">'ArrowDown'</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">'-'</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">'+'</span><span style="color:#F8F8F2">].</span><span style="color:#A6E22E">includes</span><span style="color:#F8F8F2">(key)) {</span></span>
<span class="line"><span style="color:#F8F8F2"> e.</span><span style="color:#A6E22E">stopPropagation</span><span style="color:#F8F8F2">()</span></span>
<span class="line"><span style="color:#F8F8F2"> e.</span><span style="color:#A6E22E">preventDefault</span><span style="color:#F8F8F2">()</span></span>
<span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> (key </span><span style="color:#F92672">===</span><span style="color:#E6DB74"> 'ArrowDown'</span><span style="color:#F92672"> ||</span><span style="color:#F8F8F2"> key </span><span style="color:#F92672">===</span><span style="color:#E6DB74"> '-'</span><span style="color:#F8F8F2">) el.value</span><span style="color:#F92672">--</span></span>
<span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> (key </span><span style="color:#F92672">===</span><span style="color:#E6DB74"> 'ArrowUp'</span><span style="color:#F92672"> ||</span><span style="color:#F8F8F2"> key </span><span style="color:#F92672">===</span><span style="color:#E6DB74"> '+'</span><span style="color:#F8F8F2">) el.value</span><span style="color:#F92672">++</span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ]</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<ul>
<li>Whenever the user clicks a button or presses a handled key, the value property is updated.</li>
<li>The component sets hidden and disabled states of buttons and updates the text of the <code>.value</code> element.</li>
</ul>
<h3 id="full-example">
<a name="full-example" class="anchor" href="#full-example">
<span class="permalink">🔗</span>
</a>
Full Example
</h3>
<p>Here's how everything comes together:</p>
<ul>
<li>Each <code>FormSpinbutton</code> tracks its own value.</li>
<li>The <code>ModuleCatalog</code> sums all quantities and passes the total to <code>BasicButton</code>.</li>
<li>The <code>BasicButton</code> displays the total if it's greater than zero.</li>
</ul>
<p><strong>No custom events are needed – state flows naturally!</strong></p>
<module-demo>
<div class="preview">
<module-catalog>
<header>
<p>Shop</p>
<basic-button disabled>
<button type="button" disabled>
<span class="label">🛒 Shopping Cart</span>
<span class="badge"></span>
</button>
</basic-button>
</header>
<ul>
<li>
<p>Product 1</p>
<form-spinbutton
value="0"
zero-label="Add to Cart"
increment-label="Increment"
>
<button
type="button"
class="decrement"
aria-label="Decrement"
hidden
>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment">Add to Cart</button>
</form-spinbutton>
</li>
<li>
<p>Product 2</p>
<form-spinbutton
value="0"
zero-label="Add to Cart"
increment-label="Increment"
>
<button
type="button"
class="decrement"
aria-label="Decrement"
hidden
>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment">Add to Cart</button>
</form-spinbutton>
</li>
<li>
<p>Product 3</p>
<form-spinbutton
value="0"
zero-label="Add to Cart"
increment-label="Increment"
>
<button
type="button"
class="decrement"
aria-label="Decrement"
hidden
>
−
</button>
<p class="value" hidden>0</p>
<button type="button" class="increment">Add to Cart</button>
</form-spinbutton>
</li>
</ul>
</module-catalog>
</div>
<details>
<summary>ModuleCatalog Source Code</summary>
<module-lazy src="./examples/module-catalog.html">
<card-callout>
<p class="loading" role="status">Loading...</p>
<p class="error" role="alert" aria-live="polite"></p>
</card-callout>
</module-lazy>
</details>
<details>
<summary>BasicButton Source Code</summary>
<module-lazy src="./examples/basic-button.html">
<card-callout>
<p class="loading" role="status">Loading...</p>
<p class="error" role="alert" aria-live="polite"></p>
</card-callout>
</module-lazy>
</details>
<details>
<summary>FormSpinbutton Source Code</summary>
<module-lazy src="./examples/form-spinbutton.html">
<card-callout>
<p class="loading" role="status">Loading...</p>
<p class="error" role="alert" aria-live="polite"></p>
</card-callout>
</module-lazy>
</details>
</module-demo>
</section>
<section>
<h2 id="custom-events">
<a name="custom-events" class="anchor" href="#custom-events">
<span class="permalink">🔗</span>
</a>
Custom Events
</h2>
<p>Passing state down works well when a <strong>parent component can directly observe child state</strong>, but sometimes a <strong>child needs to notify its parent</strong> about an action <strong>without managing shared state itself</strong>.</p>
<p>Custom events are perfect for this - they allow components to communicate upward through the DOM tree without tight coupling.</p>
<h3 id="typescript-support-for-components-and-events">
<a name="typescript-support-for-components-and-events" class="anchor" href="#typescript-support-for-components-and-events">
<span class="permalink">🔗</span>
</a>
TypeScript Support for Components and Events
</h3>
<p>To get full TypeScript support, declare your components and custom events globally:</p>
<module-codeblock collapsed language="typescript" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">typescript</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#88846F">// In your component file</span></span>
<span class="line"><span style="color:#F92672">export</span><span style="color:#66D9EF;font-style:italic"> type</span><span> </span><span style="color:#A6E22E;text-decoration:underline">ProductCardProps</span><span style="color:#F92672"> =</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> productId</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> string</span></span>
<span class="line"><span style="color:#F8F8F2"> quantity</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> number</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672">declare</span><span style="color:#F8F8F2"> global {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> interface</span><span> </span><span style="color:#A6E22E;text-decoration:underline">HTMLElementTagNameMap</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#E6DB74"> 'product-card'</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">Component</span><span style="color:#F8F8F2"><</span><span style="color:#A6E22E;text-decoration:underline">ProductCardProps</span><span style="color:#F8F8F2">></span></span>
<span class="line"><span style="color:#E6DB74"> 'shopping-cart'</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">Component</span><span style="color:#F8F8F2"><</span><span style="color:#A6E22E;text-decoration:underline">ShoppingCartProps</span><span style="color:#F8F8F2">></span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> interface</span><span> </span><span style="color:#A6E22E;text-decoration:underline">HTMLElementEventMap</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> itemAdded</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">CustomEvent</span><span style="color:#F8F8F2"><{ id</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> string</span><span style="color:#F8F8F2">; quantity</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> number</span><span style="color:#F8F8F2"> }></span></span>
<span class="line"><span style="color:#F8F8F2"> cartUpdated</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">CustomEvent</span><span style="color:#F8F8F2"><{ total</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> number</span><span style="color:#F8F8F2"> }></span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>This enables full type checking, autocompletion, and access to UIElement component methods like <code>.getSignal()</code> and <code>.setSignal()</code>.</p>
<h3 id="example-shopping-cart-events">
<a name="example-shopping-cart-events" class="anchor" href="#example-shopping-cart-events">
<span class="permalink">🔗</span>
</a>
Example: Shopping Cart Events
</h3>
<p>Consider a <strong>product card</strong> that needs to notify its parent when an item is added:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#88846F">// Child component dispatches custom event</span></span>
<span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'product-card'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> productId: </span><span style="color:#A6E22E">asString</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> quantity: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.add-button'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> // Dispatch custom event with product details</span></span>
<span class="line"><span style="color:#F8F8F2"> el.</span><span style="color:#A6E22E">dispatchEvent</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#F92672"> new</span><span style="color:#A6E22E"> CustomEvent</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'itemAdded'</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> detail: {</span></span>
<span class="line"><span style="color:#F8F8F2"> id: el.productId,</span></span>
<span class="line"><span style="color:#F8F8F2"> quantity: el.quantity,</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> bubbles: </span><span style="color:#AE81FF">true</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> )</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#88846F">// Parent component listens for custom events</span></span>
<span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'shopping-cart'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> items: </span><span style="color:#A6E22E">fromEvent</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'product-card'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#E6DB74"> 'itemAdded'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> ({ </span><span style="color:#FD971F;font-style:italic">event</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">source</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">value</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> // TypeScript knows 'source' is Component<ProductCardProps></span></span>
<span class="line"><span style="color:#88846F"> // Can access UIElement methods like source.getSignal('quantity')</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> newItem </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> id: event.detail.id,</span></span>
<span class="line"><span style="color:#F8F8F2"> quantity: event.detail.quantity,</span></span>
<span class="line"><span style="color:#F8F8F2"> addedAt: Date.</span><span style="color:#A6E22E">now</span><span style="color:#F8F8F2">(),</span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> [</span><span style="color:#F92672">...</span><span style="color:#F8F8F2">value, newItem]</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> [],</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> total</span><span style="color:#F8F8F2">: () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> el.items.</span><span style="color:#A6E22E">reduce</span><span style="color:#F8F8F2">((</span><span style="color:#FD971F;font-style:italic">sum</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">item</span><span style="color:#F8F8F2">) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> sum </span><span style="color:#F92672">+</span><span style="color:#F8F8F2"> item.quantity, </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">),</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'.cart-count'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'total'</span><span style="color:#F8F8F2">)),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.items-list'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> setText</span><span style="color:#F8F8F2">(() </span><span style="color:#66D9EF;font-style:italic">=></span></span>
<span class="line"><span style="color:#F8F8F2"> el.items.</span><span style="color:#A6E22E">map</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">item</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#E6DB74"> `</span><span style="color:#F92672">${</span><span style="color:#F8F8F2">item.id</span><span style="color:#F92672">}</span><span style="color:#E6DB74">: </span><span style="color:#F92672">${</span><span style="color:#F8F8F2">item.quantity</span><span style="color:#F92672">}</span><span style="color:#E6DB74">`</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">join</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">', '</span><span style="color:#F8F8F2">),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672">declare</span><span style="color:#F8F8F2"> global {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> interface</span><span> </span><span style="color:#A6E22E;text-decoration:underline">HTMLElementTagNameMap</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#E6DB74"> 'shopping-cart'</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">Component</span><span style="color:#F8F8F2"><</span><span style="color:#A6E22E;text-decoration:underline">ShoppingCartProps</span><span style="color:#F8F8F2">></span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<h3 id="benefits-of-custom-events">
<a name="benefits-of-custom-events" class="anchor" href="#benefits-of-custom-events">
<span class="permalink">🔗</span>
</a>
Benefits of Custom Events
</h3>
<ul>
<li><strong>Decoupling</strong>: Child components don't need to know about parent implementation</li>
<li><strong>Reusability</strong>: Components can be used in different contexts</li>
<li><strong>Standard DOM</strong>: Uses native event system, works with any framework</li>
<li><strong>Bubbling</strong>: Events naturally flow up the DOM tree</li>
<li><strong>Cancellable</strong>: Parent can prevent default behavior if needed</li>
</ul>
<h3 id="when-to-use-custom-events">
<a name="when-to-use-custom-events" class="anchor" href="#when-to-use-custom-events">
<span class="permalink">🔗</span>
</a>
When to Use Custom Events
</h3>
<ul>
<li><strong>User Actions</strong>: Button clicks, form submissions, gestures</li>
<li><strong>State Changes</strong>: When a component's internal state affects others</li>
<li><strong>Lifecycle Events</strong>: Component initialization, destruction, errors</li>
<li><strong>Data Flow</strong>: When child needs to send data upward without direct coupling</li>
</ul>
<h3 id="component-type-safety-best-practices">
<a name="component-type-safety-best-practices" class="anchor" href="#component-type-safety-best-practices">
<span class="permalink">🔗</span>
</a>
Component Type Safety Best Practices
</h3>
<p>Each UIElement component should declare its own <code>HTMLElementTagNameMap</code> extension:</p>
<module-codeblock collapsed language="ts" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">ts</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#88846F">// In my-component.ts</span></span>
<span class="line"><span style="color:#F92672">export</span><span style="color:#66D9EF;font-style:italic"> type</span><span> </span><span style="color:#A6E22E;text-decoration:underline">MyComponentProps</span><span style="color:#F92672"> =</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> value</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> string</span></span>
<span class="line"><span style="color:#F8F8F2"> count</span><span style="color:#F92672">:</span><span style="color:#66D9EF;font-style:italic"> number</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672">export</span><span style="color:#F92672"> default</span><span style="color:#A6E22E"> component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> /* ... */</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672">declare</span><span style="color:#F8F8F2"> global {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> interface</span><span> </span><span style="color:#A6E22E;text-decoration:underline">HTMLElementTagNameMap</span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F92672">:</span><span> </span><span style="color:#A6E22E;text-decoration:underline">Component</span><span style="color:#F8F8F2"><</span><span style="color:#A6E22E;text-decoration:underline">MyComponentProps</span><span style="color:#F8F8F2">></span></span>
<span class="line"><span style="color:#F8F8F2"> }</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>This enables:</p>
<ul>
<li><strong>Full type safety</strong> when using signal producers like <code>fromDescendants('my-component', ...)</code></li>
<li><strong>Access to UIElement methods</strong> like <code>.getSignal()</code> and <code>.setSignal()</code></li>
<li><strong>IntelliSense</strong> for component properties and methods</li>
<li><strong>Compile-time validation</strong> of component interactions</li>
</ul>
</section>
<section>
<h2 id="providing-context">
<a name="providing-context" class="anchor" hre