UNPKG

@zeix/ui-element

Version:

UIElement - minimal reactive framework based on Web Components

462 lines (431 loc) β€’ 22.7 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> <header class="content-grid"> <h1 class="content">UIElement Docs <small>Version 0.11.0</small></h1> <nav class="breakout"> <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="building-components.html"> <span class="icon">πŸ—οΈ</span> <strong>Building Components</strong> <small>Anatomy, lifecycle, signals, effects</small> </a> </li> <li> <a href="styling-components.html"> <span class="icon">🎨</span> <strong>Styling Components</strong> <small>Scoped styles, CSS custom properties</small> </a> </li> <li> <a href="data-flow.html" class="active"> <span class="icon">πŸ”„</span> <strong>Data Flow</strong> <small>Passing state, events, context</small> </a> </li> <li> <a href="patterns-techniques.html"> <span class="icon">πŸ’‘</span> <strong>Patterns & Techniques</strong> <small>Composition, scheduling, best practices</small> </a> </li> <li> <a href="examples-recipes.html"> <span class="icon">🍽️</span> <strong>Examples & Recipes</strong> <small>Common use cases and demos</small> </a> </li> <li> <a href="api-reference.html"> <span class="icon">πŸ“š</span> <strong>API Reference</strong> <small>Detailed documentation of classes and functions</small> </a> </li> <li> <a href="about-community.html"> <span class="icon">🀝</span> <strong>About & Community</strong> <small>License, versioning, getting involved</small> </a> </li> </ol> </nav> </header> <main> <section class="hero"> <h1>πŸ”„ Data Flow</h1> <p class="lead"><strong>UIElement enables smooth data flow between components using signals, events, and context.</strong> State can be <strong>passed down</strong> to child components, events can <strong>bubble up</strong> to notify parents of changes, and context can propagate across the component tree to <strong>share global state</strong> efficiently. This page explores different patterns for structuring data flow, helping you create modular, loosely coupled components that work seamlessly together.</p> </section> <section> <h2>Passing State Down</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>ProductCatalog</code> <strong>(Parent)</strong>:<ul> <li><strong>Tracks all <code>SpinButton</code> components</strong> in its subtree and calculates the <strong>total count</strong> of items in the shopping cart.</li> <li><strong>Passes that total</strong> to a <code>InputButton</code>, which displays the number of items in the cart.</li> </ul> </li> <li><code>InputButton</code> <strong>(Child)</strong>:<ul> <li>Displays a <strong>cart badge</strong> when the <code>&#39;badge&#39;</code> signal is set.</li> <li><strong>Does not track any state</strong> – it simply renders whatever value is passed down.</li> </ul> </li> <li><code>SpinButton</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>InputButton</code> <strong>and</strong> <code>SpinButton</code> are completely independent, they need to work together.<br>So <code>ProductCatalog</code> <strong>coordinates the data flow between them</strong>.</p> <h3>Parent Component: ProductCatalog</h3> <p>The <strong>parent component (<code>ProductCatalog</code>) knows about its children</strong>, meaning it can <strong>observe and pass state</strong> to them.</p> <p>Use the <code>.pass()</code> method to send values to child components. It takes an object where:</p> <ul> <li><strong>Keys</strong> = Signal names in the <strong>child</strong> (<code>InputButton</code>)</li> <li><strong>Values</strong> = Signal names in the parent (<code>ProductCatalog</code>) or functions returning computed values</li> </ul> <code-block 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:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'input-button'</span><span style="color:#F8F8F2">).</span><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:#A6E22E"> asPositiveIntegerString</span><span style="color:#F8F8F2">(</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">all</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'spin-button'</span><span style="color:#F8F8F2">).targets</span></span> <span class="line"><span style="color:#F8F8F2"> .</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.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'value'</span><span style="color:#F8F8F2">), </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> <span class="line"></span></code></pre> <input-button class="copy"> <button type="button" class="secondary small"> <span class="label">Copy</span> </button> </input-button> </code-block> <ul> <li>βœ… <strong>Whenever one of the <code>value</code> signals of a <code>&lt;spin-button&gt;</code> updates, the total in the badge of <code>&lt;input-button&gt;</code> automatically updates.</strong></li> <li>βœ… <strong>No need for event listeners or manual updates!</strong></li> </ul> <h3>Child Component: InputButton</h3> <p>The <code>InputButton</code> component <strong>displays a badge when needed</strong> – it does not track state itself.</p> <p>Whenever the <code>&#39;badge&#39;</code> <strong>signal assigned by a parent component</strong> updates, the badge text updates.</p> <code-block 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:#66D9EF;font-style:italic">class</span><span> </span><span style="color:#A6E22E;text-decoration:underline">InputButton</span><span style="color:#F92672"> extends</span><span> </span><span style="color:#A6E22E;font-style:italic;text-decoration:underline">UIElement</span><span style="color:#F8F8F2"> {</span></span> <span class="line"><span style="color:#A6E22E"> connectedCallback</span><span style="color:#F8F8F2">() {</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><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">sync</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> <input-button class="copy"> <button type="button" class="secondary small"> <span class="label">Copy</span> </button> </input-button> </code-block> <ul> <li>βœ… The <code>setText(&#39;badge&#39;)</code> effect <strong>keeps the badge in sync</strong> with the <code>&#39;badge&#39;</code> signal.</li> <li>βœ… If badge is an <strong>empty string</strong>, the badge is <strong>hidden</strong>.</li> </ul> <p>The <code>InputButton</code> <strong>doesn&#39;t care how the badge value is calculated</strong> – just that it gets one!</p> <h3>Full Example</h3> <p>Here&#39;s how everything comes together:</p> <ul> <li>Each <code>SpinButton</code> <strong>tracks its own count</strong>.</li> <li>The <code>ProductCatalog</code> <strong>sums all counts and passes the total to <code>InputButton</code></strong>.</li> <li>The <code>InputButton</code> <strong>displays the total</strong> if it&#39;s greater than zero.</li> </ul> <p><strong>No custom events are needed – state flows naturally!</strong></p> <component-demo> <div class="preview"> <product-catalog> <header> <p>Shop</p> <input-button> <button type="button"> <span class="label">πŸ›’ Shopping Cart</span> <span class="badge"></span> </button> </input-button> </header> <ul> <li> <p>Product 1</p> <spin-button 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> </spin-button> </li> <li> <p>Product 2</p> <spin-button 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> </spin-button> </li> <li> <p>Product 3</p> <spin-button 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> </spin-button> </li> </ul> </product-catalog> </div> <details> <summary>ProductCatalog Source Code</summary> <lazy-load src="./examples/product-catalog.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>InputButton Source Code</summary> <lazy-load src="./examples/input-button.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>SpinButton Source Code</summary> <lazy-load src="./examples/spin-button.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> </component-demo> </section> <section> <h2>Events Bubbling Up</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>Let&#39;s consider a Todo App, where users can add tasks:</p> <ul> <li><code>TodoApp</code> <strong>(Parent)</strong>:<ul> <li>Holds the list of todos as a state signal.</li> <li>Listens for an <code>&#39;add-todo&#39;</code> event from the child (<code>TodoForm</code>).</li> <li>Updates the state when a new todo is submitted.</li> </ul> </li> <li><code>TodoForm</code> <strong>(Child)</strong>:<ul> <li>Handles <strong>user input</strong> but does <strong>not</strong> store todos.</li> <li>Emits an <code>&#39;add-todo&#39;</code> event when the user submits the form.</li> <li>Lets the parent decide <strong>what to do with the data</strong>.</li> </ul> </li> </ul> <h3>Why use events here?</h3> <ul> <li>The child <strong>doesn’t need to know where the data goes</strong> – it just <strong>emits an event</strong>.</li> <li>The parent <strong>decides what to do</strong> with the new todo (e.g., adding it to a list).</li> <li>This keeps <code>TodoForm</code> <strong>reusable</strong> – it could work in different apps without modification.</li> </ul> <h3>Parent Component: TodoApp</h3> <p>The parent (<code>TodoApp</code>) <strong>listens for events</strong> and calls the <code>.addItem()</code> method on <code>TodoList</code> when a new todo is added:</p> <code-block 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:#FD971F">this</span><span style="color:#F8F8F2">.self.</span><span style="color:#A6E22E">on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'add-todo'</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:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">querySelector</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'todo-list'</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">addItem</span><span style="color:#F8F8F2">(e.detail)</span></span> <span class="line"><span style="color:#F8F8F2">})</span></span> <span class="line"></span></code></pre> <input-button class="copy"> <button type="button" class="secondary small"> <span class="label">Copy</span> </button> </input-button> </code-block> <ul> <li>βœ… <strong>Whenever <code>TodoForm</code> emits an <code>&#39;add-todo&#39;</code> event</strong>, a new task is appended to the todo list.</li> <li>βœ… The <strong>event carries data</strong> (<code>e.detail</code>), so the parent knows what was submitted.</li> </ul> <h3>Child Component: TodoForm</h3> <p>The child (<code>TodoForm</code>) <strong>collects user input</strong> and emits an event when the form is submitted:</p> <code-block 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:#66D9EF;font-style:italic">const</span><span style="color:#F8F8F2"> input </span><span style="color:#F92672">=</span><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">querySelector</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'input-field'</span><span style="color:#F8F8F2">)</span></span> <span class="line"><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'form'</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'submit'</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:#F8F8F2"> e.</span><span style="color:#A6E22E">preventDefault</span><span style="color:#F8F8F2">()</span></span> <span class="line"></span> <span class="line"><span style="color:#88846F"> // Wait for microtask to ensure the input field value is updated before dispatching the event</span></span> <span class="line"><span style="color:#A6E22E"> queueMicrotask</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"> value </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> input?.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'value'</span><span style="color:#F8F8F2">)?.</span><span style="color:#A6E22E">trim</span><span style="color:#F8F8F2">()</span></span> <span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> (value) {</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.self.</span><span style="color:#A6E22E">emit</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'add-todo'</span><span style="color:#F8F8F2">, value)</span></span> <span class="line"><span style="color:#F8F8F2"> input?.</span><span style="color:#A6E22E">clear</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></code></pre> <input-button class="copy"> <button type="button" class="secondary small"> <span class="label">Copy</span> </button> </input-button> <button type="button" class="overlay">Expand</button> </code-block> <ul> <li>βœ… The form does <strong>NOT store the todo</strong> – it just emits an event.</li> <li>βœ… The parent (<code>TodoApp</code>) <strong>decides what happens next</strong>.</li> <li>βœ… The <strong>event includes data</strong> (value), making it easy to handle.</li> </ul> <h3>Full Example</h3> <p>Here&#39;s how everything comes together:</p> <ul> <li><strong>User types a task</strong> into input field in <code>TodoForm</code>.</li> <li><strong>On submit, <code>TodoForm</code> emits <code>&#39;add-todo&#39;</code></strong> with the new task as event detail.</li> <li><strong><code>TodoApp</code> listens for <code>&#39;add-todo&#39;</code></strong> and updates the todo list.</li> </ul> <component-demo> <div class="preview"> <todo-app> <form action="#"> <input-field> <label for="add-todo">What needs to be done?</label> <div class="row"> <div class="group auto"> <input id="add-todo" type="text" value="" required> </div> </div> </input-field> <input-button class="submit"> <button type="submit" class="primary" disabled>Add Todo</button> </input-button> </form> <ol filter="all"></ol> <template> <li> <input-checkbox class="todo"> <label> <input type="checkbox" class="visually-hidden" /> <span></span> </label> </input-checkbox> <input-button class="delete"> <button type="button">Delete</button> </input-button> </li> </template> <footer> <div class="todo-count"> <p class="all-done">Well done, all done!</p> <p class="remaining"> <span class="count"></span> <span class="singular">task</span> <span class="plural">tasks</span> remaining </p> </div> <input-radiogroup value="all" class="split-button"> <fieldset> <legend class="visually-hidden">Filter</legend> <label class="selected"> <input type="radio" class="visually-hidden" name="filter" value="all" checked> <span>All</span> </label> <label> <input type="radio" class="visually-hidden" name="filter" value="active"> <span>Active</span> </label> <label> <input type="radio" class="visually-hidden" name="filter" value="completed"> <span>Completed</span> </label> </fieldset> </input-radiogroup> <input-button class="clear-completed"> <button type="button">Clear Completed</button> </input-button> </footer> </todo-app> </div> <details> <summary>TodoApp Source Code</summary> <lazy-load src="./examples/todo-app.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>InputField Source Code</summary> <lazy-load src="./examples/input-field.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>InputButton Source Code</summary> <lazy-load src="./examples/input-button.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>InputCheckbox Source Code</summary> <lazy-load src="./examples/input-checkbox.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> <details> <summary>InputRadiogroup Source Code</summary> <lazy-load src="./examples/input-radiogroup.html"> <p class="loading" role="status">Loading...</p> <p class="error" role="alert" aria-live="polite" hidden></p> </lazy-load> </details> </component-demo> </section> <section> <h2>Providing Context</h2> </section> <section> <h2>Consuming Context</h2> </section> <section> <h2>Next Steps</h2> </section> </main> <footer class="content-grid"> <div class="content"> <h2 class="visually-hidden">Footer</h2> <p>Β© 2025 Zeix AG</p> </div> </footer> </body> </html>