π‘ 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 Principles
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 the internal states or DOM of their child components directly.
class ChildComponent extends UIElement {
connectedCallback() {
this.set('color', 'blue');
this.first('.box').map(setStyle('background-color', 'color'));
}
}
ChildComponent.define('child-component');
<child-component>
<div class="box">I am styled internally!</div>
</child-component>
Styling Components
Each component should have scoped styles, ensuring no unintended global styles affect it. Use CSS nesting and avoid styling inner elements of sub-components directly from parent components.
Scoped Styles Using CSS Nesting
Style your components using their tag names, and use CSS nesting to target sub-elements. Avoid using classes and IDs unless they add clarity or specificity.
<style>
child-component {
padding: 10px;
}
child-component div {
background-color: lightgray;
}
</style>
<child-component></child-component>
Passing State Between Components
Parent components can control sub-components by setting publicly accessible signals or using CSS custom properties to influence their appearance. Use the pass() function to pass state directly and synchronize signals.
Using CSS Custom Properties
CSS custom properties allow parent components to influence the appearance of sub-components without affecting their internal DOM.
<style>
parent-component {
--box-color: red;
}
</style>
<parent-component>
<child-component></child-component>
</parent-component>
class ChildComponent extends UIElement {
connectedCallback() {
this.first('.box').map(setStyle('background-color', 'var(--box-color)'));
}
}
Updating Publicly Exposed Signals
A parent component can update a child componentβs exposed signals using .set().
// In parent component
this.first('child-component').target.set('color', 'green');
Passing State with pass()
Use the pass() function to pass a state from a parent component to a child component, keeping the state synchronized.
class ParentComponent extends UIElement {
connectedCallback() {
this.set('parentColor', 'blue');
this.pass('parentColor', 'child-component', 'color');
}
}
ParentComponent.define('parent-component');
class ChildComponent extends UIElement {
connectedCallback() {
this.first('.box').map(setStyle('background-color', 'color'));
}
}
ChildComponent.define('child-component');
<parent-component>
<child-component></child-component>
</parent-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.
// 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.
// In parent component
this.first('child-component').map(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').map(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').map(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
- Emit only when necessary: Emit events to notify parents of significant state changes.
- Consistent event names: Use clear, meaningful names for custom events.
- Use bubbling carefully: Understand the scope of event bubbling and which ancestor components may handle the event.
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.