@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
722 lines (655 loc) β’ 51 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">
<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"><</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>Web Component Lifecycle in UIElement</h2>
<p>Every UIElement component follows a <strong>lifecycle</strong> from creation to removal. Here's how the key lifecycle methods work:</p>
<h3>Component Creation (constructor())</h3>
<p>Runs when the element is created <strong>but before it'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 & 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 & 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'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>"count"</code> or <code>"is-active"</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>''</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>"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>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 & 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 <button> elements & 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<string, string></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('even')</code> automatically uses the <code>"even"</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'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'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 & 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">βΉ<