Widget System Architecture

Comprehensive guide to the visual editor widget system architecture

🧩 Widget System Architecture

Overview

The widget system provides a visual editor for building platforms through drag-and-drop components. It extends the canonical @libs-core/module-base architecture with visual editing capabilities.


Architecture Diagram

┌─────────────────────────────────────────────────────────────────────────────┐
│  CANONICAL LAYER (@libs-core/module-base)                                   │
│  ─────────────────────────────────────────                                  │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  ModuleEmbeddableComponent                                           │   │
│  │  ├── id: string                                                      │   │
│  │  ├── componentType: Type<unknown>                                    │   │
│  │  ├── displayName: string                                             │   │
│  │  ├── category: 'widget' | 'list' | 'form' | 'dashboard'              │   │
│  │  ├── inputs?: ModuleParameterMetadata[]                              │   │
│  │  ├── outputs?: ModuleOutputMetadata[]                                │   │
│  │  └── permissions?: string[]                                          │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  ComponentRegistryService                                            │   │
│  │  ├── registerModule(manifest): void                                  │   │
│  │  ├── getComponent(id): ModuleEmbeddableComponent                     │   │
│  │  ├── getAllComponents(): ModuleEmbeddableComponent[]                 │   │
│  │  └── getComponentsByCategory(category): ModuleEmbeddableComponent[]  │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                              │
                     extends & wraps
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  VISUAL EDITOR LAYER (@libs-widgets/widgets)                                │
│  ─────────────────────────────────────────────                              │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  EmbeddableDefinition (extends ModuleEmbeddableComponent)            │   │
│  │  ├── ...all ModuleEmbeddableComponent fields...                      │   │
│  │  ├── icon: string              ← Visual editor                       │   │
│  │  ├── inputs: EmbeddableInput[] ← Extended metadata                   │   │
│  │  ├── outputs: EmbeddableOutput[]                                     │   │
│  │  ├── source: 'builtin' | 'module' | 'user-created'                   │   │
│  │  ├── variants: SimpleWidgetVariant[]  ← Named configurations         │   │
│  │  ├── tags: string[]            ← Search keywords                     │   │
│  │  ├── customPropertyGroups?: PropertyGroup[]                          │   │
│  │  └── demoRoute?: string        ← Showcase link                       │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │  EmbeddableRegistryService (wraps ComponentRegistryService)          │   │
│  │  ├── Bridges:                                                        │   │
│  │  │   └── ComponentRegistryService (module embeddables)               │   │
│  │  ├── Adds:                                                           │   │
│  │  │   ├── Built-in widgets (Button, Input, Card, etc.)                │   │
│  │  │   ├── User-created widgets (Widget Builder)                       │   │
│  │  │   ├── Property panel generation                                   │   │
│  │  │   ├── Variant management                                          │   │
│  │  │   └── Search, filtering, categorization                           │   │
│  │  └── API:                                                            │   │
│  │      ├── register(definition): void                                  │   │
│  │      ├── get(id): EmbeddableDefinition                               │   │
│  │      ├── getAll(): EmbeddableDefinition[]                            │   │
│  │      ├── getByCategory(category): EmbeddableDefinition[]             │   │
│  │      ├── generatePropertyGroups(id): PropertyGroup[]                 │   │
│  │      └── search(query): EmbeddableDefinition[]                       │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘

Key Interfaces

Canonical (from @libs-core/module-base)

// ModuleEmbeddableComponent - Core interface for module embeddables
interface ModuleEmbeddableComponent {
  readonly id: string;
  readonly componentType: Type<unknown>;
  readonly displayName: string;
  readonly description?: string;
  readonly category: 'widget' | 'list' | 'form' | 'dashboard';
  readonly permissions?: readonly string[];
  readonly inputs?: readonly ModuleParameterMetadata[];
  readonly outputs?: readonly ModuleOutputMetadata[];
  readonly thumbnail?: string;
}

// ModuleParameterMetadata - Parameter definition
interface ModuleParameterMetadata {
  readonly name: string;
  readonly type: ParameterType;
  readonly required: boolean;
  readonly defaultValue?: unknown;
  readonly description?: string;
  readonly validators?: readonly ParameterValidator[];
  readonly uiHints?: ParameterUIHints;
}

Visual Editor Extensions (from @libs-widgets/widgets)

// EmbeddableDefinition - Extended for visual editing
interface EmbeddableDefinition {
  // Core (maps to ModuleEmbeddableComponent)
  id: string;
  name: string;           // Maps to displayName
  description: string;
  category: string;       // Extended categories
  component: Type<unknown>; // Maps to componentType
  
  // Visual Editor Extensions
  icon: string;                           // Material icon name
  inputs: EmbeddableInput[];              // Extended input metadata
  outputs: EmbeddableOutput[];            // Extended output metadata
  source: 'builtin' | 'module' | 'user-created';
  sourceModuleId?: string;
  tags?: string[];                        // Search keywords
  variants?: SimpleWidgetVariant[];       // Named configurations
  customPropertyGroups?: PropertyGroup[]; // Property panel override
  demoRoute?: string;                     // Showcase page link
  hasDemoPage?: boolean;
  supportsChildren?: boolean;
  thumbnail?: string;
  version?: string;
  registeredAt?: Date;
  defaultProperties?: Record<string, unknown>;
}

// EmbeddableInput - Extended input with visual editor metadata
interface EmbeddableInput {
  name: string;
  type: EmbeddableInputType;
  label: string;           // Human-readable (from uiHints.label)
  description?: string;
  defaultValue: unknown;
  options?: EmbeddableSelectOption[];  // From enum validators
  group?: string;          // Property panel grouping
  order?: number;          // Sort order within group
  responsive?: boolean;    // Per-breakpoint support
  min?: number;
  max?: number;
  step?: number;
  required?: boolean;
}

// SimpleWidgetVariant - Named configuration preset
interface SimpleWidgetVariant {
  id: string;
  name: string;            // e.g., "Primary Button", "Outline Button"
  description: string;
  inputOverrides: Record<string, unknown>;
  thumbnail?: string;
  tags?: string[];
}

Conversion Utilities

The system provides utilities to convert between canonical and extended interfaces:

import {
  convertParameterToInput,
  convertOutputToEmbeddable,
  convertModuleEmbeddableToDefinition
} from '@libs-widgets/widgets';

// Convert ModuleEmbeddableComponent to EmbeddableDefinition
const definition = convertModuleEmbeddableToDefinition(moduleEmb, moduleId);

// Convert ModuleParameterMetadata to EmbeddableInput
const input = convertParameterToInput(parameter);

// Convert ModuleOutputMetadata to EmbeddableOutput
const output = convertOutputToEmbeddable(outputMeta);

Component Sources

The widget system unifies three component sources:

1. Built-in Widgets (source: 'builtin')

Pre-built UI components that ship with the platform:

Registration:

// In widget-registry-init.ts
embeddableRegistry.register({
  id: 'builtin-button',
  name: 'Button',
  source: 'builtin',
  component: ButtonWidgetComponent,
  // ...
});

2. Module Embeddables (source: 'module')

Components from registered modules via BrowserModuleContract.embeddableComponents:

// In module manifest
const manifest: ModuleManifest = {
  id: 'user-management',
  embeddableComponents: [{
    id: 'user-list-widget',
    componentType: UserListWidgetComponent,
    displayName: 'User List',
    category: 'widget',
    inputs: [/* ModuleParameterMetadata[] */],
  }],
};

// Auto-synced to EmbeddableRegistryService via ComponentRegistryService bridge

3. User-Created Widgets (source: 'user-created')

Widgets created via the Widget Builder:

// In widget-storage.service.ts
const widgetDef = await widgetStorage.createWidget({
  name: 'My Custom Widget',
  template: '<div>Custom content</div>',
  styles: '.widget { color: red; }',
  // ...
});

// Automatically registered with EmbeddableRegistryService

Property Panel Integration

The system auto-generates property panels from EmbeddableInput[]:

// Get property groups for a widget
const propertyGroups = embeddableRegistry.generatePropertyGroups('builtin-button');

// Result:
[
  {
    id: 'builtin-button-appearance',
    title: 'Appearance',
    icon: 'fa-paint-brush',
    expanded: true,
    properties: [
      {
        key: 'properties.variant',
        label: 'Variant',
        type: 'select',
        options: [
          { value: 'primary', label: 'Primary' },
          { value: 'secondary', label: 'Secondary' },
        ],
        getValue: (widget) => widget.properties?.variant ?? 'primary',
        setValue: (widget, value) => ({ ...widget, properties: { ...widget.properties, variant: value } }),
      },
      // ... more properties
    ],
  },
  // ... more groups
]

Custom Property Groups

For complex widgets, override auto-generation:

embeddableRegistry.register({
  id: 'pricing-widget',
  customPropertyGroups: [
    {
      id: 'pricing-plans',
      title: 'Plans Configuration',
      properties: [
        {
          key: 'properties.plans',
          label: 'Pricing Plans',
          type: 'custom',
          component: PricingPlansEditorComponent,
          // Custom editor for complex data
        },
      ],
    },
  ],
});

Variant System

Variants are named configurations of the same component:

// Button variants
embeddableRegistry.register({
  id: 'builtin-button',
  name: 'Button',
  inputs: [
    { name: 'variant', type: 'select', defaultValue: 'primary', /* ... */ },
    { name: 'size', type: 'select', defaultValue: 'medium', /* ... */ },
    { name: 'label', type: 'string', defaultValue: 'Click me', /* ... */ },
  ],
  variants: [
    {
      id: 'primary',
      name: 'Primary Button',
      description: 'Main call-to-action button',
      inputOverrides: { variant: 'primary', size: 'large' },
    },
    {
      id: 'outline',
      name: 'Outline Button',
      description: 'Transparent button with border',
      inputOverrides: { variant: 'outline', size: 'medium' },
    },
    {
      id: 'small-secondary',
      name: 'Small Secondary',
      description: 'Compact secondary action',
      inputOverrides: { variant: 'secondary', size: 'small' },
    },
  ],
});

Registration Flow

Application Startup

┌─────────────────────────────────────────────────────────────────────────┐
│  APP_INITIALIZER                                                        │
├─────────────────────────────────────────────────────────────────────────┤
│  1. WidgetRegistryInitService.registerWidgets()                         │
│     └── Registers built-in widgets (Button, Input, etc.)                │
│                                                                         │
│  2. UiComponentsRegistryInitializer                                     │
│     └── Registers libs-ui components (Panel, Menu, etc.)                │
│                                                                         │
│  3. Module Registration (on module activate)                            │
│     └── ComponentRegistryService.registerModule(manifest)               │
│     └── Auto-bridged to EmbeddableRegistryService                       │
│                                                                         │
│  4. User Widgets (on app load)                                          │
│     └── WidgetStorageService.loadSavedWidgets()                         │
│     └── Registered with EmbeddableRegistryService                       │
└─────────────────────────────────────────────────────────────────────────┘

Usage Examples

Get All Embeddables

import { EmbeddableRegistryService } from '@libs-widgets/widgets';

@Component({...})
export class WidgetPaletteComponent {
  private readonly registry = inject(EmbeddableRegistryService);
  
  // Reactive signal - auto-updates when registry changes
  readonly allWidgets = this.registry.embeddables;
  
  // Get by category
  readonly formWidgets = computed(() => 
    this.registry.getByCategory('forms')
  );
}

Register a Custom Widget

import { EmbeddableRegistryService } from '@libs-widgets/widgets';

@Component({...})
export class WidgetBuilderComponent {
  private readonly registry = inject(EmbeddableRegistryService);
  
  createWidget(config: WidgetConfig) {
    this.registry.register({
      id: `custom-${config.id}`,
      name: config.name,
      description: config.description,
      category: 'custom',
      icon: 'widgets',
      component: DynamicWidgetHostComponent,
      inputs: this.extractInputsFromConfig(config),
      outputs: [],
      source: 'user-created',
    });
  }
}

Search Widgets

const results = registry.search('button');
// Returns all embeddables matching "button" in name, description, or tags

File Structure

libs/libs-widgets/widgets/
├── src/
│   ├── lib/
│   │   ├── services/
│   │   │   ├── embeddable-registry.service.ts  ← Main registry
│   │   │   └── index.ts
│   │   ├── models/
│   │   │   └── property-group.model.ts         ← Property panel types
│   │   ├── registry/
│   │   │   └── widget-registry-init.ts         ← Built-in registration
│   │   ├── widgets/
│   │   │   ├── button-widget/
│   │   │   ├── input-widget/
│   │   │   └── ...
│   │   └── types/
│   │       └── widget-variant.types.ts
│   └── index.ts

Relationship to gograb-modules

This architecture is compatible with the canonical gograb-modules architecture:

gograb-modules editor (this) Relationship
ModuleEmbeddableComponent EmbeddableDefinition Extended
ModuleParameterMetadata EmbeddableInput Extended
ModuleOutputMetadata EmbeddableOutput Extended
ComponentRegistryService EmbeddableRegistryService Wrapped
N/A SimpleWidgetVariant Visual editor addition
N/A Property panel generation Visual editor addition

Key Points:


Best Practices

1. Always Use EmbeddableRegistryService

// ✅ CORRECT - For visual editor operations
const registry = inject(EmbeddableRegistryService);

// ⚠️ ONLY for pure module operations
const moduleRegistry = inject(ComponentRegistryService);

2. Define Comprehensive Inputs

// ✅ CORRECT - Rich metadata for property panel
{
  name: 'variant',
  type: 'select',
  label: 'Button Style',
  description: 'Visual style of the button',
  defaultValue: 'primary',
  options: [
    { value: 'primary', label: 'Primary' },
    { value: 'secondary', label: 'Secondary' },
  ],
  group: 'appearance',
  order: 1,
}

3. Include Variants for Reusability

// ✅ CORRECT - Multiple variants increase reusability
variants: [
  { id: 'primary', name: 'Primary', inputOverrides: { variant: 'primary' } },
  { id: 'outline', name: 'Outline', inputOverrides: { variant: 'outline' } },
]

4. Group Properties Logically

// ✅ CORRECT - Organized property groups
inputs: [
  { name: 'label', group: 'content', order: 1 },
  { name: 'variant', group: 'appearance', order: 1 },
  { name: 'size', group: 'appearance', order: 2 },
  { name: 'disabled', group: 'behavior', order: 1 },
]

Related Documentation


Last Updated: December 4, 2025
Session ID: 5
Status: ✅ Complete