Built-in Embeddable Pattern - Best Practices
The correct pattern for creating built-in embeddable components
Built-in Embeddable Pattern
🎯 The Rule
Built-in embeddables (Tier 1) should be PURE Angular standalone components.
❌ DO NOT extend BaseWidgetComponent or BaseEmbeddableComponent
✅ DO use pure Angular with signal inputs and outputs
✅ Correct Pattern
Component Structure
import { Component, input, output, ChangeDetectionStrategy } from '@angular/core';
import { NgClass } from '@angular/common';
@Component({
selector: 'ui-button',
standalone: true,
imports: [NgClass],
templateUrl: './ui-button.component.html',
styleUrls: ['./ui-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UiButtonComponent {
// ✅ Signal inputs (Angular 20)
text = input<string>('Button');
variant = input<string>('primary');
disabled = input<boolean>(false);
// ✅ Signal outputs
buttonClick = output<{ mouseX: number; mouseY: number }>();
// ✅ Pure methods (no base class dependencies)
onClick(event: MouseEvent): void {
if (this.disabled()) {
event.preventDefault();
return;
}
this.buttonClick.emit({
mouseX: event.clientX,
mouseY: event.clientY
});
}
}
Embeddable Registration
import { ModuleEmbeddableComponent } from '@libs-core/module-base';
import { UiButtonComponent } from '../ui-button.component';
export const BUTTON_EMBEDDABLE: ModuleEmbeddableComponent = {
id: 'ui-button',
displayName: 'UI Button',
description: 'Reusable button embeddable',
category: 'widget',
componentType: UiButtonComponent,
inputs: [
{ name: 'text', type: 'string', required: false },
{ name: 'variant', type: 'string', required: false },
{ name: 'disabled', type: 'boolean', required: false },
],
outputs: [
{ name: 'buttonClick', type: 'MouseEvent', description: 'Click event' },
],
};
❌ Wrong Pattern (What You Had Before)
// ❌ WRONG - Don't extend base classes
import { BaseWidgetComponent } from '@libs-widgets/widget-system';
export class UiButtonComponent extends BaseWidgetComponent {
// ❌ Creates coupling to widget system
override loading = input<boolean>(false);
// ❌ Requires base class methods
onWidgetInit(): void {
this.markReady();
}
// ❌ Uses base class methods
onClick(event: MouseEvent): void {
this.emitWidgetEvent('click', { ... });
}
}
Why this is wrong:
- Built-in embeddables are Tier 1 (foundation)
- They should have ZERO dependencies on widget system
- They should be pure, reusable Angular components
- Widget system wraps them (not the other way around)
🏗️ Architecture Hierarchy
TIER 1: Built-in Embeddables (Pure Components)
↓ (wrapped by)
TIER 2: Widget System (Adds editor metadata)
↓ (used by)
TIER 3: Widget Builder (Creates user widgets)
Key Insight: Tier 1 doesn't know about Tier 2 or 3. It's pure Angular.
📋 Components Fixed
All these components were updated to follow the correct pattern:
- ✅
UiButtonComponent- RemovedBaseWidgetComponentextension - ✅
UxIconComponent- RemovedBaseEmbeddableComponentextension - ✅
UxChipComponent- Converted to pure signals - ✅
UxChipListComponent- Removed base class (keptControlValueAccessor) - ✅
UiInputComponent- Removed base class (keptControlValueAccessor) - ✅
UxCardComponent- RemovedBaseWidgetComponentextension
🎨 Signal Pattern (Angular 20)
Inputs
// ✅ Use signal inputs
text = input<string>('Button');
variant = input<'primary' | 'secondary'>('primary');
disabled = input<boolean>(false);
// ✅ Access in template with ()
@if (disabled()) {
<span>Disabled</span>
}
// ✅ Access in class with ()
onClick() {
if (this.disabled()) return;
}
Outputs
// ✅ Use signal outputs
buttonClick = output<{ x: number; y: number }>();
// ✅ Emit events
this.buttonClick.emit({ x: 10, y: 20 });
Computed Values
// ✅ Use computed for derived state
import { computed } from '@angular/core';
isActive = computed(() =>
!this.disabled() && this.variant() === 'primary'
);
🔧 Special Cases
Form Controls (ControlValueAccessor)
// ✅ KEEP ControlValueAccessor for form integration
export class UiInputComponent implements ControlValueAccessor {
// ✅ Still use signal inputs
disabled = input<boolean>(false);
// ✅ Implement CVA methods
writeValue(value: string): void { ... }
registerOnChange(fn: any): void { ... }
registerOnTouched(fn: any): void { ... }
}
Rule: Remove base widget classes, but KEEP Angular interfaces like ControlValueAccessor, OnDestroy, etc.
🎯 Benefits of This Pattern
For Built-in Components
✅ Zero coupling to widget system
✅ Pure Angular - can be used anywhere
✅ Lightweight - no base class overhead
✅ Testable - no mocking base classes
For Widget System
✅ Clean separation - widgets wrap embeddables
✅ Flexibility - can wrap ANY component
✅ Extensibility - users can create custom wrappers
For AI Generation
✅ Clear contracts - inputs/outputs explicit
✅ No magic - no hidden base class methods
✅ Predictable - pure functions, no side effects
📚 Related Documentation
- Embeddable Architecture Clarification - Two-tier system
- System Analysis - Complete architecture overview
Last Updated: December 9, 2025 - Session 10.2
Status: All components fixed and verified