UNPKG

@zeix/ui-element

Version:

UIElement - a HTML-first library for reactive Web Components

666 lines (639 loc) 51.1 kB
<!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&#39;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&#39;s it!</p> <ul> <li>Whenever one of the <code>value</code> signals of a <code>&lt;form-spinbutton&gt;</code> updates, the total in the badge of <code>&lt;basic-button&gt;</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&#39;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&#39;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&#39;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">&#x3C;</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">&#x3C;</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">&#x3C;{ 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">&#x3C;{ 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&#x3C;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">&#x3C;</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&#39;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&#39;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">&#x3C;</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(&#39;my-component&#39;, ...)</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