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:
- Forms: Button, Input, Select, Checkbox, Toggle
- Layout: Panel, Card, Tabs, Grid
- Navigation: Menu, Breadcrumb, Pagination
- Data Display: Text, Image, Icon, Badge
- Feedback: Dialog, Toast, Loading
- Complex: Hero, Pricing, Features
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:
- Module embeddables flow through
ComponentRegistryService→EmbeddableRegistryService - Conversion utilities bridge the two type systems
- gograb-modules modules work seamlessly in the visual editor
- Visual editor extensions don't break module compatibility
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