@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
760 lines (684 loc) • 54.2 kB
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Building Components – UIElement Docs</title>
<meta name="description" content="Anatomy, lifecycle, signals, effects" />
<base href="./building-components.html" />
<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.12.2</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" class="active">
<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">
<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 id="building-components">
<a name="building-components" class="anchor" href="#building-components">
<span class="permalink">🔗</span>
</a>
🏗️ Building Components
</h1><p class="lead"><strong>Create lightweight, self-contained Web Components with built-in reactivity</strong>. UIElement lets you define custom elements that manage state efficiently, update the DOM automatically, and enhance server-rendered pages without an SPA framework.</p>
</section>
<section>
<h2 id="defining-a-component">
<a name="defining-a-component" class="anchor" href="#defining-a-component">
<span class="permalink">🔗</span>
</a>
Defining a Component
</h2><p>UIElement builds on <strong>Web Components</strong>, extending <code>HTMLElement</code> to provide <strong>built-in state management and reactive updates</strong>.</p>
<p>A UIElement creates components using the <code>component()</code> function:</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</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"> // Component setup</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>
<p>Every UIElement component must be registered with a valid custom element tag name (two or more words joined with <code>-</code>) as the first parameter.</p>
<h3 id="using-the-custom-element-in-html">
<a name="using-the-custom-element-in-html" class="anchor" href="#using-the-custom-element-in-html">
<span class="permalink">🔗</span>
</a>
Using the Custom Element in HTML
</h3><p>Once registered, the component can be used like any native HTML element:</p>
<code-block language="html" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">html</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#F8F8F2"><</span><span style="color:#F92672">my-component</span><span style="color:#F8F8F2">>Content goes here</</span><span style="color:#F92672">my-component</span><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>
</section>
<section>
<h2 id="component-lifecycle">
<a name="component-lifecycle" class="anchor" href="#component-lifecycle">
<span class="permalink">🔗</span>
</a>
Component Lifecycle
</h2><p>UIElement manages the <strong>Web Component lifecycle</strong> from creation to removal. Here's what happens.</p>
<h3 id="component-creation">
<a name="component-creation" class="anchor" href="#component-creation">
<span class="permalink">🔗</span>
</a>
Component Creation
</h3><p>In the <code>constructor()</code> reactive properties are initialized. You pass a second argument to the <code>component()</code> function to defines initial values for <strong>component states</strong>.</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">, </span><span style="color:#88846F">// Initial value of "count" signal</span></span>
<span class="line"><span style="color:#A6E22E"> isEven</span><span style="color:#F8F8F2">: </span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> !</span><span style="color:#F8F8F2">(el.count </span><span style="color:#F92672">%</span><span style="color:#AE81FF"> 2</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Computed signal based on "count" signal</span></span>
<span class="line"><span style="color:#F8F8F2"> value: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(</span><span style="color:#AE81FF">5</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Parse "value" attribute as integer defaulting to 5</span></span>
<span class="line"><span style="color:#F8F8F2"> name: </span><span style="color:#A6E22E">consume</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"display-name"</span><span style="color:#F8F8F2">) </span><span style="color:#88846F">// Consume "display-name" signal from closest context provider</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:#88846F"> // Component setup</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>
<p>In this example you see all four ways to define a reactive property:</p>
<ul>
<li>A <strong>static initial value</strong> for a <code>State</code> signal (e.g., <code>count: 0</code>)</li>
<li>A <strong>signal producer</strong> that derives an initial value or a callback function from other properties of the element (e.g., <code>isEven: el => () => !(el.count % 2)</code>)</li>
<li>An <strong>attribute parser</strong> that may provide a fallback value (e.g., <code>value: asInteger(5)</code>)</li>
<li>A <strong>context consumer</strong> that emits a <code>ContextRequestEvent</code> (e.g., <code>name: consume("display-name")</code>)</li>
</ul>
<callout-box class="caution">
<p><strong>Note</strong>: Property initialization runs <strong>before the element is attached to the DOM</strong>. You can't access not yet defined properties or child elements here.</p>
</callout-box>
<h3 id="mounted-in-the-dom">
<a name="mounted-in-the-dom" class="anchor" href="#mounted-in-the-dom">
<span class="permalink">🔗</span>
</a>
Mounted in the DOM
</h3><p>Runs when the component is added to the page (<code>connectedCallback()</code>). This is where you:</p>
<ul>
<li><strong>Access sub-elements</strong></li>
<li><strong>Set up event listeners</strong></li>
<li><strong>Apply effects</strong></li>
<li><strong>Emit custom events</strong></li>
<li><strong>Provide context</strong></li>
</ul>
<p>UIElement expects you to return an array of partially applied functions to be executed during the setup phase. The order doesn't matter, as each function targets a specific element or event. So feel free to organize your code in a way that makes sense to you.</p>
<p>Each of these functions will return a cleanup function that will be executed during the <code>disconnectedCallback()</code> lifecycle method.</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#AE81FF">0</span><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:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> emit</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"update-count"</span><span style="color:#F8F8F2">, el.count), </span><span style="color:#88846F">// Emit custom event</span></span>
<span class="line"><span style="color:#A6E22E"> provide</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"count"</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Provide context</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">".increment"</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"> { el.count</span><span style="color:#F92672">++</span><span style="color:#F8F8F2"> }) </span><span style="color:#88846F">// Add click event listener</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 style="color:#E6DB74">".count"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"count"</span><span style="color:#F8F8F2">) </span><span style="color:#88846F">// Apply effect to update text</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>
<h3 id="removed-from-the-dom">
<a name="removed-from-the-dom" class="anchor" href="#removed-from-the-dom">
<span class="permalink">🔗</span>
</a>
Removed from the DOM
</h3><p>Runs when the component is removed (<code>disconnectedCallback()</code>). UIElement will run all cleanup functions returned by event listeners and effects during the setup phase (<code>connectedCallback()</code>). This will unsubscribe all signals the component is subscribed to, so you don't need to worry about memory leaks.</p>
<p>If you added <strong>event listeners</strong> outside the scope of your component or <strong>subscribed manually to external APIs</strong>, you need to return a cleanup function:</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {}, </span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Setup logic</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:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> observer </span><span style="color:#F92672">=</span><span style="color:#F92672"> new</span><span style="color:#A6E22E"> IntersectionObserver</span><span style="color:#F8F8F2">(([</span><span style="color:#FD971F;font-style:italic">entry</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"> // Do something</span></span>
<span class="line"><span style="color:#F8F8F2"> })</span></span>
<span class="line"><span style="color:#F8F8F2"> observer.</span><span style="color:#A6E22E">observe</span><span style="color:#F8F8F2">(el);</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F"> // Cleanup logic</span></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> observer.</span><span style="color:#A6E22E">disconnect</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>
<button type="button" class="overlay">Expand</button>
</code-block>
<h3 id="observed-attributes">
<a name="observed-attributes" class="anchor" href="#observed-attributes">
<span class="permalink">🔗</span>
</a>
Observed Attributes
</h3><p>UIElement automatically observes and converts attributes with an associated <strong>parser function</strong> in the init block and updates them whenever the attribute changes (<code>attributeChangedCallback()</code>).</p>
</section>
<section>
<h2 id="managing-state-with-signals">
<a name="managing-state-with-signals" class="anchor" href="#managing-state-with-signals">
<span class="permalink">🔗</span>
</a>
Managing State with Signals
</h2><p>UIElement manages state using <strong>signals</strong>, which are atomic reactive states that trigger updates when they change. We use regular properties to access or update them:</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:#F8F8F2">console.</span><span style="color:#A6E22E">log</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"count"</span><span style="color:#F92672"> in</span><span style="color:#F8F8F2"> el); </span><span style="color:#88846F">// Check if the signal exists</span></span>
<span class="line"><span style="color:#F8F8F2">console.</span><span style="color:#A6E22E">log</span><span style="color:#F8F8F2">(el.count); </span><span style="color:#88846F">// Read the signal value</span></span>
<span class="line"><span style="color:#F8F8F2">el.count </span><span style="color:#F92672">=</span><span style="color:#AE81FF"> 42</span><span style="color:#F8F8F2">; </span><span style="color:#88846F">// Update the signal value</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>
<h3 id="characteristics-and-special-values">
<a name="characteristics-and-special-values" class="anchor" href="#characteristics-and-special-values">
<span class="permalink">🔗</span>
</a>
Characteristics and Special Values
</h3><p>Signals in UIElement are of a <strong>static type</strong> and <strong>non-nullable</strong>. This allows to <strong>simplify the logic</strong> as you will never have to check the type or perform null-checks.</p>
<ul>
<li>If you use <strong>TypeScript</strong> (recommended), <strong>you will be warned</strong> that <code>null</code> or <code>undefined</code> cannot be assigned to a signal or if you try to assign a value of a wrong type.</li>
<li>If you use vanilla <strong>JavaScript</strong> without a build step, setting a signal to <code>null</code> or <code>undefined</code> <strong>will log an error to the console and abort</strong>. However, strict type checking is not enforced at runtime.</li>
</ul>
<p>Because of the <strong>non-nullable nature of signals</strong> in UIElement, we need two special values that can be assigned to any signal type:</p>
<ul>
<li><strong><code>RESET</code></strong>: Will <strong>reset to the server-rendered version</strong> that was there before UIElement took control. This is what you want to do most of the times when a signal lacks a specific value.</li>
<li><strong><code>UNSET</code></strong>: Will <strong>delete the signal</strong>, <strong>unsubscribe its watchers</strong> and also <strong>delete related attributes or style properties</strong> in effects. Use this with special care!</li>
</ul>
<h3 id="initializing-state-from-attributes">
<a name="initializing-state-from-attributes" class="anchor" href="#initializing-state-from-attributes">
<span class="permalink">🔗</span>
</a>
Initializing State from Attributes
</h3><p>The standard way to set initial state in UIElement is via <strong>server-rendered attributes</strong> on the component that needs it. No props drilling as in other frameworks. UIElements provides some bundled attribute parsers to convert attribute values to the desired type. And you can also define your own custom parsers.</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(), </span><span style="color:#88846F">// Bundled parser: Convert '42' -> 42</span></span>
<span class="line"><span style="color:#A6E22E"> date</span><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">v</span><span style="color:#F8F8F2">) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> new</span><span style="color:#A6E22E"> Date</span><span style="color:#F8F8F2">(v), </span><span style="color:#88846F">// Custom parser: '2025-02-14' -> Date object</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:#88846F"> // Component setup</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>
<callout-box class="caution">
<p><strong>Careful</strong>: Attributes <strong>may not be present</strong> on the element or <strong>parsing to the desired type may fail</strong>. To ensure <strong>non-nullability</strong> of signals, UIElement falls back to neutral defaults:</p>
<ul>
<li><code>""</code> (empty string) for <code>string</code></li>
<li><code>0</code> for <code>number</code></li>
<li><code>{}</code> (empty object) for objects of any kind</li>
</ul>
</callout-box>
<h3 id="bundled-attribute-parsers">
<a name="bundled-attribute-parsers" class="anchor" href="#bundled-attribute-parsers">
<span class="permalink">🔗</span>
</a>
Bundled Attribute Parsers
</h3><table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>asBoolean</code></td>
<td>Converts <code>"true"</code> / <code>"false"</code> to a <strong>boolean</strong> (<code>true</code> / <code>false</code>). Also treats empty attributes (<code>checked</code>) as <code>true</code>.</td>
</tr>
<tr>
<td><code>asInteger()</code></td>
<td>Converts a numeric string (e.g., <code>"42"</code>) to an <strong>integer</strong> (<code>42</code>).</td>
</tr>
<tr>
<td><code>asNumber()</code></td>
<td>Converts a numeric string (e.g., <code>"3.14"</code>) to a <strong>floating-point number</strong> (<code>3.14</code>).</td>
</tr>
<tr>
<td><code>asString()</code></td>
<td>Returns the attribute value as a <strong>string</strong> (unchanged).</td>
</tr>
<tr>
<td><code>asEnum([...])</code></td>
<td>Ensures the string matches <strong>one of the allowed values</strong>. Example: <code>asEnum(["small", "medium", "large"])</code>. If the value is not in the list, it defaults to the first option.</td>
</tr>
<tr>
<td><code>asJSON({...})</code></td>
<td>Parses a JSON string (e.g., <code>'["a", "b", "c"]'</code>) into an <strong>array</strong> or <strong>object</strong>. If invalid, returns the fallback object.</td>
</tr>
</tbody></table>
<p>The pre-defined parsers <code>asInteger()</code>, <code>asNumber()</code> and <code>asString()</code> allow to set a custom fallback value as parameter.</p>
<p>The <code>asEnum()</code> parser requires an array of valid values, while the first will be the fallback value for invalid results.</p>
<p>The <code>asJSON()</code> parser requires a fallback object as parameter as <code>{}</code> probably won't match the type you're expecting.</p>
</section>
<section>
<h2 id="accessing-sub-elements">
<a name="accessing-sub-elements" class="anchor" href="#accessing-sub-elements">
<span class="permalink">🔗</span>
</a>
Accessing Sub-elements
</h2><p>Before adding <strong>event listeners</strong>, <strong>applying effects</strong>, or <strong>passing states</strong> to sub-elements, you need to select them using a function for <strong>element selection</strong>:</p>
<table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>first(selector, ...fns)</code></td>
<td>Selects <strong>the first matching element</strong> inside the component and applies the given setup functions.</td>
</tr>
<tr>
<td><code>all(selector, ...fns)</code></td>
<td>Selects <strong>all matching elements</strong> inside the component and applies the given setup functions.</td>
</tr>
</tbody></table>
<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:#88846F">// Select the first ".increment" button and apply effects on it</span></span>
<span class="line"><span style="color:#A6E22E">first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">".increment"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#88846F"> // Fx functions</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F">// Select all <button> elements and apply effects on them</span></span>
<span class="line"><span style="color:#A6E22E">all</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"button"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#88846F"> // Fx functions</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>
<p>The <code>first()</code> function expects the matched element to be present at connection time. If not, it will silently ignore the call.</p>
<p>On the other hand, the <code>all()</code> function creates a dynamic array of elements that will be updated whenever the matching elements are added or removed from the component's DOM branch. UIElement will apply the given setup functions to added elements and run the cleanup functions on removed elements.</p>
<callout-box class="tip">
<p><strong>Tip</strong>: The <code>all()</code> function is more flexible but also more resource-intensive than <code>first()</code>. Prefer <code>first()</code> when targeting a single element known to be present at connection time.</p>
</callout-box>
<h3 id="adding-event-listeners">
<a name="adding-event-listeners" class="anchor" href="#adding-event-listeners">
<span class="permalink">🔗</span>
</a>
Adding Event Listeners
</h3><p>Event listeners allow to respond to user interactions. They are the cause of changes in the component's state.</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> active: </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> value: </span><span style="color:#E6DB74">''</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:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> all</span><span style="color:#F8F8F2">(</span><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">"click"</span><span style="color:#F8F8F2">, (</span><span style="color:#FD971F;font-style:italic">e</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"> // Set "active" signal to value of data-index attribute of button</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> index </span><span style="color:#F92672">=</span><span style="color:#A6E22E"> parseInt</span><span style="color:#F8F8F2">(e.target.dataset[</span><span style="color:#E6DB74">'index'</span><span style="color:#F8F8F2">], </span><span style="color:#AE81FF">10</span><span style="color:#F8F8F2">);</span></span>
<span class="line"><span style="color:#F8F8F2"> el.active </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> Number.</span><span style="color:#A6E22E">isInteger</span><span style="color:#F8F8F2">(index) </span><span style="color:#F92672">?</span><span style="color:#F8F8F2"> index </span><span style="color:#F92672">:</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 style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"input"</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">"change"</span><span style="color:#F8F8F2">, (</span><span style="color:#FD971F;font-style:italic">e</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"> // Set "value" signal to value of input element</span></span>
<span class="line"><span style="color:#F8F8F2"> el.value </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> e.target.value;</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>
</section>
<section>
<h2 id="synchronizing-state-with-effects">
<a name="synchronizing-state-with-effects" class="anchor" href="#synchronizing-state-with-effects">
<span class="permalink">🔗</span>
</a>
Synchronizing State with Effects
</h2><p>Effects <strong>automatically update the DOM</strong> when signals change, avoiding manual DOM manipulation.</p>
<h3 id="applying-effects">
<a name="applying-effects" class="anchor" href="#applying-effects">
<span class="permalink">🔗</span>
</a>
Applying Effects
</h3><p>Apply one or multiple effects in the setup function (for component itself) or in element selector functions:</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:#F8F8F2">() </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // On the component itself</span></span>
<span class="line"><span style="color:#A6E22E"> setAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"open"</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">"isOpen"</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Set "open" attribute according to "isOpen" signal</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F"> // On first element matching ".count"</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">".count"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"count"</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Update text content according to "count" signal</span></span>
<span class="line"><span style="color:#A6E22E"> toggleClass</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"even"</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">"isEven"</span><span style="color:#F8F8F2">) </span><span style="color:#88846F">// Toggle "even" class according to "isEven" signal</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>
<p>Again, the order of effects is not important. Feel free to apply them in any order that suits your needs.</p>
<h3 id="bundled-effects">
<a name="bundled-effects" class="anchor" href="#bundled-effects">
<span class="permalink">🔗</span>
</a>
Bundled Effects
</h3><table>
<thead>
<tr>
<th>Function</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>setText()</code></td>
<td>Updates <strong>text content</strong> with a <code>string</code> signal value (while preserving comment nodes).</td>
</tr>
<tr>
<td><code>setProperty()</code></td>
<td>Updates a given <strong>property</strong> with any signal value.*</td>
</tr>
<tr>
<td><code>setAttribute()</code></td>
<td>Updates a given <strong>attribute</strong> with a <code>string</code> signal value.</td>
</tr>
<tr>
<td><code>toggleAttribute()</code></td>
<td>Toggles a given <strong>boolean attribute</strong> with a <code>boolean</code> signal value.</td>
</tr>
<tr>
<td><code>toggleClass()</code></td>
<td>Toggles a given <strong>CSS class</strong> with a <code>boolean</code> signal value.</td>
</tr>
<tr>
<td><code>setStyle()</code></td>
<td>Updates a given <strong>CSS property</strong> with a <code>string</code> signal value.</td>
</tr>
<tr>
<td><code>dangerouslySetInnerHTML()</code></td>
<td>Sets <strong>HTML content</strong> with a <code>string</code> signal value.</td>
</tr>
<tr>
<td><code>insertOrRemoveElement()</code></td>
<td>Inserts (positive integer) or removes (negative integer) elements with a <code>number</code> signal value.</td>
</tr>
</tbody></table>
<callout-box class="tip">
<p><strong>Tip</strong>: TypeScript will check whether a value of a given type is assignable to a certain element type. You might have to pass a type hint for the queried element type. Prefer <code>setProperty()</code> over <code>setAttribute()</code> for increased type safety. Setting string attributes is possible for all elements, but will have an effect only on some.</p>
</callout-box>
<h3 id="simplifying-effect-notation">
<a name="simplifying-effect-notation" class="anchor" href="#simplifying-effect-notation">
<span class="permalink">🔗</span>
</a>
Simplifying Effect Notation
</h3><p>For effects that take two arguments, <strong>the second argument can be omitted</strong> if the signal key matches the targeted property name, attribute, class, or style property.</p>
<p>When signal key matches property name:</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:#A6E22E">first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">".count"</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">toggleClass</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"even"</span><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>
<p>Here, <code>toggleClass("even")</code> automatically uses the <code>"even"</code> signal.</p>
<h3 id="using-local-signals-for-protected-state">
<a name="using-local-signals-for-protected-state" class="anchor" href="#using-local-signals-for-protected-state">
<span class="permalink">🔗</span>
</a>
Using Local Signals for Protected State
</h3><p>Local signals are useful for storing state that should not be exposed to the outside world. They can be used to manage internal state within a component:</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</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"> count </span><span style="color:#F92672">=</span><span style="color:#A6E22E"> state</span><span style="color:#F8F8F2">(</span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">);</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> double </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> count.</span><span style="color:#A6E22E">map</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">v</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> v </span><span style="color:#F92672">*</span><span style="color:#AE81FF"> 2</span><span style="color:#F8F8F2">);</span></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">".increment"</span><span style="color:#F8F8F2">, </span><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"> count.</span><span style="color:#A6E22E">update</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">v</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F92672"> ++</span><span style="color:#F8F8F2">v);</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 style="color:#E6DB74">".count"</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(count)),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">".double"</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(double))</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>
<p>Outside components cannot access the <code>count</code> or <code>double</code> signals.</p>
<h3 id="using-functions-for-ad-hoc-derived-state">
<a name="using-functions-for-ad-hoc-derived-state" class="anchor" href="#using-functions-for-ad-hoc-derived-state">
<span class="permalink">🔗</span>
</a>
Using Functions for Ad-hoc Derived State
</h3><p>Instead of a signal key or a local signal, you can <strong>pass a function</strong> that derives a value dynamically:</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:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#AE81FF">0</span></span>
<span class="line"><span style="color:#F8F8F2">}, </span><span style="color:#FD971F;font-style:italic">el</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"> double </span><span style="color:#F92672">=</span><span style="color:#A6E22E"> computed</span><span style="color:#F8F8F2">(() </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> el.count </span><span style="color:#F92672">*</span><span style="color:#AE81FF"> 2</span><span style="color:#F8F8F2">);</span></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">".count"</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">toggleClass</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"even"</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> !</span><span style="color:#F8F8F2">(el.count </span><span style="color:#F92672">%</span><span style="color:#AE81FF"> 2</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">".double"</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(() </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#A6E22E"> String</span><span style="color:#F8F8F2">(double.</span><span style="color:#A6E22E">get</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>
<callout-box class="tip">
<p><strong>When to use</strong></p>
<ul>
<li><strong>Use a signal key or a local signal</strong> when the state is part of the component's public interface or internally reused.</li>
<li><strong>Use a function</strong> to <strong>derive a value on the fly</strong> when it is needed only in this one place.</li>
</ul>
<p>Ad-hoc derived state is more efficient than the overhead of a memoized computed signal for simple functions like converting to a string or boolean, formatting a value or performing a calculation.</p>
</callout-box>
<h3 id="efficient-amp-fine-grained-updates">
<a name="efficient-amp-fine-grained-updates" class="anchor" href="#efficient-amp-fine-grained-updates">
<span class="permalink">🔗</span>
</a>
Efficient & Fine-Grained Updates
</h3><p>Unlike some frameworks that <strong>re-render entire components</strong>, UIElement updates only what changes:</p>
<ul>
<li><strong>No virtual DOM</strong> – UIElement modifies the DOM directly.</li>
<li><strong>Signals propagate automatically</strong> – No need to track dependencies manually.</li>
<li><strong>Optimized with a scheduler</strong> – Multiple updates are batched efficiently.</li>
</ul>
</section>
<section>
<h2 id="single-component-example-myslider">
<a name="single-component-example-myslider" class="anchor" href="#single-component-example-myslider">
<span class="permalink">🔗</span>
</a>
Single Component Example: MySlider
</h2><p>Bringing all of the above together, you are now ready to build your own components like this slider with prev / next buttons and dot indicators, demonstrating single-component reactivity.</p>
<component-demo>
<div class="preview">
<my-slider>
<h2 class="visually-hidden">Slides</h2>
<div class="slides">
<div class="slide active" data-index="0">
<h3>Slide 1</h3>
<hello-world>
<label>Your name<br>
<input type="text">
</label>
<p>Hello, <span>World</span>!</p>
</hello-world>
</div>
<div class="slide" data-index="1">
<h3>Slide 2</h3>
<calc-table rows="3" columns="3">
<div class="rows">
<p>Number of rows:</p>
<spin-button value="3" zero-label="Add Row" increment-label="Increment">
<button type="button" class="decrement" aria-label="Decrement">
−
</button>
<p class="value">3</p>
<button type="button" class="increment" aria-label="Increment">
+
</button>
</spin-button>
</div>
<div class="columns">
<p>Number of columns:</p>
<spin-button
value="3"
zero-label="Add Column"
increment-label="Increment"
>
<button type="button" class="decrement" aria-label="Decrement">
−
</button>
<p class="value">3</p>
<button type="button" class="increment" aria-label="Increment">
+
</button>
</spin-button>
</div>
<table>
<thead>
<tr>
<th scope="col">Row</th>
</tr>
<