Clean V2 Architecture - Complete Design
Comprehensive architecture that leverages existing solid foundations and fixes the mess
Clean V2 Widget Architecture - Complete Design
🎯 Analysis Complete
After deep analysis, here's what we have:
✅ SOLID FOUNDATIONS (Keep & Build On)
widget-system/BaseWidgetComponent⭐- State machine (idle → initializing → ready → error)
- Timeout protection
- Event bus integration
- Error handling
- Logging
- THIS IS EXCELLENT - We build on this!
Properties Backend (
backend-api/properties) ⭐PropertyDefinition- Atomic property building blocksPropertyTemplate- Groups of properties- Stored in MongoDB per space
- AI metadata for smart generation
- THIS IS SOLID - We integrate with it!
Widget Builder ⭐
- AI-powered generation
- Property groups & values
- Variants system
- Template system
- Flash sync to backend
- THIS WORKS - We enhance it!
###❌ THE MESS (Fix or Replace)
Widget Components (
libs-widgets/widgets/components)- Mix UI + editor concerns
- Duplicate code with ui-core
- No standard contract
- REPLACE with clean wrappers
WidgetHostComponent
- 1778 lines of complexity
- Tightly coupled to Widget interface
- Not reusable
- REPLACE with EditorWidgetWrapper
Container Children Logic
- Hard-coded in ContainerWidgetComponent
- Can't reuse for other widgets
- ViewChildren mess
- REPLACE with universal children support
🏗️ The Clean V2 Architecture
Core Principle: Separation + Composition
┌──────────────────────────────────────────────────────────────┐
│ CLEAN V2 ARCHITECTURE │
├──────────────────────────────────────────────────────────────┤
│ │
│ LAYER 1: Pure UI Components (KEEP - Already Good!) │
│ ═══════════════════════════════════════════════════════ │
│ @libs-ui-core/ui-core │
│ ├─ UxButtonComponent │
│ ├─ UxCardComponent │
│ ├─ UxInputComponent │
│ └─ Pure Angular, zero editor knowledge │
│ │
│ widget-system/BaseWidgetComponent (Foundation) │
│ ├─ State machine │
│ ├─ Event bus integration │
│ ├─ Error handling │
│ └─ Lifecycle hooks │
│ │
├──────────────────────────────────────────────────────────────┤
│ LAYER 2: Widget Adapters (NEW - The Bridge!) │
│ ═══════════════════════════════════════════════════════ │
│ Declares how UI components work in editor │
│ │
│ interface EditorWidgetAdapter { │
│ // Identity │
│ id: string; // "button", "container" │
│ name: string; // "Button Widget" │
│ category: string; // "basic", "layout" │
│ │
│ // Component │
│ component: Type<any>; // UxButtonComponent │
│ baseClass?: Type<BaseWidgetComponent>; │
│ │
│ // Capabilities │
│ capabilities: { │
│ supportsChildren: boolean; │
│ childrenConfig?: ChildrenConfig; │
│ supportsResize: boolean; │
│ supportsDrag: boolean; │
│ }; │
│ │
│ // Properties (from backend!) │
│ propertyTemplateIds: string[]; // References backend │
│ customProperties?: PropertySchema[]; │
│ │
│ // Mapping │
│ mapPropertiesToInputs(props): ComponentInputs; │
│ mapOutputsToEvents(component): EventMap; │
│ │
│ // Defaults │
│ defaultSize: { width: number; height: number }; │
│ defaultProperties: Record<string, any>; │
│ } │
│ │
├──────────────────────────────────────────────────────────────┤
│ LAYER 3: Editor Widget Wrapper (NEW - Universal!) │
│ ═══════════════════════════════════════════════════════ │
│ ONE wrapper for ALL widgets │
│ │
│ <editor-widget-wrapper │
│ [widget]="widgetData" │
│ [adapter]="buttonAdapter" │
│ > │
│ <!-- Dynamically renders component --> │
│ <!-- Adds drag/resize handles --> │
│ <!-- Applies position/size styles --> │
│ <!-- Renders children if supported --> │
│ </editor-widget-wrapper> │
│ │
│ Features: │
│ ✅ Universal children support │
│ ✅ Drag/resize from directives │
│ ✅ Selection/editing chrome │
│ ✅ Works with ANY adapter │
│ │
├──────────────────────────────────────────────────────────────┤
│ LAYER 4: Clean Data Model (NEW - Lightweight!) │
│ ═══════════════════════════════════════════════════════ │
│ interface EditorWidget { │
│ id: string; │
│ adapterId: string; // References adapter │
│ position: SparseResponsiveValue<Position>; │
│ size: SparseResponsiveValue<Size>; │
│ styles: SparseResponsiveValue<Styles>; │
│ properties: Record<string, any>; │
│ parentId?: string; │
│ childIds?: string[]; // Flat, not nested │
│ } │
│ │
│ interface PageWidgets { │
│ widgets: EditorWidget[]; // Flat array │
│ // Hierarchy via parentId, children resolved at runtime │
│ } │
│ │
├──────────────────────────────────────────────────────────────┤
│ LAYER 5: Property System (INTEGRATE WITH BACKEND!) │
│ ═══════════════════════════════════════════════════════ │
│ Backend (MongoDB): │
│ ├─ PropertyDefinition (atomic properties) │
│ ├─ PropertyTemplate (grouped properties) │
│ └─ AI metadata for smart generation │
│ │
│ Frontend (Runtime): │
│ ├─ Load PropertyTemplates from backend │
│ ├─ Generate PropertyGroups for UI │
│ ├─ NEVER serialize functions │
│ └─ Clean, declarative │
│ │
└──────────────────────────────────────────────────────────────┘
📦 Implementation Plan
Phase 1: Foundation (Day 1)
Create: libs/libs-widgets/editor-widgets-v2
1.1: EditorWidgetAdapter Interface
/**
* Adapter that bridges ANY component to editor
*/
export interface EditorWidgetAdapter<T = any> {
// ========== IDENTITY ==========
id: string;
name: string;
description: string;
category: 'basic' | 'layout' | 'form' | 'media' | 'custom';
icon: string;
tags: string[];
// ========== COMPONENT ==========
/**
* The component to render
* Can be:
* - Pure UI component (UxButtonComponent)
* - BaseWidget subclass (extends lifecycle)
* - Any Angular component
*/
component: Type<T>;
/**
* Whether component extends BaseWidgetComponent
* Enables lifecycle hooks, state machine, event bus
*/
extendsBase: boolean;
// ========== CAPABILITIES ==========
capabilities: {
/** Can this widget contain other widgets? */
supportsChildren: boolean;
/** Can this widget be resized? */
supportsResize: boolean;
/** Can this widget be dragged? */
supportsDrag: boolean;
/** Can this widget be rotated? */
supportsRotation: boolean;
};
// ========== CHILDREN SUPPORT (if supportsChildren: true) ==========
childrenConfig?: {
/** Which adapter IDs can be children? (undefined = all) */
acceptedAdapters?: string[];
/** Maximum children count */
maxChildren?: number;
/** Supported layout types */
layoutTypes: ('free' | 'flex' | 'grid' | 'masonry')[];
/** Default layout type */
defaultLayout: 'free' | 'flex' | 'grid' | 'masonry';
/** Default spacing between children */
defaultGap: number;
/** Default padding */
defaultPadding: number;
};
// ========== PROPERTIES ==========
/**
* Property templates from backend (by ID)
* Example: ["sizing-template", "colors-template", "typography-template"]
*
* System loads these from backend API and merges into property groups
*/
propertyTemplateIds: string[];
/**
* Custom properties specific to this widget
* (in addition to templates)
*/
customProperties?: PropertySchema[];
// ========== MAPPING ==========
/**
* Convert widget.properties to component inputs
* @example
* mapPropertiesToInputs({ text: "Click Me", variant: "primary" })
* // Returns: { text: "Click Me", variant: "primary" }
*/
mapPropertiesToInputs(properties: Record<string, any>): Partial<T>;
/**
* Map component outputs to editor events
* @example
* mapOutputsToEvents(buttonComponent)
* // Returns: { click: buttonComponent.clicked }
*/
mapOutputsToEvents(component: T): Record<string, Observable<any>>;
// ========== DEFAULTS ==========
defaultSize: { width: number; height: number };
defaultProperties: Record<string, any>;
// ========== METADATA ==========
version: string;
author?: string;
}
1.2: EditorWidgetWrapper Component
/**
* Universal wrapper for ANY component in the editor
*
* This replaces the messy WidgetHostComponent
*
* Features:
* - Works with ANY adapter
* - Adds editor capabilities (drag, resize, selection)
* - Universal children support
* - Clean, reusable, maintainable
*/
@Component({
selector: 'editor-widget-wrapper',
standalone: true,
imports: [
CommonModule,
ResponsiveDraggableDirective,
ResponsiveResizableDirective,
EditorWidgetChromeComponent, // Selection UI, handles, etc.
],
template: `
<div
class="editor-widget-wrapper"
[class.has-children]="hasChildren()"
[class.is-selected]="isSelected()"
[class.is-locked]="widget.locked"
[attr.data-widget-id]="widget.id"
[attr.data-adapter-id]="adapter.id"
[style]="wrapperStyles()"
libResponsiveDraggable
[widget]="legacyWidget()"
[dragEnabled]="canDrag()"
(dragEnd)="onDragEnd($event)"
libResponsiveResizable
[widget]="legacyWidget()"
[resizeEnabled]="canResize()"
(resizeEnd)="onResizeEnd($event)"
>
<!-- Render the actual component -->
<ng-container #componentContainer></ng-container>
<!-- Universal children rendering -->
@if (adapter.capabilities.supportsChildren && hasChildren()) {
<div
class="children-container"
[attr.data-layout]="childrenLayout()"
[style]="childrenContainerStyles()"
libDropZone
[dropZoneEnabled]="canAcceptChildren() && isDesignMode()"
(dropped)="onChildDropped($event)"
>
@for (child of children(); track child.id) {
<div
class="child-wrapper"
[style]="getChildWrapperStyles(child)"
#childContainer
>
<editor-widget-wrapper
[widget]="child"
[adapter]="getAdapter(child.adapterId)"
[isChild]="true"
(widgetChange)="onChildChange($event)"
/>
</div>
}
</div>
}
<!-- Editor chrome (selection, handles, labels) -->
@if (isDesignMode() && !isPreview()) {
<editor-widget-chrome
[isSelected]="isSelected()"
[isLocked]="widget.locked"
[widgetName]="widget.name || adapter.name"
[adapterName]="adapter.name"
[childrenCount]="childrenCount()"
/>
}
</div>
`
})
export class EditorWidgetWrapperComponent implements AfterViewInit, OnDestroy {
// Inputs
widget = input.required<EditorWidget>();
adapter = input.required<EditorWidgetAdapter>();
isChild = input<boolean>(false);
isDesignMode = input<boolean>(true);
isPreview = input<boolean>(false);
isSelected = input<boolean>(false);
// Outputs
widgetChange = output<EditorWidget>();
widgetEvent = output<{ event: string; data: any }>();
// Services
private readonly adaptersRegistry = inject(WidgetAdaptersRegistry);
private readonly propertyLoader = inject(PropertyTemplateLoaderService);
// State
private componentRef?: ComponentRef<any>;
private childRefs: ComponentRef<any>[] = [];
ngAfterViewInit() {
this.renderComponent();
if (this.adapter().capabilities.supportsChildren) {
this.renderChildren();
}
}
private renderComponent() {
const adapter = this.adapter();
const widget = this.widget();
// Create component
const ref = this.componentContainer.createComponent(adapter.component);
this.componentRef = ref;
// Map properties to inputs
const inputs = adapter.mapPropertiesToInputs(widget.properties);
Object.entries(inputs).forEach(([key, value]) => {
ref.setInput(key, value);
});
// If component extends BaseWidget, set widgetId
if (adapter.extendsBase) {
ref.setInput('widgetId', widget.id);
}
// Map outputs to events
const outputs = adapter.mapOutputsToEvents(ref.instance);
Object.entries(outputs).forEach(([event, observable]) => {
observable.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(data => {
this.widgetEvent.emit({ event, data });
});
});
}
private renderChildren() {
// Universal children rendering
// Works for ANY adapter with supportsChildren: true
const children = this.children();
const containers = this.childContainers.toArray();
children.forEach((child, index) => {
const container = containers[index];
if (!container) return;
const childAdapter = this.adaptersRegistry.get(child.adapterId);
if (!childAdapter) return;
// Recursively render child with wrapper
const childRef = container.createComponent(EditorWidgetWrapperComponent);
childRef.setInput('widget', child);
childRef.setInput('adapter', childAdapter);
childRef.setInput('isChild', true);
// Forward events
childRef.instance.widgetChange.subscribe(updated => {
this.onChildChange(updated);
});
this.childRefs.push(childRef);
});
}
}
Phase 2: Property Integration (Day 1-2)
2.1: PropertyTemplateLoaderService
/**
* Loads PropertyTemplates from backend
* Caches them locally
* Generates PropertyGroups for UI
*/
@Injectable({ providedIn: 'root' })
export class PropertyTemplateLoaderService {
private readonly http = inject(HttpClient);
private readonly storage = inject(GgStorage);
// Cache of loaded templates
private templates = new Map<string, PropertyTemplate>();
/**
* Load templates by IDs
*/
async loadTemplates(templateIds: string[], spaceId: string): Promise<PropertyTemplate[]> {
const uncached = templateIds.filter(id => !this.templates.has(id));
if (uncached.length === 0) {
return templateIds.map(id => this.templates.get(id)!);
}
// Batch load from backend
const response = await firstValueFrom(
this.http.get<{ data: PropertyTemplate[] }>(
`/api/spaces/${spaceId}/properties/templates/batch?ids=${uncached.join(',')}`
)
);
response.data.forEach(template => {
this.templates.set(template.id, template);
});
return templateIds.map(id => this.templates.get(id)!);
}
/**
* Generate PropertyGroups for UI from templates
*/
generatePropertyGroups(
templates: PropertyTemplate[],
customProperties?: PropertySchema[]
): PropertyGroup[] {
const groups: PropertyGroup[] = [];
// Convert each template to a property group
templates.forEach(template => {
groups.push({
id: template.id,
title: template.name,
icon: template.icon || 'fa-cog',
expanded: template.category === 'content', // Content group expanded by default
order: this.getCategoryOrder(template.category),
properties: template.properties.map(prop => this.schemaToProperty(prop)),
});
});
// Add custom properties group if any
if (customProperties && customProperties.length > 0) {
groups.push({
id: 'custom',
title: 'Custom Properties',
icon: 'fa-wrench',
expanded: false,
order: 999,
properties: customProperties.map(prop => this.schemaToProperty(prop)),
});
}
return groups.sort((a, b) => (a.order || 0) - (b.order || 0));
}
private schemaToProperty(schema: PropertySchema): PropertyDefinition {
return {
key: schema.key,
label: schema.label,
type: schema.type,
responsive: schema.responsive,
defaultValue: schema.defaultValue,
getValue: (widget) => widget.properties[schema.key] ?? schema.defaultValue,
setValue: (widget, value) => ({
...widget,
properties: { ...widget.properties, [schema.key]: value }
}),
validation: schema.validation,
};
}
}
Phase 3: Core Adapters (Day 2-4)
3.1: Button Adapter
export const BUTTON_ADAPTER: EditorWidgetAdapter<UxButtonComponent> = {
id: 'button',
name: 'Button',
description: 'Interactive button widget',
category: 'basic',
icon: 'fa-hand-pointer',
tags: ['button', 'cta', 'action', 'click'],
component: UxButtonComponent,
extendsBase: false, // Pure UI component
capabilities: {
supportsChildren: false,
supportsResize: true,
supportsDrag: true,
supportsRotation: false,
},
// ✅ USES BACKEND TEMPLATES!
propertyTemplateIds: [
'content-text', // From backend: text, placeholder properties
'appearance-basic', // From backend: variant, size properties
'colors-button', // From backend: backgroundColor, textColor, borderColor
'behavior-button', // From backend: disabled, type, onClick properties
],
customProperties: [
{
key: 'icon',
label: 'Icon',
type: 'icon',
category: 'content',
responsive: false,
defaultValue: '',
description: 'Optional icon to display in button',
},
],
mapPropertiesToInputs(props) {
return {
text: props.text || 'Click Me',
variant: props.variant || 'primary',
size: props.size || 'medium',
disabled: props.disabled || false,
icon: props.icon,
};
},
mapOutputsToEvents(component) {
return {
click: component.clicked,
};
},
defaultSize: { width: 120, height: 40 },
defaultProperties: {
text: 'Click Me',
variant: 'primary',
size: 'medium',
disabled: false,
},
version: '1.0.0',
};
3.2: Container Adapter (THE KEY ONE!)
/**
* Simple container component (pure)
*/
@Component({
selector: 'ux-container',
standalone: true,
template: `
<div
class="ux-container"
[style.padding.px]="padding"
[style.gap.px]="gap"
[style.background]="background"
>
<ng-content></ng-content>
</div>
`
})
export class UxContainerComponent {
@Input() padding: number = 10;
@Input() gap: number = 10;
@Input() background: string = 'transparent';
@Input() layout: 'free' | 'flex' | 'grid' = 'free';
}
/**
* Container Adapter
*/
export const CONTAINER_ADAPTER: EditorWidgetAdapter<UxContainerComponent> = {
id: 'container',
name: 'Container',
description: 'Container for organizing other widgets',
category: 'layout',
icon: 'fa-box',
tags: ['container', 'layout', 'group', 'section'],
component: UxContainerComponent,
extendsBase: false,
capabilities: {
supportsChildren: true, // ✅ KEY!
supportsResize: true,
supportsDrag: true,
supportsRotation: false,
},
// ✅ CHILDREN CONFIGURATION!
childrenConfig: {
acceptedAdapters: undefined, // Accepts ALL widget types
maxChildren: undefined, // No limit
layoutTypes: ['free', 'flex', 'grid', 'masonry'],
defaultLayout: 'free',
defaultGap: 10,
defaultPadding: 10,
},
// ✅ USES BACKEND TEMPLATES!
propertyTemplateIds: [
'sizing-dimensions', // width, height, min/max
'spacing-padding', // padding all sides
'spacing-gap', // gap between children
'background-basic', // background color/image
'border-basic', // border width/style/color/radius
'layout-container', // layout type, alignment
],
customProperties: [
{
key: 'overflow',
label: 'Overflow',
type: 'select',
category: 'layout',
responsive: true,
defaultValue: 'visible',
validation: {
options: [
{ label: 'Visible', value: 'visible' },
{ label: 'Hidden', value: 'hidden' },
{ label: 'Scroll', value: 'scroll' },
{ label: 'Auto', value: 'auto' },
],
},
},
],
mapPropertiesToInputs(props) {
return {
padding: props.padding || 10,
gap: props.gap || 10,
background: props.background || 'transparent',
layout: props.layout || 'free',
};
},
mapOutputsToEvents(component) {
return {}; // Container has no events
},
defaultSize: { width: 300, height: 200 },
defaultProperties: {
padding: 10,
gap: 10,
background: 'transparent',
layout: 'free',
overflow: 'visible',
},
version: '1.0.0',
};
Phase 4: Widget Builder Integration (Day 4-5)
4.1: Update Widget Builder to Use Adapters
// widget-builder-workspace.component.ts
async saveWidget(): Promise<void> {
// Determine which adapter this widget will use
const adapterId = this.selectedType(); // e.g., "button", "container"
// Get property templates from the form
const propertyTemplates = this.propertyGroups().map(g => g.id);
// Create widget with adapter reference
const widget: EditorWidget = {
id: generateId(),
adapterId, // ✅ References adapter, not component!
position: createSparseValue({ x: 0, y: 0 }),
size: createSparseValue({ width: 120, height: 40 }),
styles: createSparseValue({}),
properties: this.propertyValues(),
parentId: undefined,
childIds: [],
createdAt: new Date(),
updatedAt: new Date(),
};
// Save to backend
await this.widgetStorage.saveWidget(widget);
// Register adapter if custom
if (adapterId === 'custom') {
await this.registerCustomAdapter({
id: widget.id,
component: this.generatedComponent,
propertyTemplateIds: propertyTemplates,
// ... rest of config
});
}
}
Phase 5: Editor-Web Integration (Day 5-6)
5.1: Update Editor Canvas
// editor-canvas.component.html (SIMPLIFIED!)
<div class="editor-canvas">
@for (widget of widgets(); track widget.id) {
<editor-widget-wrapper
[widget]="widget"
[adapter]="getAdapter(widget.adapterId)"
[isSelected]="isSelected(widget.id)"
[isDesignMode]="true"
(widgetChange)="onWidgetChange($event)"
/>
}
</div>
That's it! No more:
- Complex WidgetHostComponent
- Hard-coded container logic
- Messy property spread
- Circular dependencies
ONE wrapper for everything!
📊 Benefits Summary
What We Keep (Good Stuff)
- ✅ BaseWidgetComponent - Solid foundation
- ✅ Properties Backend - Atomic building blocks
- ✅ Widget Builder - AI generation
- ✅ Pure UI Components - Reusable
- ✅ Directives - Drag/resize work great
What We Replace (Mess)
- ❌ Widget components → Use pure UI + adapters
- ❌ WidgetHostComponent → EditorWidgetWrapper
- ❌ Container special case → Universal children support
- ❌ Property spread → Template-based system
- ❌ Fragile config → Adapter-declared contracts
🎯 Implementation Timeline
Week 1: Core System
Day 1: Foundation
- Create
editor-widgets-v2library - Implement
EditorWidgetAdapterinterface - Implement
EditorWidgetWrapperComponent - Implement
PropertyTemplateLoaderService - Implement
WidgetAdaptersRegistry
Day 2-3: Core Adapters
- Button adapter (wraps UxButtonComponent)
- Text adapter (wraps UxTextComponent or create simple one)
- Image adapter (wraps existing or create)
- Input adapter (wraps UxInputComponent)
- Container adapter (wraps simple UxContainerComponent)
Day 4-5: Integration
- Integrate with Widget Builder
- Integrate with Editor Canvas
- Test widget creation
- Test container + children ⭐
- Test drag/drop/resize
Day 6-7: Migration
- Migration script for existing widgets
- Update documentation
- Complete testing
🔑 Key Success Factors
1. Adapters ARE the Contract
Every adapter EXPLICITLY declares:
- What component it wraps
- What properties it needs (via backend templates!)
- Whether it supports children
- How to map props ↔ inputs
No guessing, no fragile spreads, no magic!
2. Universal Children Support
// ANY adapter can support children:
{
capabilities: {
supportsChildren: true,
},
childrenConfig: {
layoutTypes: ['free', 'flex', 'grid'],
// ...
}
}
// EditorWidgetWrapper handles ALL children rendering
// Container is NOT special anymore!
3. Backend Property Templates
// Backend defines reusable templates:
{
id: "colors-button",
name: "Button Colors",
properties: [
{ key: "backgroundColor", type: "color", ... },
{ key: "textColor", type: "color", ... },
]
}
// Adapters reference them:
{
propertyTemplateIds: ["colors-button", "sizing-basic"]
}
// Frontend loads and caches them
// Property panel uses them
// NO HARDCODED PROPERTY GROUPS!
4. Clean Data Model
// EditorWidget - ONLY data
{
id: "widget-123",
adapterId: "button", // References adapter
position: { base: { x: 10, y: 20 } }, // Sparse
size: { base: { width: 120, height: 40 } },
properties: { text: "Click", variant: "primary" },
parentId: undefined,
childIds: [],
}
// Size in MongoDB: ~200 bytes (vs current 16KB!)
🚀 Let's Start!
I'll create the v2 architecture with:
- ✅ EditorWidgetAdapter - Clean contract
- ✅ EditorWidgetWrapper - Universal wrapper
- ✅ PropertyTemplateLoader - Backend integration
- ✅ Core adapters - Button, Text, Image, Input, Container
- ✅ Universal children - Works for any adapter
- ✅ Widget Builder integration - Uses new system
- ✅ Editor-web integration - Clean canvas
Ready to proceed? Say "GO" and I'll start creating the v2 library! 🚀
Session: 11.9
Date: December 18, 2025
Status: Complete architecture designed, ready to implement