@zeix/ui-element
Version:
UIElement - a HTML-first library for reactive Web Components
679 lines (634 loc) • 65.7 kB
HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Components – UIElement Docs</title>
<meta name="description" content="Anatomy, lifecycle, signals, effects" />
<link
rel="preload"
href="./assets/main.css?v=452f3291"
as="style"
/>
<link
rel="modulepreload"
href="./assets/main.js?v=4387827c"
/>
<link
rel="stylesheet"
href="./assets/main.css?v=452f3291"
/>
<script
type="module"
src="./assets/main.js?v=4387827c"
></script>
</head>
<body class="">
<context-router>
<header class="content-grid">
<a href="#main" class="skiplink visually-hidden">
Skip to main content
</a>
<h1 class="content">
UIElement Docs <small>Version 0.14.0</small>
</h1>
<section-menu>
<nav>
<h2 class="visually-hidden">Main Menu</h2>
<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="components.html">
<span class="icon">🏗️</span>
<strong>Components</strong>
<small>Anatomy, lifecycle, signals, effects</small>
</a>
</li>
<li>
<a href="styling.html">
<span class="icon">🎨</span>
<strong>Styling</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="examples.html">
<span class="icon">🍽️</span>
<strong>Examples</strong>
<small>Common use cases and demos</small>
</a>
</li>
<li>
<a href="blog.html">
<span class="icon">📜</span>
<strong>Blog</strong>
<small>Latest articles and updates</small>
</a>
</li>
<li>
<a href="api.html">
<span class="icon">📚</span>
<strong>API Reference</strong>
<small>Functions, types, and constants</small>
</a>
</li>
<li>
<a href="about.html">
<span class="icon">🤝</span>
<strong>About</strong>
<small>License, versioning, getting involved</small>
</a>
</li>
</ol>
</nav>
</section-menu>
<card-callout class="content danger" hidden>
<p class="error" role="alert" aria-live="polite"></p>
</card-callout>
</header>
<main id="main" class="content-grid"><section-hero>
<h1 id="components">
<a name="components" class="anchor" href="#components">
<span class="permalink">🔗</span>
<span class="title">🏗️ Components</span>
</a>
</h1>
<div>
<p class="lead"><strong>Create lightweight, self-contained Web Components with built-in reactivity</strong>. UIElement lets you define custom elements that manage state efficiently, update the DOM automatically, and enhance server-rendered pages without an SPA framework.</p>
<module-toc>
<nav>
<h2>In This Page</h2>
<ol>
<li><a href="#defining-a-component">Defining a Component</a></li>
<li><a href="#component-lifecycle">Component Lifecycle</a></li>
<li><a href="#managing-state-with-signals">Managing State with Signals</a></li>
<li><a href="#selecting-elements">Selecting Elements</a></li>
<li><a href="#adding-event-listeners">Adding Event Listeners</a></li>
<li><a href="#synchronizing-state-with-effects">Synchronizing State with Effects</a></li>
<li><a href="#next-steps">Next Steps</a></li>
</ol>
</nav>
</module-toc>
</div>
</section-hero>
<section>
<h2 id="defining-a-component">
<a name="defining-a-component" class="anchor" href="#defining-a-component">
<span class="permalink">🔗</span>
<span class="title">Defining a Component</span>
</a>
</h2>
<p>UIElement builds on <strong>Web Components</strong>, extending <code>HTMLElement</code> to provide <strong>built-in state management and reactive updates</strong>.</p>
<p>UIElement creates components using the <code>component()</code> function:</p>
<module-codeblock language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'my-component'</span><span style="color:#F8F8F2">, {}, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Component setup</span></span>
<span class="line"><span style="color:#F8F8F2">])</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
</module-codeblock>
<p>Every UIElement component must be registered with a valid custom element tag name (two or more words joined with <code>-</code>) as the first parameter.</p>
<h3 id="using-the-custom-element-in-html">
<a name="using-the-custom-element-in-html" class="anchor" href="#using-the-custom-element-in-html">
<span class="permalink">🔗</span>
<span class="title">Using the Custom Element in HTML</span>
</a>
</h3>
<p>Once registered, the component can be used like any native HTML element:</p>
<module-codeblock 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>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
</module-codeblock>
<h3 id="anatomy-of-a-component">
<a name="anatomy-of-a-component" class="anchor" href="#anatomy-of-a-component">
<span class="permalink">🔗</span>
<span class="title">Anatomy of a Component</span>
</a>
</h3>
<p>Let's examine a complete component example to understand how UIElement works:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'hello-world'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> name: </span><span style="color:#A6E22E">asString</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> el.</span><span style="color:#A6E22E">querySelector</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'span'</span><span style="color:#F8F8F2">)?.textContent?.</span><span style="color:#A6E22E">trim</span><span style="color:#F8F8F2">() </span><span style="color:#F92672">??</span><span style="color:#E6DB74"> ''</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 style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</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"> fallback </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> el.name</span></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'input'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'input'</span><span style="color:#F8F8F2">, ({ </span><span style="color:#FD971F;font-style:italic">target</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> ({ name: target.value </span><span style="color:#F92672">||</span><span style="color:#F8F8F2"> fallback })),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'span'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'name'</span><span style="color:#F8F8F2">)),</span></span>
<span class="line"><span style="color:#F8F8F2"> ]</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<h4 id="reactive-properties">
<a name="reactive-properties" class="anchor" href="#reactive-properties">
<span class="permalink">🔗</span>
<span class="title">Reactive Properties</span>
</a>
</h4>
<module-codeblock language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#F8F8F2">{</span></span>
<span class="line"><span style="color:#88846F"> // Create "name" property from attribute "name" as a string, falling back to server-rendered content</span></span>
<span class="line"><span style="color:#F8F8F2"> name: </span><span style="color:#A6E22E">asString</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> el.</span><span style="color:#A6E22E">querySelector</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'span'</span><span style="color:#F8F8F2">)?.textContent?.</span><span style="color:#A6E22E">trim</span><span style="color:#F8F8F2">() </span><span style="color:#F92672">??</span><span style="color:#E6DB74"> ''</span><span style="color:#F8F8F2">),</span></span>
<span class="line"><span style="color:#F8F8F2">}</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
</module-codeblock>
<p>This creates a reactive property called <code>name</code>:</p>
<ul>
<li><code>asString()</code> observes the attribute <code>name</code> and assigns its value as a string to the <code>name</code> property</li>
<li><code>el => ...</code> is an instruction how to get the fallback value in the DOM if there is no name attribute</li>
<li>UIElement automatically reads "World" from the <code><span></code> element as the initial value</li>
<li>When <code>name</code> changes, any effects that depend on it automatically update</li>
</ul>
<h4 id="setup-function">
<a name="setup-function" class="anchor" href="#setup-function">
<span class="permalink">🔗</span>
<span class="title">Setup Function</span>
</a>
</h4>
<p>The setup function takes two arguments:</p>
<ol>
<li>The component element. In this example we name it <code>el</code>.</li>
<li>Helper functions for accessing descendant elements. In this example we use <code>first</code> to find the first descendant matching a selector and apply effects to it.</li>
</ol>
<p>The setup function returns an array of effects:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> // set the fallback value we want to use instead of an empty string</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> fallback </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> el.name</span></span>
<span class="line"></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Handle user input to change the "name" property</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'input'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'input'</span><span style="color:#F8F8F2">, ({ </span><span style="color:#FD971F;font-style:italic">target</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> ({ name: target.value </span><span style="color:#F92672">||</span><span style="color:#F8F8F2"> fallback })),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F"> // Update content when the "name" property changes</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'span'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'name'</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>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>Effects define <strong>component behaviors</strong>:</p>
<ul>
<li><code>first('input', on('input', ...))</code> finds the first <code><input></code> and adds an event listener</li>
<li><code>first('span', setText('name'))</code> finds the first <code><span></code> and keeps its text in sync with the <code>name</code> property</li>
</ul>
<p>Characteristics of Effects:</p>
<ul>
<li>Effects run when the component is added to the page</li>
<li>Effects rerun when their dependencies change</li>
<li>Effects may return a cleanup function to be executed when the target element or the component is removed from the page</li>
</ul>
</section>
<section>
<h2 id="component-lifecycle">
<a name="component-lifecycle" class="anchor" href="#component-lifecycle">
<span class="permalink">🔗</span>
<span class="title">Component Lifecycle</span>
</a>
</h2>
<p>UIElement manages the <strong>Web Component lifecycle</strong> from creation to removal. Here's what happens.</p>
<h3 id="component-creation">
<a name="component-creation" class="anchor" href="#component-creation">
<span class="permalink">🔗</span>
<span class="title">Component Creation</span>
</a>
</h3>
<p>In the <code>constructor()</code> reactive properties are initialized. You pass a second argument to the <code>component()</code> function to defines initial values for <strong>component states</strong>.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">, </span><span style="color:#88846F">// Initial value of "count" signal</span></span>
<span class="line"><span style="color:#F8F8F2"> value: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(</span><span style="color:#AE81FF">5</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Parse "value" attribute as integer defaulting to 5</span></span>
<span class="line"><span style="color:#A6E22E"> isEven</span><span style="color:#F8F8F2">: </span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> !</span><span style="color:#F8F8F2">(el.count </span><span style="color:#F92672">%</span><span style="color:#AE81FF"> 2</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Computed signal based on "count" signal</span></span>
<span class="line"><span style="color:#F8F8F2"> name: </span><span style="color:#A6E22E">fromContext</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'display-name'</span><span style="color:#F8F8F2">, </span><span style="color:#E6DB74">'World'</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Consume "display-name" signal from closest context provider</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Component setup</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>In this example you see all three ways to define a reactive property:</p>
<ul>
<li>A <strong>static initial value</strong> for a <code>State</code> signal (e.g., <code>count: 0</code>)</li>
<li>An <strong>attribute parser</strong> that may provide a fallback value (e.g., <code>value: asInteger(5)</code>)</li>
<li>A <strong>signal producer</strong> function that derives an initial value or a callback function from other properties of the element (e.g., <code>isEven: el => () => !(el.count % 2)</code>) or bundled signal producers (e.g. <code>fromContext(context, fallback)</code>)</li>
</ul>
<card-callout class="caution">
<p><strong>Caution</strong>: Property initialization runs <strong>before the element is attached to the DOM</strong>. You can't access not yet defined properties or descendant elements here.</p>
</card-callout>
<h3 id="mounted-in-the-dom">
<a name="mounted-in-the-dom" class="anchor" href="#mounted-in-the-dom">
<span class="permalink">🔗</span>
<span class="title">Mounted in the DOM</span>
</a>
</h3>
<p>Runs when the component is added to the page (<code>connectedCallback()</code>). This is where you:</p>
<ul>
<li><strong>Access sub-elements</strong></li>
<li><strong>Set up event listeners</strong></li>
<li><strong>Apply effects</strong></li>
<li><strong>Emit custom events</strong></li>
<li><strong>Provide context</strong></li>
</ul>
<p>The setup function has two arguments:</p>
<ol>
<li><code>el</code>: The component element instance.</li>
<li><code>{ all, first }</code>: An object containing two functions, <code>all</code> and <code>first</code>, which can be used to select elements within the component. See <a href="#accessing-sub-elements">Accessing Sub-elements</a>.</li>
</ol>
<p>UIElement expects you to return an array of partially applied functions to be executed during the setup phase. The order doesn't matter, as each function targets a specific element or event. So feel free to organize your code in a way that makes sense to you.</p>
<p>Each of these functions will return a cleanup function that will be executed during the <code>disconnectedCallback()</code> lifecycle method.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> emit</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'update-count'</span><span style="color:#F8F8F2">, el.count), </span><span style="color:#88846F">// Emit custom event</span></span>
<span class="line"><span style="color:#A6E22E"> provide</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Provide context</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.increment'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> el.count</span><span style="color:#F92672">++</span></span>
<span class="line"><span style="color:#F8F8F2"> }), </span><span style="color:#88846F">// Add click event listener</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '.count'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">), </span><span style="color:#88846F">// Apply effect to update text</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<h3 id="removed-from-the-dom">
<a name="removed-from-the-dom" class="anchor" href="#removed-from-the-dom">
<span class="permalink">🔗</span>
<span class="title">Removed from the DOM</span>
</a>
</h3>
<p>Runs when the component is removed (<code>disconnectedCallback()</code>). UIElement will run all cleanup functions returned by event listeners and effects during the setup phase (<code>connectedCallback()</code>). This will unsubscribe all signals the component is subscribed to, so you don't need to worry about memory leaks.</p>
<p>If you added <strong>event listeners</strong> outside the scope of your component or <strong>subscribed manually to external APIs</strong>, you need to return a cleanup function:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'my-component'</span><span style="color:#F8F8F2">, {}, </span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Setup logic</span></span>
<span class="line"><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> observer </span><span style="color:#F92672">=</span><span style="color:#F92672"> new</span><span style="color:#A6E22E"> IntersectionObserver</span><span style="color:#F8F8F2">(([</span><span style="color:#FD971F;font-style:italic">entry</span><span style="color:#F8F8F2">]) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> // Do something</span></span>
<span class="line"><span style="color:#F8F8F2"> })</span></span>
<span class="line"><span style="color:#F8F8F2"> observer.</span><span style="color:#A6E22E">observe</span><span style="color:#F8F8F2">(el)</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F"> // Cleanup logic</span></span>
<span class="line"><span style="color:#F92672"> return</span><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> observer.</span><span style="color:#A6E22E">disconnect</span><span style="color:#F8F8F2">()</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2">])</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<h3 id="observed-attributes">
<a name="observed-attributes" class="anchor" href="#observed-attributes">
<span class="permalink">🔗</span>
<span class="title">Observed Attributes</span>
</a>
</h3>
<p>UIElement automatically observes and converts attributes with an associated <strong>parser function</strong> in the init block and updates them whenever the attribute changes (<code>attributeChangedCallback()</code>).</p>
</section>
<section>
<h2 id="managing-state-with-signals">
<a name="managing-state-with-signals" class="anchor" href="#managing-state-with-signals">
<span class="permalink">🔗</span>
<span class="title">Managing State with Signals</span>
</a>
</h2>
<p>UIElement manages state using <strong>signals</strong>, which are atomic reactive states that trigger updates when they change. We use regular properties to access or update them:</p>
<module-codeblock language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#F8F8F2">console.</span><span style="color:#A6E22E">log</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F92672"> in</span><span style="color:#F8F8F2"> el) </span><span style="color:#88846F">// Check if the signal exists</span></span>
<span class="line"><span style="color:#F8F8F2">console.</span><span style="color:#A6E22E">log</span><span style="color:#F8F8F2">(el.count) </span><span style="color:#88846F">// Read the signal value</span></span>
<span class="line"><span style="color:#F8F8F2">el.count </span><span style="color:#F92672">=</span><span style="color:#AE81FF"> 42</span><span style="color:#88846F"> // Update the signal value</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
</module-codeblock>
<h3 id="characteristics-and-special-values">
<a name="characteristics-and-special-values" class="anchor" href="#characteristics-and-special-values">
<span class="permalink">🔗</span>
<span class="title">Characteristics and Special Values</span>
</a>
</h3>
<p>Signals in UIElement are of a <strong>static type</strong> and <strong>non-nullable</strong>. This allows to <strong>simplify the logic</strong> as you will never have to check the type or perform null-checks.</p>
<ul>
<li>If you use <strong>TypeScript</strong> (recommended), <strong>you will be warned</strong> that <code>null</code> or <code>undefined</code> cannot be assigned to a signal or if you try to assign a value of a wrong type.</li>
<li>If you use vanilla <strong>JavaScript</strong> without a build step, setting a signal to <code>null</code> or <code>undefined</code> <strong>will log an error to the console and abort</strong>. However, strict type checking is not enforced at runtime.</li>
</ul>
<p>Because of the <strong>non-nullable nature of signals</strong> in UIElement, we need two special values that can be assigned to any signal type:</p>
<ul>
<li><strong><code>RESET</code></strong>: Will <strong>reset to the server-rendered version</strong> that was there before UIElement took control. This is what you want to do most of the times when a signal lacks a specific value.</li>
<li><strong><code>UNSET</code></strong>: Will <strong>delete the signal</strong>, <strong>unsubscribe its watchers</strong> and also <strong>delete related attributes or style properties</strong> in effects. Use this with special care!</li>
</ul>
<h3 id="initializing-state-from-attributes">
<a name="initializing-state-from-attributes" class="anchor" href="#initializing-state-from-attributes">
<span class="permalink">🔗</span>
<span class="title">Initializing State from Attributes</span>
</a>
</h3>
<p>The standard way to set initial state in UIElement is via <strong>server-rendered attributes</strong> on the component that needs it. No props drilling as in other frameworks. UIElements provides some bundled attribute parsers to convert attribute values to the desired type. And you can also define your own custom parsers.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'my-component'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> count: </span><span style="color:#A6E22E">asInteger</span><span style="color:#F8F8F2">(), </span><span style="color:#88846F">// Bundled parser: Convert '42' -> 42</span></span>
<span class="line"><span style="color:#A6E22E"> date</span><span style="color:#F8F8F2">: (</span><span style="color:#FD971F;font-style:italic">_</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">v</span><span style="color:#F8F8F2">) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F92672"> new</span><span style="color:#A6E22E"> Date</span><span style="color:#F8F8F2">(v), </span><span style="color:#88846F">// Custom parser: '2025-02-14' -> Date object</span></span>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#88846F"> // Component setup</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<card-callout 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>
</card-callout>
<h3 id="bundled-attribute-parsers">
<a name="bundled-attribute-parsers" class="anchor" href="#bundled-attribute-parsers">
<span class="permalink">🔗</span>
<span class="title">Bundled Attribute Parsers</span>
</a>
</h3>
<p>UIElement provides several built-in parsers for common attribute types. See the <a href="api.html#parsers">Parsers section</a> in the API reference for detailed descriptions and usage examples.</p>
</section>
<section>
<h2 id="selecting-elements">
<a name="selecting-elements" class="anchor" href="#selecting-elements">
<span class="permalink">🔗</span>
<span class="title">Selecting Elements</span>
</a>
</h2>
<p>Use the provided selector utilities to find elements within your component:</p>
<h3 id="first">
<a name="first" class="anchor" href="#first">
<span class="permalink">🔗</span>
<span class="title">first()</span>
</a>
</h3>
<p>Selects the first matching element and applies effects:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'basic-counter'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><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>
<span class="line"><span style="color:#F8F8F2"> },</span></span>
<span class="line"><span style="color:#F8F8F2"> (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'.count'</span><span style="color:#F8F8F2">, </span><span style="color:#A6E22E">setText</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'count'</span><span style="color:#F8F8F2">)),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'button'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, () </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> el.count</span><span style="color:#F92672">++</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<h3 id="all">
<a name="all" class="anchor" href="#all">
<span class="permalink">🔗</span>
<span class="title">all()</span>
</a>
</h3>
<p>Selects all matching elements and applies effects to each:</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'module-tabgroup'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> selected: </span><span style="color:#E6DB74">''</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 style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">all</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"> // Apply click handler to all buttons</span></span>
<span class="line"><span style="color:#A6E22E"> all</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '[role="tab"]'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'click'</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">e</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#F8F8F2"> el.selected </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> e.currentTarget?.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'aria-controls'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">??</span><span style="color:#E6DB74"> ''</span></span>
<span class="line"><span style="color:#F8F8F2"> }),</span></span>
<span class="line"><span style="color:#A6E22E"> setProperty</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> 'ariaSelected'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#FD971F;font-style:italic"> target</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> target?.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'aria-controls'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">===</span><span style="color:#F8F8F2"> el.selected,</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> setProperty</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'tabIndex'</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">target</span><span style="color:#66D9EF;font-style:italic"> =></span></span>
<span class="line"><span style="color:#F8F8F2"> target?.</span><span style="color:#A6E22E">getAttribute</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">'aria-controls'</span><span style="color:#F8F8F2">) </span><span style="color:#F92672">===</span><span style="color:#F8F8F2"> el.selected </span><span style="color:#F92672">?</span><span style="color:#AE81FF"> 0</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:#F8F8F2"> ),</span></span>
<span class="line"></span>
<span class="line"><span style="color:#88846F"> // Apply hidden property to all tabs</span></span>
<span class="line"><span style="color:#A6E22E"> all</span><span style="color:#F8F8F2">(</span></span>
<span class="line"><span style="color:#E6DB74"> '[role="tabpanel"]'</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> show</span><span style="color:#F8F8F2">(</span><span style="color:#FD971F;font-style:italic">target</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> el.selected </span><span style="color:#F92672">===</span><span style="color:#F8F8F2"> target.id),</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#F8F8F2"> ],</span></span>
<span class="line"><span style="color:#F8F8F2">)</span></span>
<span class="line"></span></code></pre>
<basic-button class="copy">
<button type="button" class="secondary small">
<span class="label">Copy</span>
</button>
</basic-button>
<button type="button" class="overlay">Expand</button>
</module-codeblock>
<p>The <code>first()</code> function expects the matched element to be present at connection time. If not, it will silently ignore the call.</p>
<p>On the other hand, the <code>all()</code> function creates a dynamic array of elements that will be updated whenever the matching elements are added or removed from the component's DOM branch. UIElement will apply the given setup functions to added elements and run the cleanup functions on removed elements.</p>
<card-callout class="tip">
<p><strong>Tip</strong>: The <code>all()</code> function is more flexible but also more resource-intensive than <code>first()</code>. Prefer <code>first()</code> when targeting a single element known to be present at connection time.</p>
</card-callout>
</section>
<section>
<h2 id="adding-event-listeners">
<a name="adding-event-listeners" class="anchor" href="#adding-event-listeners">
<span class="permalink">🔗</span>
<span class="title">Adding Event Listeners</span>
</a>
</h2>
<p>Event listeners allow to respond to user interactions. They are the cause of changes in the component's state.</p>
<module-codeblock collapsed language="js" copy-success="Copied!" copy-error="Error trying to copy to clipboard!">
<p class="meta">
<span class="language">js</span>
</p>
<pre class="shiki monokai" style="background-color:#272822;color:#F8F8F2" tabindex="0"><code><span class="line"><span style="color:#A6E22E">component</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"my-component"</span><span style="color:#F8F8F2">, {</span></span>
<span class="line"><span style="color:#F8F8F2"> active: </span><span style="color:#AE81FF">0</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#F8F8F2"> value: </span><span style="color:#E6DB74">''</span></span>
<span class="line"><span style="color:#F8F8F2">}, (</span><span style="color:#FD971F;font-style:italic">el</span><span style="color:#F8F8F2">, { </span><span style="color:#FD971F;font-style:italic">all</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">first</span><span style="color:#F8F8F2"> }) </span><span style="color:#66D9EF;font-style:italic">=></span><span style="color:#F8F8F2"> [</span></span>
<span class="line"><span style="color:#A6E22E"> all</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"button"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"click"</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">e</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F8F8F2"> {</span></span>
<span class="line"><span style="color:#88846F"> // Set "active" signal to value of data-index attribute of button</span></span>
<span class="line"><span style="color:#66D9EF;font-style:italic"> const</span><span style="color:#F8F8F2"> index </span><span style="color:#F92672">=</span><span style="color:#A6E22E"> parseInt</span><span style="color:#F8F8F2">(e.target.dataset[</span><span style="color:#E6DB74">'index'</span><span style="color:#F8F8F2">], </span><span style="color:#AE81FF">10</span><span style="color:#F8F8F2">);</span></span>
<span class="line"><span style="color:#F8F8F2"> el.active </span><span style="color:#F92672">=</span><span style="color:#F8F8F2"> Number.</span><span style="color:#A6E22E">isInteger</span><span style="color:#F8F8F2">(index) </span><span style="color:#F92672">?</span><span style="color:#F8F8F2"> index </span><span style="color:#F92672">:</span><span style="color:#AE81FF"> 0</span><span style="color:#F8F8F2">;</span></span>
<span class="line"><span style="color:#F8F8F2"> })</span></span>
<span class="line"><span style="color:#F8F8F2"> ),</span></span>
<span class="line"><span style="color:#A6E22E"> first</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"input"</span><span style="color:#F8F8F2">,</span></span>
<span class="line"><span style="color:#A6E22E"> on</span><span style="color:#F8F8F2">(</span><span style="color:#E6DB74">"change"</span><span style="color:#F8F8F2">, </span><span style="color:#FD971F;font-style:italic">e</span><span style="color:#66D9EF;font-style:italic"> =></span><span style="color:#F