UIElement Docs Version 0.11.0

πŸ”„ 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:

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:

js

this.first('input-button').pass({
	badge: () => asPositiveIntegerString(
		this.all('spin-button').targets
			.reduce((sum, item) => sum + item.get('value'), 0)
	)
});

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.

js

class InputButton extends UIElement {
	connectedCallback() {
		this.first('.badge').sync(setText('badge'));
	}
}

The InputButton doesn't care how the badge value is calculated – just that it gets one!

Full Example

Here's how everything comes together:

No custom events are needed – state flows naturally!

Shop

  • Product 1

  • Product 2

  • Product 3

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:

Why use events here?

Parent Component: TodoApp

The parent (TodoApp) listens for events and calls the .addItem() method on TodoList when a new todo is added:

js

this.self.on('add-todo', e => {
	this.querySelector('todo-list').addItem(e.detail)
})

Child Component: TodoForm

The child (TodoForm) collects user input and emits an event when the form is submitted:

js

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()
		}
	})
})

Full Example

Here's how everything comes together:

    Well done, all done!

    task tasks remaining

    Filter
    TodoApp Source Code

    Loading...

    InputField Source Code

    Loading...

    InputButton Source Code

    Loading...

    InputCheckbox Source Code

    Loading...

    InputRadiogroup Source Code

    Loading...

    Providing Context

    Consuming Context

    Next Steps