@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
362 lines (337 loc) โข 18.7 kB
HTML
<html>
<head>
<title>UIElement Docs โ Detailed Walkthrough</title>
<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.9.4</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="installation-setup.html">
<span class="icon">โ๏ธ</span>
<strong>Installation & Setup</strong>
<small>How to install and set up the library</small>
</a>
</li>
<li>
<a href="core-concepts.html">
<span class="icon">๐งฉ</span>
<strong>Core Concepts</strong>
<small>Learn about signals, state, and reactivity</small>
</a>
</li>
<li class="active">
<a href="detailed-walkthrough.html">
<span class="icon">๐</span>
<strong>Detailed Walkthrough</strong>
<small>Step-by-step guide to creating components</small>
</a>
</li>
<li>
<a href="best-practices-patterns.html">
<span class="icon">๐ก</span>
<strong>Best Practices & Patterns</strong>
<small>Tips for effective and scalable usage</small>
</a>
</li>
<li>
<a href="advanced-topics.html">
<span class="icon">๐</span>
<strong>Advanced Topics</strong>
<small>Diving deeper into contexts and performance</small>
</a>
</li>
<li>
<a href="examples-recipes.html">
<span class="icon">๐งช</span>
<strong>Examples & Recipes</strong>
<small>Sample components and practical use cases</small>
</a>
</li>
<li>
<a href="troubleshooting-faqs.html">
<span class="icon">โ</span>
<strong>Troubleshooting & FAQs</strong>
<small>Common issues and frequently asked questions</small>
</a>
</li>
<li>
<a href="api-reference.html">
<span class="icon">๐</span>
<strong>API Reference</strong>
<small>Detailed documentation of classes and methods</small>
</a>
</li>
<li>
<a href="contributing-development.html">
<span class="icon">๐ค</span>
<strong>Contributing & Development</strong>
<small>How to contribute and set up the dev environment</small>
</a>
</li>
<li>
<a href="changelog-versioning.html">
<span class="icon">๐</span>
<strong>Changelog & Versioning</strong>
<small>Track changes and understand versioning</small>
</a>
</li>
<li>
<a href="licensing-credits.html">
<span class="icon">โ๏ธ</span>
<strong>Licensing & Credits</strong>
<small>License details and acknowledgments</small>
</a>
</li>
</ol>
</nav>
</header>
<main>
<section class="hero">
<h1>๐ Detailed Walkthrough</h1>
<p class="lead">This guide provides an in-depth look at how state flows through your components, from initialization to mutation, and how to build efficient, reactive Web Components.</p>
</section>
<section>
<h2>Ways State Enters a Component</h2>
<p>State in a component can come from several sources, each with its own way of being initialized and managed.</p>
<h3>Observed Attributes</h3>
<p>Attributes declared in <code>static observedAttributes</code> are automatically parsed and converted into reactive signals using <code>static states</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="language-js"><code>import { UIElement, asInteger } from '@zeix/ui-element'
class CounterComponent extends UIElement {
static observedAttributes = ['count']
static states = { count: asInteger }
}</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<p>If you set an attribute on your custom element, it becomes a signal within the component.</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="language-html"><code><counter-component count="5"></counter-component></code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<p>Available attribute parsers are:</p>
<ul>
<li><code>asBoolean</code>: Convert boolean attributes like <code>disabled</code> to actual booleans.</li>
<li><code>asInteger</code>: Try to parse numeric attributes as integer numbers.</li>
<li><code>asNumber</code>: Try to parse numeric attributes as floating point numbers.</li>
<li><code>asString</code>: Does nothing as all attributes are strings; string attributes may be omitted in <code>states</code>.</li>
<li><code>asJSON</code>: Try to parse structured data in an attribute as JSON.</li>
</ul>
<p>If the parser fails, for example because numeric attribute evaluate to <code>NaN</code> or the supplied JSON is invalid, the signal will silently default to <code>undefined</code>, just as if the attribute wasn't there. Undefined signals won't trigger any effects.</p>
<p>The pre-defined attribute parser functions connect to the <code>Maybe</code> interface used in the <code>attributeChangedCallback()</code> of the <code>UIElement</code> base class. Attribute parser functions are pure functions that map a string to the desired type. This allows you to define your own attribute parser functions and reuse them across components.</p>
<h3>Defaults from DOM in Auto-Effects</h3>
<p>When an auto-effect like <code>setText()</code> is used, the content of the target DOM element is taken as the default value of the signal if it hasn't been set manually.</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="language-html"><code><hello-world>
<p>Hello, <span class="name">World</span>!</p>
</hello-world></code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<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="language-js"><code>this.first('.name').sync(setText('name'))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<p>In this case, the <code>name</code> signal defaults to <code>"World"</code> as it uses the initial content of <code>.name</code>.</p>
<p>Later setting the <code>name</code> signal to <code>undefined</code> or by deleting it would revert the text content of <code>.name</code> back to the server-rendered version.</p>
<h3>Manually Set Signals</h3>
<p>You can set signals manually using <code>this.set()</code>. This is necessary in the following cases:</p>
<ul>
<li>Awaiting an async source or external API to get the initial value.</li>
<li>Creating derived signals with arrow functions based on other state.</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="language-js"><code>// Setting a default state
this.set('count', 10)
// Setting a derived state
this.set('even', () => this.get('count') % 2 === 0)</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<h3>Passed State</h3>
<p>State can be passed from a parent to child component. This is covered in the <a href="best-practices-patterns.html">Best Practices & Patterns</a> section.</p>
<h3>Context Consumers</h3>
<p>State can also be provided to a component through context. This allows sharing state across components and will be covered in the <a href="advanced-topics.html">Advanced Topics</a> section.</p>
</section>
<section>
<h2>Accessing Sub-Elements</h2>
<p>Accessing sub-elements within your `UIElement` component is essential for setting up event listeners, auto-effects, or custom effects.</p>
<h3>this.self</h3>
<p>A reference to the custom element itself, which acts as the component wrapper. Useful for reflecting attributes or managing the component as a whole.</p>
<h3>this.first(selector)</h3>
<p>Finds the first matching sub-element within the component using <code>querySelector()</code>. This is best for targeting unique elements that are expected to exist once within the component.</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="language-js"><code>this.first('.count').sync(setText('count'))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<h3>this.all(selector)</h3>
<p>Finds all matching sub-elements within the component using <code>querySelectorAll()</code>. Use this for targeting groups of elements that require batch processing or to allow multiple instances of a given element within the component.</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="language-js"><code>this.all('.item').sync(toggleClass('active', 'isActive'))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<h3>UI Interface</h3>
<p>The <code>this.self</code> property and <code>this.first()</code> and <code>this.all()</code> methods are or return an array of objects referencing the matching DOM elements. This may seem odd for exactly one or maybe zero or one element, but it gives us a unified interface to savely iterate over, never having to fear an expected element is <code>null</code> or <code>undefined</code>.</p>
<p>Namely, the object for each matching element contains a <code>host</code> and a <code>target</code> field:</p>
<ul>
<li>Host is the UIElement instance, needed to access signals using the <code>Map</code> interface.</li>
<li>Target is the matching DOM element, needed to bind events or to get or set text content, properties, attributes, class tokens or style properties.</li>
</ul>
<p>The pre-defined functions in UIElement for event listeners (<code>on()</code> and <code>off()</code>), to pass state (<code>pass()</code>), and the auto-effects are partially applied functions connecting to this <code>UI</code> interface. You may define custom functions connecting to the <code>UI</code> interface and reuse them across components.</p>
<h3>The Power of Array Methods</h3>
<p>As we are using native arrays, it's possible to chain multiple effects and event handlers on the same accessed element(s) using the <code>map()</code> method. If you don't need chaining use <code>forEach()</code> instead, as it tends to be slightly faster.</p>
<p>Additionally, array methods like <code>.filter()</code>, <code>.find()</code>, and <code>.every()</code> can be used to further refine or manipulate the list of elements returned by <code>this.all()</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="language-js"><code>// Example chaining event handler and effect
this.first('button')
.on('click', () => this.set('clicked', true))
.sync(toggleClass('clicked'))
// Example filtering elements
this.all('.item')
.filter(item => item.target.textContent.includes('Important'))
.sync(toggleClass('highlight'))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
</section>
<section>
<h2>Ways State is Mutated</h2>
<p>Once signals are established, they can be mutated through user interactions or asynchronous operations.</p>
<h3>Event Handlers</h3>
<p>Event handlers are the primary way to mutate state based on user input. You can use <code>on()</code> to attach event listeners to elements within the component.</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="language-js"><code>this.first('input').on('input', e => this.set('name', e.target.value))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<p>When an event occurs, the signal <code>name</code> is updated, triggering any bound effects.</p>
<p>The pre-defined event listener functions are:</p>
<ul>
<li><code>on(event, handler)</code>: Attaches an event listener to the element.</li>
<li><code>off(event, handler)</code>: Removes an event listener from the element.</li>
</ul>
<p>Usually it's not necessary to remove an event listener from elements. Event handlers get garbage collected if the component or inner element is removed. However, if you bind events to <code>window</code> or any outer element, be sure to remove the event listener in <code>disconnectedCallback()</code>.</p>
<h3>Resolved Promises</h3>
<p>Asynchronous data sources, such as <code>fetch</code> requests or other Promises, can be used to mutate signals once resolved.</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="language-js"><code>fetch('/data')
.then(response => response.json())
.then(data => this.set('data', data))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
</section>
<section>
<h2>Ways to React to State Changes</h2>
<p>When state changes in a component, UIElement provides various mechanisms to react and update the DOM or perform other side effects</p>
<h3>Auto-Effects for DOM Updates</h3>
<p>Auto-effects are declarative bindings between signals and the DOM, automatically updating elements when signals change.</p>
<ul>
<li><code>setText()</code>: Sets the text content of an element to the signal value.</li>
<li><code>setProperty()</code>: Sets a property on an element based on a signal.</li>
<li><code>setAttribute()</code>: Sets an attribute based on a string signal.</li>
<li><code>toggleAttribute()</code>: Toggles an boolean attribute based on a boolean signal.</li>
<li><code>toggleClass()</code>: Adds or removes a class based on a boolean signal.</li>
<li><code>setStyle()</code>: Updates an inline style property based on a string signal.</li>
<li><code>emit()</code>: Triggers a custom event based on signal changes.</li>
</ul>
<p>Most auto-effect take the property to be mutated as a first argument and a <code>StateLike</code> as a second argument. Exceptions are <code>setText()</code>, that takes only the <code>StateLike</code> argument, and <code>emit()</code>, that takes the custom event name as first argument. <code>StateLike</code> can be either:</p>
<ul>
<li>a state key,</li>
<li>an arrow function,</li>
<li>a signal variable.</li>
</ul>
<p>If the second argument is the same as the first, it may be omitted. If unambiguous, it's good practice to name signal keys after the property they are intended to update.</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><code>// Using multiple auto-effects
this.first('.count').sync(setText('count'))
this.all('.item').sync(toggleClass('active'))
this.self.sync(toggleAttribute('selected'))</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<h3>Custom Effects with effect()</h3>
<p>For more complex or custom reactions to state changes, use the <code>effect()</code> method to define your own effect handlers.</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><code>this.effect(() => {
const count = this.get('count')
console.log('Count changed:', count)
})</code></pre>
<input-button class="copy">
<button type="button" class="secondary small">Copy</button>
</input-button>
</code-block>
<p>Custom effects allow you to perform side effects such as logging, requesting additional data, cloning templates, or calling external APIs when state changes.</p>
<p>When setting signals in effects, be careful to avoid creating infinite loops. Usually, you should rather derive states than setting up a manual chain-reaction.</p>
<p>Custom effects may return a cleanup function that will be executed after all enqueued DOM updates are done.</p>
</section>
<section>
<h2>Conclusion & Next Steps</h2>
<p>Understanding how state flows into, through, and out of your UIElement components is key to building efficient, reactive Web Components. Now that you've learned about data flow and reactivity, explore the next sections to master <a href="best-practices-patterns.html">Best Practices & Patterns</a> or delve deeper into <a href="advanced-topics.html">Advanced Topics</a>.</p>
</section>
</main>
</body>
</html>