UIElement Docs Version 0.9.4

๐Ÿ“‹ Detailed Walkthrough

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.

Ways State Enters a Component

State in a component can come from several sources, each with its own way of being initialized and managed.

Observed Attributes

Attributes declared in static observedAttributes are automatically parsed and converted into reactive signals using static states.

js

import { UIElement, asInteger } from '@zeix/ui-element'

class CounterComponent extends UIElement {
	static observedAttributes = ['count']
	static states = { count: asInteger }
}

If you set an attribute on your custom element, it becomes a signal within the component.

html

<counter-component count="5"></counter-component>

Available attribute parsers are:

If the parser fails, for example because numeric attribute evaluate to NaN or the supplied JSON is invalid, the signal will silently default to undefined, just as if the attribute wasn't there. Undefined signals won't trigger any effects.

The pre-defined attribute parser functions connect to the Maybe interface used in the attributeChangedCallback() of the UIElement 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.

Defaults from DOM in Auto-Effects

When an auto-effect like setText() 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.

html

<hello-world>
	<p>Hello, <span class="name">World</span>!</p>
</hello-world>

js

this.first('.name').sync(setText('name'))

In this case, the name signal defaults to "World" as it uses the initial content of .name.

Later setting the name signal to undefined or by deleting it would revert the text content of .name back to the server-rendered version.

Manually Set Signals

You can set signals manually using this.set(). This is necessary in the following cases:

js

// Setting a default state
this.set('count', 10)

// Setting a derived state
this.set('even', () => this.get('count') % 2 === 0)

Passed State

State can be passed from a parent to child component. This is covered in the Best Practices & Patterns section.

Context Consumers

State can also be provided to a component through context. This allows sharing state across components and will be covered in the Advanced Topics section.

Accessing Sub-Elements

Accessing sub-elements within your `UIElement` component is essential for setting up event listeners, auto-effects, or custom effects.

this.self

A reference to the custom element itself, which acts as the component wrapper. Useful for reflecting attributes or managing the component as a whole.

this.first(selector)

Finds the first matching sub-element within the component using querySelector(). This is best for targeting unique elements that are expected to exist once within the component.

js

this.first('.count').sync(setText('count'))

this.all(selector)

Finds all matching sub-elements within the component using querySelectorAll(). Use this for targeting groups of elements that require batch processing or to allow multiple instances of a given element within the component.

js

this.all('.item').sync(toggleClass('active', 'isActive'))

UI Interface

The this.self property and this.first() and this.all() 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 null or undefined.

Namely, the object for each matching element contains a host and a target field:

The pre-defined functions in UIElement for event listeners (on() and off()), to pass state (pass()), and the auto-effects are partially applied functions connecting to this UI interface. You may define custom functions connecting to the UI interface and reuse them across components.

The Power of Array Methods

As we are using native arrays, it's possible to chain multiple effects and event handlers on the same accessed element(s) using the map() method. If you don't need chaining use forEach() instead, as it tends to be slightly faster.

Additionally, array methods like .filter(), .find(), and .every() can be used to further refine or manipulate the list of elements returned by this.all().

js

// 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'))

Ways State is Mutated

Once signals are established, they can be mutated through user interactions or asynchronous operations.

Event Handlers

Event handlers are the primary way to mutate state based on user input. You can use on() to attach event listeners to elements within the component.

js

this.first('input').on('input', e => this.set('name', e.target.value))

When an event occurs, the signal name is updated, triggering any bound effects.

The pre-defined event listener functions are:

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 window or any outer element, be sure to remove the event listener in disconnectedCallback().

Resolved Promises

Asynchronous data sources, such as fetch requests or other Promises, can be used to mutate signals once resolved.

js

fetch('/data')
	.then(response => response.json())
	.then(data => this.set('data', data))

Ways to React to State Changes

When state changes in a component, UIElement provides various mechanisms to react and update the DOM or perform other side effects

Auto-Effects for DOM Updates

Auto-effects are declarative bindings between signals and the DOM, automatically updating elements when signals change.

Most auto-effect take the property to be mutated as a first argument and a StateLike as a second argument. Exceptions are setText(), that takes only the StateLike argument, and emit(), that takes the custom event name as first argument. StateLike can be either:

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.

js

// Using multiple auto-effects
this.first('.count').sync(setText('count'))
this.all('.item').sync(toggleClass('active'))
this.self.sync(toggleAttribute('selected'))

Custom Effects with effect()

For more complex or custom reactions to state changes, use the effect() method to define your own effect handlers.

js

this.effect(() => {
	const count = this.get('count')
	console.log('Count changed:', count)
})

Custom effects allow you to perform side effects such as logging, requesting additional data, cloning templates, or calling external APIs when state changes.

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.

Custom effects may return a cleanup function that will be executed after all enqueued DOM updates are done.

Conclusion & Next Steps

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 Best Practices & Patterns or delve deeper into Advanced Topics.