UIElement Docs Version 0.12.0

πŸ—οΈ Building Components

Create lightweight, self-contained Web Components with built-in reactivity. UIElement lets you define custom elements that manage state efficiently, update the DOM automatically, and enhance server-rendered pages without a framework.

Anatomy of Components

UIElement builds on Web Components, extending HTMLElement to provide built-in state management and reactive updates.

Defining a Component

A UIElement creates components using the component() function:

js

component('my-component', {}, () => {
	// Component setup
})

Every UIElement component must be registered with a valid custom element tag name (two or more words joined with -) as the first parameter.

Using the Custom Element in HTML

Once registered, the component can be used like any native HTML element:

html

<my-component>Content goes here</my-component>

Web Component Lifecycle in UIElement

UIElement manages the Web Component lifecycle from creation to removal. Here's what happens.

Component Creation (constructor())

This is when reactive properties are initialized. You pass a second argument to the component() function to defines initial values for component states.

js

component('my-component', {
	count: 0, // Initial value of 'count' signal
	isEven: el => () => !(el.count % 2) // Computed signal based on 'count'
	value: asInteger(5), // Parse 'value' attribute as integer
	name: consume('display-name') // Consume 'display-name' signal from closest context provider
}, () => {
	// Component setup
})

In this example you see all 4 ways to define a reactive property:

Note: Property initialization runs before the element is attached to the DOM. You can't access other properties or child elements here.

Mounted in the DOM (connectedCallback())

Runs when the component is added to the page. This is where you:

js

component('my-component', {
	count: 0,
}, el => {
	el.first('.increment', on('click', () => { el.count++ })) // Add click event listener
	el.first('.count', setText('count')) // Apply effect to update text
	el.self(
		emit('update-count', el.count) // Emit custom event
		provide('count') // Provide context
	)
})

Removed from the DOM (disconnectedCallback())

Runs when the component is removed. Event listeners bound with on() and signal subscriptions of effects are automatically removed by UIElement.

If you added event listeners outside the scope of your component or external subscriptions, you need to return a cleanup function:

js

component('my-component', {}, el => {
	const intersectionObserver = new IntersectionObserver(([entry]) => {
		// Do something
	}).observe(el)

	return () => {
		// Cleanup logic
		intersectionObserver.disconnect()
	}
})

Observed Attributes (attributeChangedCallback())

UIElement automatically converts attributes to signals. Usually, you don’t need to override this method manually.

State Management with UIElement

UIElement manages state using signals, which are atomic reactive states that trigger updates when they change. We use regular properties to access or update them.

Accessing and Updating Signal Values

js

console.log('count' in el); // Check if the signal exists
console.log(el.count); // Read the signal value
el.count = 42; // Update the signal value

Accessing & Setting Signals Directly

If you need to access the signals for a property key directly, you can use the getSignal() and setSignal() methods:

js

const doubleString = el.getSignal('count').map(v => String(v * 2)); // Derive a new Computed signal from 'count' signal
el.querySelector('input-field').setSignal('description', doubleString); // Replace the signal on another element with a new one

However, you should avoid manipulating signals directly unless you have a specific reason to do so. Use the pass()function to pass a signal or a derivation thereof to other elements.

Characteristics and Special Values

Signals in UIElement are of a fixed type and non-nullable. This allows to simplify the logic as you will never have to check the type or perform null-checks.

Because of the non-nullable nature of signals in UIElement, we need two special values that can be assigned to any signal type:

Why Signals with a Map Interface?

UIElement uses signals instead of standard properties or attributes because it ensures reactivity, loose coupling, and avoids common pitfalls with the DOM API.

Initializing State from Attributes

Declaring Observed Attributes

js

static observedAttributes = ['count']; // Automatically becomes a signal

Parsing Attribute Values

js

init = {
	count: asInteger(), // Convert '42' -> 42
	date: v => new Date(v), // Custom parser: '2025-02-14' -> Date object
};

Careful: Attributes may not be present on the element or parsing to the desired type may fail. To ensure non-nullability of signals, UIElement falls back to neutral defaults:

  • '' (empty string) for string
  • 0 for number
  • {} (empty object) for objects of any kind

Pre-defined Parsers in UIElement

Function Description
asBoolean Converts "true" / "false" to a boolean (true / false). Also treats empty attributes (checked) as true.
asInteger() Converts a numeric string (e.g., "42") to an integer (42).
asNumber() Converts a numeric string (e.g., "3.14") to a floating-point number (3.14).
asString() Returns the attribute value as a string (unchanged).
asEnum([...]) Ensures the string matches one of the allowed values. Example: asEnum(['small', 'medium', 'large']). If the value is not in the list, it defaults to the first option.
asJSON({...}) Parses a JSON string (e.g., '["a", "b", "c"]') into an array or object. If invalid, returns the fallback object.

The pre-defined parsers asInteger(), asNumber() and asString() allow to set a custom fallback value as parameter.

The asEnum() parser requires an array of valid values, while the first will be the fallback value for invalid results.

The asJSON() parser requires a fallback object as parameter as {} probably won't match the type you're expecting.

Accessing Sub-elements within the Component

Before adding event listeners, applying effects, or passing states, you need to select elements inside the component.

UIElement provides the following methods for element selection:

Method Description
this.self Selects the component itself.
this.first(selector) Selects the first matching element inside the component.
this.all(selector) Selects all matching elements inside the component.

js

// Select the component itself
this.self.sync(setProperty('hidden'));

// Select the first '.increment' button & add a click event
this.first('.increment').on('click', () => {
	this.set('count', v => null != v ? ++v : 1);
});

// Select all <button> elements & sync their 'disabled' properties
this.all('button').sync(setProperty('disabled', 'hidden'));

Updating State with Events

User interactions should update signals, not the DOM directly. This keeps the components loosly coupled.

Bind event handlers to one or many elements using the .on() method:

js

this.first('.increment').on('click', () => {
	this.set('count', v => null != v ? v++ : 1)
});
this.first('input').on('input', e => {
	this.set('name', e.target.value || undefined)
});

Synchronizing State with Effects

Effects automatically update the DOM when signals change, avoiding manual DOM manipulation.

Applying Effects with .sync()

Apply one or multiple effects to elements using .sync():

js

this.first('.count').sync(
	setText('count'), // Update text content according to 'count' signal
	toggleClass('even', 'isEven') // Toggle 'even' class according to 'isEven' signal
);

Pre-defined Effects in UIElement

Function Description
setText() Updates text content with a string signal value (while preserving comment nodes).
setProperty() Updates a given property with any signal value.*
setAttribute() Updates a given attribute with a string signal value.
toggleAttribute() Toggles a given boolean attribute with a boolean signal value.
toggleClass() Toggles a given CSS class with a boolean signal value.
setStyle() Updates a given CSS property with a string signal value.
createElement() Inserts a new element with a given tag name with a Record<string, string> signal value for attributes.
removeElement() Removes an element if the boolean signal value is true.

Tip: 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 setProperty() over setAttribute() for increased type safety. Setting string attributes is possible for all elements, but will have an effect only on some.

Simplifying Effect Notation

For effects that take two arguments, the second argument can be omitted if the signal key matches the targeted property name, attribute, class, or style property.

When signal key matches property name:

js

this.first('.count').sync(toggleClass('even'));

Here, toggleClass('even') automatically uses the "even" signal.

Using Functions for Ad-hoc Derived State

Instead of a signal key, you can pass a function that derives a value dynamically:

js

this.first('.count').sync(toggleClass('even', () => !((this.get('count') ?? 0) % 2)));

When to use

  • Use a signal key when the state is already stored as a signal.
  • Use a function when you derive a value on the fly needed only in this one place and you don't want to expose it as a signal on the element.

Custom Effects

For complex DOM manipulations, define your own effect using effect().

Here's an example effect that attaches a Shadow DOM and updates its content:

js

// Update the shadow DOM when content changes
effect(() => {
	const content = this.get('content')
	if (content) {
		this.root = this.shadowRoot || this.attachShadow({ mode: 'open' })
		this.root.innerHTML = content
	}
});

Efficient & Fine-Grained Updates

Unlike some frameworks that re-render entire components, UIElement updates only what changes:

In practical terms: UIElement is as easy as React but without re-renders.

Single Component Example: MySlider

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.

Slides

Slide 1

Hello, World!

Slide 2

Slide 3

Rate
Source Code

Loading...

Next Steps

Now that you understand the basics, explore: