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:


🏗️ 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:

  1. ✅ UiButtonComponent - Removed BaseWidgetComponent extension
  2. ✅ UxIconComponent - Removed BaseEmbeddableComponent extension
  3. ✅ UxChipComponent - Converted to pure signals
  4. ✅ UxChipListComponent - Removed base class (kept ControlValueAccessor)
  5. ✅ UiInputComponent - Removed base class (kept ControlValueAccessor)
  6. ✅ UxCardComponent - Removed BaseWidgetComponent extension

🎨 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


Last Updated: December 9, 2025 - Session 10.2
Status: All components fixed and verified