๐ก 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.
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.
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.
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').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.
// 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').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
- 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.