π Data Flow
UIElement enables smooth data flow between components using signals, events, and context. State can be passed down to child components, events can bubble up to notify parents of changes, and context can propagate across the component tree to share global state efficiently. This page explores different patterns for structuring data flow, helping you create modular, loosely coupled components that work seamlessly together.
Passing State Down
Let's consider a product catalog where users can add items to a shopping cart. We have three independent components that work together:
ProductCatalog
(Parent):- Tracks all
SpinButton
components in its subtree and calculates the total count of items in the shopping cart. - Passes that total to a
InputButton
, which displays the number of items in the cart.
- Tracks all
InputButton
(Child):- Displays a cart badge when the
'badge'
signal is set. - Does not track any state β it simply renders whatever value is passed down.
- Displays a cart badge when the
SpinButton
(Child):- Displays an βAdd to Cartβ button initially.
- When an item is added, it transforms into a stepper (increment/decrement buttons).
Although InputButton
and SpinButton
are completely independent, they need to work together.
So ProductCatalog
coordinates the data flow between them.
Parent Component: ProductCatalog
The parent component (ProductCatalog
) knows about its children, meaning it can observe and pass state to them.
Use the .pass()
method to send values to child components. It takes an object where:
- Keys = Signal names in the child (
InputButton
) - Values = Signal names in the parent (
ProductCatalog
) or functions returning computed values
this.first('input-button').pass({
badge: () => asPositiveIntegerString(
this.all('spin-button').targets
.reduce((sum, item) => sum + item.get('value'), 0)
)
});
- β
Whenever one of the
value
signals of a<spin-button>
updates, the total in the badge of<input-button>
automatically updates. - β No need for event listeners or manual updates!
Child Component: InputButton
The InputButton
component displays a badge when needed β it does not track state itself.
Whenever the 'badge'
signal assigned by a parent component updates, the badge text updates.
class InputButton extends UIElement {
connectedCallback() {
this.first('.badge').sync(setText('badge'));
}
}
- β
The
setText('badge')
effect keeps the badge in sync with the'badge'
signal. - β If badge is an empty string, the badge is hidden.
The InputButton
doesn't care how the badge value is calculated β just that it gets one!
Full Example
Here's how everything comes together:
- Each
SpinButton
tracks its own count. - The
ProductCatalog
sums all counts and passes the total toInputButton
. - The
InputButton
displays the total if it's greater than zero.
No custom events are needed β state flows naturally!
Shop
-
Product 1
0
-
Product 2
0
-
Product 3
0
ProductCatalog Source Code
Loading...
InputButton Source Code
Loading...
SpinButton Source Code
Loading...
Events Bubbling Up
Passing state down works well when a parent component can directly observe child state, but sometimes a child needs to notify its parent about an action without managing shared state itself.
Let's consider a Todo App, where users can add tasks:
TodoApp
(Parent):- Holds the list of todos as a state signal.
- Listens for an
'add-todo'
event from the child (TodoForm
). - Updates the state when a new todo is submitted.
TodoForm
(Child):- Handles user input but does not store todos.
- Emits an
'add-todo'
event when the user submits the form. - Lets the parent decide what to do with the data.
Why use events here?
- The child doesnβt need to know where the data goes β it just emits an event.
- The parent decides what to do with the new todo (e.g., adding it to a list).
- This keeps
TodoForm
reusable β it could work in different apps without modification.
Parent Component: TodoApp
The parent (TodoApp
) listens for events and calls the .addItem()
method on TodoList
when a new todo is added:
this.self.on('add-todo', e => {
this.querySelector('todo-list').addItem(e.detail)
})
- β
Whenever
TodoForm
emits an'add-todo'
event, a new task is appended to the todo list. - β
The event carries data (
e.detail
), so the parent knows what was submitted.
Child Component: TodoForm
The child (TodoForm
) collects user input and emits an event when the form is submitted:
const input = this.querySelector('input-field')
this.first('form').on('submit', e => {
e.preventDefault()
// Wait for microtask to ensure the input field value is updated before dispatching the event
queueMicrotask(() => {
const value = input?.get('value')?.trim()
if (value) {
this.self.emit('add-todo', value)
input?.clear()
}
})
})
- β The form does NOT store the todo β it just emits an event.
- β
The parent (
TodoApp
) decides what happens next. - β The event includes data (value), making it easy to handle.
Full Example
Here's how everything comes together:
- User types a task into input field in
TodoForm
. - On submit,
TodoForm
emits'add-todo'
with the new task as event detail. TodoApp
listens for'add-todo'
and updates the todo list.
TodoApp Source Code
Loading...
InputField Source Code
Loading...
InputButton Source Code
Loading...
InputCheckbox Source Code
Loading...
InputRadiogroup Source Code
Loading...