UNPKG

@zeix/ui-element

Version:

UIElement - minimal reactive framework based on Web Components

722 lines (655 loc) β€’ 51 kB
<!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"> <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" 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>πŸ—οΈ 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 a framework.</p> </section> <section> <h2>Anatomy of a UIElement 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> <h3>Defining a Component</h3> <p>A UIElement component is created by extending <code>UIElement</code>:</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">MyComponent</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:#88846F"> /* component definition */</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> <h3>Registering a Custom Element</h3> <p>Every UIElement component must be registered with a valid custom tag name (two or more words joined with <code>-</code>) using <code>.define()</code>.</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">MyComponent.</span><span style="color:#A6E22E">define</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'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> <callout-box class="tip"> <p><strong>Alternative</strong>: If you prefer you can also declare the custom element tag within the component and call <code>.define()</code> without arguments.</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">MyComponent</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:#F92672"> static</span><span style="color:#F8F8F2"> localName </span><span style="color:#F92672">=</span><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F8F8F2">;</span></span> <span class="line"><span style="color:#88846F"> /* component definition */</span></span> <span class="line"><span style="color:#F8F8F2">}</span></span> <span class="line"><span style="color:#F8F8F2">MyComponent.</span><span style="color:#A6E22E">define</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> </callout-box> <h3>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">&#x3C;</span><span style="color:#F92672">my-component</span><span style="color:#F8F8F2">>Content goes here&#x3C;/</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>Web Component Lifecycle in UIElement</h2> <p>Every UIElement component follows a <strong>lifecycle</strong> from creation to removal. Here&#39;s how the key lifecycle methods work:</p> <h3>Component Creation (constructor())</h3> <p>Runs when the element is created <strong>but before it&#39;s attached to the DOM</strong>. Avoid accessing attributes or child elements here.</p> <h3>Mounted in the DOM (connectedCallback())</h3> <p>Runs when the component is added to the page. This is where you:</p> <ul> <li>βœ… <strong>Initialize state</strong></li> <li>βœ… <strong>Set up event listeners</strong></li> <li>βœ… <strong>Apply effects</strong></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:#66D9EF;font-style:italic">class</span><span> </span><span style="color:#A6E22E;text-decoration:underline">MyComponent</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">'.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 style="color:#88846F">// Add click event listener</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</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:#AE81FF"> null</span><span style="color:#F92672"> !=</span><span style="color:#F8F8F2"> v </span><span style="color:#F92672">?</span><span style="color:#F92672"> ++</span><span style="color:#F8F8F2">v </span><span style="color:#F92672">:</span><span style="color:#AE81FF"> 1</span><span style="color:#F8F8F2">);</span></span> <span class="line"><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">'.count'</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">'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> </code-block> <p>If your component initializes states from <code>states</code> or provides or consumes context (<code>static providedContexts</code> / <code>static consumedContexts</code>), you need to call <code>super.connectedCallback()</code>.</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">class</span><span> </span><span style="color:#A6E22E;text-decoration:underline">HelloUser</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:#F92672"> static</span><span style="color:#F8F8F2"> consumedContexts </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> [</span><span style="color:#E6DB74">'display-name'</span><span style="color:#F8F8F2">]; </span><span style="color:#88846F">// Signal provided by a parent component</span></span> <span class="line"></span> <span class="line"><span style="color:#F8F8F2"> init </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> {</span></span> <span class="line"><span style="color:#F8F8F2"> greeting: </span><span style="color:#E6DB74">'Hello'</span><span style="color:#F8F8F2">, </span><span style="color:#88846F">// Initial value of 'greeting' signal</span></span> <span class="line"><span style="color:#A6E22E"> upper</span><span style="color:#F8F8F2">: () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'display-name'</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">toUpperCase</span><span style="color:#F8F8F2">(), </span><span style="color:#88846F">// Compute function for transformation on 'display-name' signal</span></span> <span class="line"><span style="color:#F8F8F2"> }</span></span> <span class="line"></span> <span class="line"><span style="color:#A6E22E"> connectedCallback</span><span style="color:#F8F8F2">() {</span></span> <span class="line"><span style="color:#FD971F"> super</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">connectedCallback</span><span style="color:#F8F8F2">(); </span><span style="color:#88846F">// Initializes state signals from values, attributes, context or creates computed signals from functions </span></span> <span class="line"></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">'.greeting'</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">'greeting'</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">'.user'</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">'display-name'</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">'.profile h2'</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">'upper'</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>Removed from the DOM (disconnectedCallback())</h3> <p>Runs when the component is removed. Event listeners bound with <code>.on()</code> are automatically removed by UIElement.</p> <p>If you added <strong>event listeners</strong> outside the scope of your component or <strong>external subscriptions</strong>, you need to manually clean up.</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">class</span><span> </span><span style="color:#A6E22E;text-decoration:underline">MyComponent</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> <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">.intersectionObserver </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 style="color:#A6E22E">observe</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F">this</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:#A6E22E"> disconnentedCallback</span><span style="color:#F8F8F2">() {</span></span> <span class="line"><span style="color:#FD971F"> super</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">disconnectedCallback</span><span style="color:#F8F8F2">(); </span><span style="color:#88846F">// Automatically removes event listeners bound with `.on()`</span></span> <span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> (</span><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.intersectionObserver) </span><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.intersectionObserver.</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> <p>Use this to clean up <strong>event listeners or external subscriptions</strong>.</p> <h3>Observed Attributes (attributeChangedCallback())</h3> <p>UIElement <strong>automatically converts attributes to reactive signals</strong>. Usually, you don’t need to override this method manually.</p> </section> <section> <h2>State Management with UIElement</h2> <p>UIElement manages state using <strong>signals</strong>, which are reactive values that trigger updates when they change. We use a familiar <code>Map</code>-like API:</p> <h3>Defining &amp; Using Signals</h3> <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">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">, </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">); </span><span style="color:#88846F">// Create a state signal</span></span> <span class="line"><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'isEven'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> !</span><span style="color:#F8F8F2">((</span><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">??</span><span style="color:#AE81FF"> 0</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">%</span><span style="color:#AE81FF"> 2</span><span style="color:#F8F8F2">)); </span><span style="color:#88846F">// Create a derived signal</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>Checking &amp; Removing Signals</h3> <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:#F92672">if</span><span style="color:#F8F8F2"> (</span><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">has</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">)) { </span><span style="color:#88846F">/* Do something */</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">delete</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">); </span><span style="color:#88846F">// Removes the signal and its dependencies</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>Characteristics and Special Values</h3> <p>Signals in UIElement are of a <strong>fixed 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>Why Signals with a Map Interface?</h3> <p>UIElement <strong>uses signals</strong> instead of standard properties or attributes because it <strong>ensures reactivity, loose coupling, and avoids common pitfalls with the DOM API</strong>.</p> <ul> <li>βœ… <strong>Signals enable loose coupling between components</strong>: A component that modifies state doesn’t need to know which or how many elements depend on that state. Any UI updates happen automatically wherever that signal is used.</li> <li>βœ… <strong>Signals trigger automatic updates</strong>: Any DOM element or effect that depends on a signal updates itself when the signal changes. The source doesn&#39;t need to know how the updated state should change the DOM.</li> <li>βœ… <strong>Standard JavaScript properties are not reactive</strong>: JavaScript properties don’t automatically trigger updates when changed. The distinct <code>Map</code>-like interface avoids confusion.</li> <li>βœ… <strong>Attributes can only store strings</strong>: Attributes in HTML are always strings. If you store numbers, booleans, or objects, you must manually convert them between string format and usable values. Signals avoid this extra conversion step.</li> <li>βœ… <strong>The Map interface avoids name conflicts</strong>:<ul> <li>The <code>HTMLElement</code> <strong>namespace is crowded</strong>, meaning using direct properties can accidentally override existing methods or properties.</li> <li>HTML attributes are <strong>kebab-case</strong> (<code>data-user-id</code>), but JavaScript properties are <strong>camelCase</strong> (<code>dataUserId</code>), which can cause inconsistencies.</li> <li>With a Map, we can <strong>use attributes names directly</strong> as state keys (e.g., <code>&quot;count&quot;</code> or <code>&quot;is-active&quot;</code>) without conversion or worrying about naming conflicts.</li> </ul> </li> </ul> </section> <section> <h2>Initializing State from Attributes</h2> <h3>Declaring Observed Attributes</h3> <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">static observedAttributes </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> [</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">]; </span><span style="color:#88846F">// Automatically becomes a signal</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>Parsing Attribute Values</h3> <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">init </span><span style="color:#F92672">=</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">// 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">v</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> <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>&#39;&#39;</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>Pre-defined Parsers in UIElement</h3> <table> <thead> <tr> <th>Function</th> <th>Description</th> </tr> </thead> <tbody><tr> <td><code>asBoolean</code></td> <td>Converts <code>&quot;true&quot;</code> / <code>&quot;false&quot;</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>&quot;42&quot;</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>&quot;3.14&quot;</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([&#39;small&#39;, &#39;medium&#39;, &#39;large&#39;])</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>&#39;[&quot;a&quot;, &quot;b&quot;, &quot;c&quot;]&#39;</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&#39;t match the type you&#39;re expecting.</p> </section> <section> <h2>Accessing Sub-elements within the Component</h2> <p>Before adding <strong>event listeners</strong>, <strong>applying effects</strong>, or <strong>passing states</strong>, you need to select elements inside the component.</p> <p>UIElement provides the following methods for <strong>element selection</strong>:</p> <table> <thead> <tr> <th>Method</th> <th>Description</th> </tr> </thead> <tbody><tr> <td><code>this.self</code></td> <td>Selects <strong>the component itself</strong>.</td> </tr> <tr> <td><code>this.first(selector)</code></td> <td>Selects <strong>the first matching element</strong> inside the component.</td> </tr> <tr> <td><code>this.all(selector)</code></td> <td>Selects <strong>all matching elements</strong> inside the component.</td> </tr> </tbody></table> <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:#88846F">// Select the component itself</span></span> <span class="line"><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.self.</span><span style="color:#A6E22E">sync</span><span style="color:#F8F8F2">(</span><span style="color:#A6E22E">setProperty</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'hidden'</span><span style="color:#F8F8F2">));</span></span> <span class="line"></span> <span class="line"><span style="color:#88846F">// Select the first '.increment' button &#x26; add a click event</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">'.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:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</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:#AE81FF"> null</span><span style="color:#F92672"> !=</span><span style="color:#F8F8F2"> v </span><span style="color:#F92672">?</span><span style="color:#F92672"> ++</span><span style="color:#F8F8F2">v </span><span style="color:#F92672">:</span><span style="color:#AE81FF"> 1</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:#88846F">// Select all &#x3C;button> elements &#x26; sync their 'disabled' properties</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">'button'</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">sync</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 style="color:#E6DB74">'hidden'</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> <button type="button" class="overlay">Expand</button> </code-block> </section> <section> <h2>Updating State with Events</h2> <p>User interactions should <strong>update signals</strong>, not the DOM directly. This keeps the components loosly coupled.</p> <p>Bind event handlers to one or many elements using the <code>.on()</code> method:</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">.</span><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:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</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:#AE81FF"> null</span><span style="color:#F92672"> !=</span><span style="color:#F8F8F2"> v </span><span style="color:#F92672">?</span><span style="color:#F8F8F2"> v</span><span style="color:#F92672">++</span><span style="color:#F92672"> :</span><span style="color:#AE81FF"> 1</span><span style="color:#F8F8F2">)</span></span> <span class="line"><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">'input'</span><span style="color:#F8F8F2">).</span><span style="color:#A6E22E">on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'input'</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">set</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'name'</span><span style="color:#F8F8F2">, e.target.value </span><span style="color:#F92672">||</span><span style="color:#AE81FF"> undefined</span><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> </section> <section> <h2>Synchronizing State with Effects</h2> <p>Effects <strong>automatically update the DOM</strong> when signals change, avoiding manual DOM manipulation.</p> <h3>Applying Effects with .sync()</h3> <p>Apply one or multiple effects to elements using <code>.sync()</code>:</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">.</span><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">sync</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></code></pre> <input-button class="copy"> <button type="button" class="secondary small"> <span class="label">Copy</span> </button> </input-button> </code-block> <h3>Pre-defined Effects in UIElement</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>createElement()</code></td> <td>Inserts a <strong>new element</strong> with a given tag name with a <code>Record&lt;string, string&gt;</code> signal value for attributes.</td> </tr> <tr> <td><code>removeElement()</code></td> <td>Removes an element if the <code>boolean</code> signal value is <code>true</code>.</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 specify 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>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:#FD971F">this</span><span style="color:#F8F8F2">.</span><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">sync</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(&#39;even&#39;)</code> automatically uses the <code>&quot;even&quot;</code> signal.</p> <h3>Using Functions for Ad-hoc Derived State</h3> <p>Instead of a signal key, 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:#FD971F">this</span><span style="color:#F8F8F2">.</span><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">sync</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">((</span><span style="color:#FD971F">this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">??</span><span style="color:#AE81FF"> 0</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">%</span><span style="color:#AE81FF"> 2</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> <callout-box class="tip"> <p><strong>When to use</strong></p> <ul> <li><strong>Use a signal key</strong> when the state is already <strong>stored as a signal</strong>.</li> <li><strong>Use a function</strong> when you <strong>derive a value on the fly</strong> needed only in this one place and you don&#39;t want to expose it as a signal on the element.</li> </ul> </callout-box> <h3>Custom Effects</h3> <p>For complex DOM manipulations, <strong>define your own effect</strong> using <code>effect()</code>.</p> <p>Here&#39;s an example effect that attaches a Shadow DOM and updates its content:</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:#88846F">// Update the shadow DOM when content changes</span></span> <span class="line"><span style="color:#A6E22E">effect</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"> content </span><span style="color:#F92672">=</span><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">get</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'content'</span><span style="color:#F8F8F2">)</span></span> <span class="line"><span style="color:#F92672"> if</span><span style="color:#F8F8F2"> (content) {</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.root </span><span style="color:#F92672">=</span><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.shadowRoot </span><span style="color:#F92672">||</span><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.</span><span style="color:#A6E22E">attachShadow</span><span style="color:#F8F8F2">({ mode: </span><span style="color:#E6DB74">'open'</span><span style="color:#F8F8F2"> })</span></span> <span class="line"><span style="color:#FD971F"> this</span><span style="color:#F8F8F2">.root.innerHTML </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> content</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> <h3>Efficient &amp; 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> <p><strong>In practical terms</strong>: UIElement is as easy as React but without re-renders.</p> </section> <section> <h2>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"> <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"> <h3>Slide 2</h3> <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 primary">Add to Cart</button> </spin-button> </div> <div class="slide"> <h3>Slide 3</h3> <rating-feedback> <form> <rating-stars> <fieldset> <legend class="visually-hidden">Rate</legend> <label> <input type="radio" class="visually-hidden" name="rating" value="1"> <span class="label">β˜†</span> </label> <label> <input type="radio" class="visually-hidden" name="rating" value="2"> <span class="label">β˜†</span> </label> <label> <input type="radio" class="visually-hidden" name="rating" value="3"> <span class="label">β˜†</span> </label> <label> <input type="radio" class="visually-hidden" name="rating" value="4"> <span class="label">β˜†</span> </label> <label> <input type="radio" class="visually-hidden" name="rating" value="5"> <span class="label">β˜†</span> </label> </fieldset> </rating-stars> <div class="feedback" hidden> <header> <button button="button" class="hide" aria-label="Hide">Γ—</button> <p hidden>We're sorry to hear that! Your feedback is important, and we'd love to improve. Let us know how we can do better.</p> <p hidden>Thank you for your honesty. We appreciate your feedback and will work on making things better.</p> <p hidden>Thanks for your rating! If there's anything we can improve, we'd love to hear your thoughts.</p> <p hidden>We're glad you had a good experience! If there's anything that could make it even better, let us know.</p> <p hidden>Thank you for your support! We're thrilled you had a great experience. Your feedback keeps us motivated!</p> </header> <fieldset> <label for="rating-feedback">Describe your experience (optional)</label> <textarea id="rating-feedback"></textarea> <input-button disabled> <button type="submit" class="primary" disabled>Submit</button> </input-button> </fieldset> </div> </form> </rating-feedback> </div> </div> <button type="button" class="prev" aria-label="Previous">β€Ή<