๐ 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.
Accessing Sub-Elements within a Component
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 setting attributes or managing the component as a whole.
this.first(selector)
Returns 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.
this.first('.count').map(setText('count'));
this.all(selector)
Returns all matching sub-elements within the component using querySelectorAll(). Use this for targeting groups of elements that require batch processing or multiple effects.
this.all('.item').map(toggleClass('active', 'isActive'));
Chaining Effects, Event Handlers & Array Methods
It is possible to chain multiple effects and event handlers on the same accessed element. Additionally, array methods like .filter(), .find(), and .every() can be used to further refine or manipulate the list of elements returned by this.all().
// Example chaining event handler and effect
this.first('button')
.map(on('click', () => this.set('clicked', true)))
.map(toggleClass('clicked', 'clicked'));
// Example filtering elements
this.all('.item')
.filter(item => item.target.textContent.includes('Important'))
.map(toggleClass('highlight', 'highlighted'));
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 attributeMap.
import { asInteger } from 'ui-element';
static observedAttributes = ['count'];
static attributeMap = { count: asInteger };
If you set an attribute on your custom element, it becomes a signal within the component.
<counter-component count="5"></counter-component>
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.
<hello-world>
<p>Hello, <span class="greeting">World</span>!</p>
</hello-world>
this.first('.greeting').map(setText('name'));
In this case, the name signal defaults to "World" as it uses the initial content of .greeting.
Manually Set Signals
You can set signals manually using this.set(). This is necessary in the following cases:
- Awaiting an async source or external API to get the initial value.
- Creating derived signals with arrow functions based on other state.
// Setting a default state
this.set('count', 10);
// Setting a derived state
this.set('isEven', () => this.get('count') % 2 === 0);
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.
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 .map() and on() to attach event listeners to elements within the component.
this.first('input').map(on('input', (event) => this.set('name', event.target.value)));
When an event occurs, the signal name is updated, triggering any bound effects.
Resolved Promises
Asynchronous data sources, such as fetch requests or other Promises, can be used to mutate signals once resolved.
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.
setText(): Sets the text content of an element.setProperty(): Sets a property on an element.setAttribute(): Sets an attribute based on a signal.toggleAttribute(): Toggles an attribute conditionally.toggleClass(): Adds or removes a class based on a signal.setStyle(): Updates a style property dynamically.emit(): Triggers a custom event based on signal changes.
// Using multiple auto-effects
this.first('.count').map(setText('count'));
this.all('.item').map(toggleClass('active', 'isActive'));
this.self.map(toggleAttribute('selected', 'isSelected'));
Custom Effects with effect()
For more complex or custom reactions to state changes, use the effect() method to define your own effect handlers.
this.effect(() => {
const count = this.get('count');
console.log('Count changed:', count);
});
Custom effects allow you to perform side effects such as logging, manipulating multiple DOM elements, or calling external APIs when state changes.
Be careful to avoid creating infinite loops by not setting signals within effect handlers unless managing derived states thoughtfully.
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."