UIElement Docs Version 0.9.4

๐Ÿ’ก Best Practices & Patterns

Learn the best practices for building loosely coupled UIElement components, focusing on managing styles, states, and inter-component communication in a controlled and predictable way.

Composability Principle

Each component should be self-contained, managing its own state and styles, without relying directly on other components for its internal logic or presentation.

Self-Managed State & Styles

Components are responsible for their internal state and appearance, making them reusable and predictable. Parent components should not modify or style the internal DOM of their child components directly.

Components should not access elements higher up in the DOM tree or in a different branch thereof.

Styling Components

Avoid styling inner elements of sub-components directly from parent components, as this would make the appearance of the inner component dependent on the styles of the outer component. Parent components may style only the wrapper of child components for layout purposes (margins, gap, flex and grid properties).

Scope Styles via Custom Element Name

Each component should have scoped styles via their custom element name, ensuring its styles don't leak out. Custom element names are unique within the document, making them ideal for scoping purposes. Aim for low specificity selectors like tag names, so it's easy to override with a single class when you need to differentiate.

css

my-component {
	padding: 1rem;

	/* Only divs that are immediate children of my-component will be styled */
	> div {
		background-color: lightgray;
	}
}

Customize via Class or CSS Custom Properties

Components should allow reasonable variants via defined classes on the wrapper element or customizations via CSS custom properties.

Classes allow parent components to choose between certain given variants.

CSS custom properties allow parent components to influence the appearance of sub-components without directly styling their DOM internals.

css

parent-component {
	--box-bg-color: red;
	--box-text-color: white;
}

/* Base message box appearance can be influenced using CSS custom properties */
message-box {
	background-color: var(--box-bg-color, lightgray);
	color: var(--box-text-color, black);

	/* While pre-defined variant with class "success" always comes with a fixed color scheme */
	&.success {
		background-color: green;
		color: white;
	}
}

Passing State Down

Parent components can control sub-components by setting their publicly accessible signals. Use the pass() function to pass state directly and share signals.

Passing State with pass()

Use the pass() function to pass a state from a parent component to a child component, keeping the state synchronized.

js

class ParentComponent extends UIElement {
	connectedCallback() {
	this.set('parentColor', 'blue');
	this.pass('parentColor', 'child-component', 'color');
	}
}
ParentComponent.define('parent-component');

js

class ChildComponent extends UIElement {
	connectedCallback() {
	this.first('.box').sync(setStyle('background-color', 'color'));
	}
}
ChildComponent.define('child-component');

Bubbling Up State with Custom Events

When a child component doesn't have full context for handling state changes, it can dispatch custom events that bubble up to parent components using emit().

Dispatching Custom Events with emit()

Use the emit() method to dispatch custom events to notify parent components of changes.

js

// In child component
this.emit('change', { detail: { value: this.get('value') } });

Handling Custom Events in Parent Components

Parent components can listen for custom events from child components and respond accordingly.

js

// In parent component
this.first('child-component').on('change', (event) => {
	console.log('Received change event:', event.detail.value);
	// Handle state changes
});

Practical Example

The child component emits a change event whenever an internal signal changes, and the parent listens and handles it.

class ChildComponent extends UIElement {
	connectedCallback() {
		this.first('input').on('input', (event) => {
			this.set('value', event.target.value);
			this.emit('change', { detail: { value: event.target.value } });
		});
	}
}
class ParentComponent extends UIElement {
	connectedCallback() {
		this.first('child-component').on('change', (event) => {
			console.log('Child value changed:', event.detail.value);
			// Update parent state or perform an action
		});
	}
}
<parent-component>
	<child-component></child-component>
</parent-component>

Best Practices for Custom Events

Conclusion & Next Steps

By adhering to best practices for composability, styling, and state management, you can build efficient and loosely coupled UIElement components. Explore "Advanced Topics" to delve deeper into context and more complex patterns.