@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
462 lines (431 loc) β’ 22.7 kB
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'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>'badge'</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><spin-button></code> updates, the total in the badge of <code><input-button></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>'badge'</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('badge')</code> effect <strong>keeps the badge in sync</strong> with the <code>'badge'</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't care how the badge value is calculated</strong> β just that it gets one!</p>
<h3>Full Example</h3>
<p>Here'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'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'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>'add-todo'</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>'add-todo'</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>'add-todo'</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'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>'add-todo'</code></strong> with the new task as event detail.</li>
<li><strong><code>TodoApp</code> listens for <code>'add-todo'</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>