How to Create Perfect Widgets - Complete Guide
Comprehensive guide for creating widgets using smart property applicators, embeddables, Singularity integration, and flash-persistence patterns
🎯 How to Create Perfect Widgets - Complete Guide
Session ID: 6.2 (December 6, 2024)
Purpose: Comprehensive guide for creating widgets following established patterns
Audience: AI agents, developers continuing widget system work
🏗️ Widget Architecture (The Foundation)
The Hierarchy:
Embeddables (ui-core, ui-forms)
↓ pure UI components
Widgets (libs-widgets)
↓ editor-aware wrappers
Modules (modularity-lib)
↓ feature bundles
Pages (editor-web-lib)
↓ complete UIs
Rule: Always build bottom-up (embeddable → widget → module → page)
✅ The Perfect Widget Pattern
1. Widget Structure (MANDATORY)
import { Component, computed, inject, input } from '@angular/core';
import { Widget, BreakpointKey } from '@libs-foundational/core-models';
import { BaseWidgetComponent } from '@libs-widgets/widget-system';
import { UiButtonComponent } from '@libs-ui-core/ui-core'; // ✅ Use embeddable!
import { usePropertyStyles } from '../../services/property-applicators'; // ✅ Smart system!
import type { IWidgetFlowExecutorService } from '@libs-foundational/core-models';
import { WIDGET_FLOW_EXECUTOR_SERVICE } from '@libs-foundational/core-angular';
@Component({
selector: 'lib-my-widget',
standalone: true,
imports: [UiButtonComponent], // ✅ Import embeddable
template: `
<ui-button
[ngStyle]="smartStyles()"
[text]="config().text"
(click)="onButtonClick($event)"
></ui-button>
`,
styles: [`
:host {
display: block;
box-sizing: border-box;
/* ❌ NO width/height: 100% ! */
}
`],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyWidgetComponent extends BaseWidgetComponent {
// ✅ Required inputs (from BaseWidgetComponent pattern)
readonly widget = input.required<Widget>();
readonly activeBreakpoint = input<BreakpointKey>('base');
readonly isSelected = input<boolean>(false);
readonly isPreview = input<boolean>(false);
// ✅ Singularity integration (optional - won't crash if not loaded)
private readonly flowExecutor = inject<IWidgetFlowExecutorService>(
WIDGET_FLOW_EXECUTOR_SERVICE,
{ optional: true }
);
// ✅ Config from widget properties
config = computed(() => ({
text: this.widget().properties['text'] || 'Default',
...this.widget().properties
}));
// ✅ SMART: Use property applicators (100+ lines → 1 line!)
smartStyles = usePropertyStyles(this.widget, this.activeBreakpoint, ['font', 'appearance']);
// ✅ Lifecycle hooks
override onWidgetInit(): void {
this.markReady();
}
override onWidgetDestroy(): void {
// Cleanup if needed
}
// ✅ Flow execution
async onButtonClick(event: MouseEvent): Promise<void> {
if (!this.isPreview() || this.config().disabled) return;
event.stopPropagation();
if (this.flowExecutor) {
await this.flowExecutor.executeFromWidget(
this.widget().id,
'click',
{
widgetId: this.widget().id,
widgetType: this.widget().type,
eventType: 'click',
eventData: { timestamp: Date.now() }
}
);
}
}
}
2. Property Groups (Define widget properties)
Location: {widget-name}-property-groups.ts
import { PropertyGroup } from '../../models/property-group.model';
import { GROUP_ID, GROUP_ORDER } from '../../providers/default-properties/default-group.types';
export const MY_WIDGET_GROUP_CONTENT: PropertyGroup = {
id: 'my-widget-content',
title: 'Content',
icon: 'fa-font',
order: GROUP_ORDER.CONTENT,
expanded: true,
properties: [
{
key: 'properties.text',
label: 'Text',
type: 'string',
getValue: (widget) => widget.properties['text'] || '',
setValue: (widget, value) => ({
...widget,
properties: { ...widget.properties, text: value }
})
}
]
};
export const MY_WIDGET_GROUP_APPEARANCE: PropertyGroup = {
id: GROUP_ID.APPEARANCE,
title: 'Appearance',
icon: 'fa-palette',
order: GROUP_ORDER.APPEARANCE,
expanded: true,
properties: [
{
key: 'properties.variant',
label: 'Variant',
type: 'select',
options: [
{ value: 'primary', label: 'Primary (Theme)' },
{ value: 'secondary', label: 'Secondary (Theme)' },
{ value: 'custom', label: 'Custom (Manual)' }
],
getValue: (widget) => widget.properties['variant'] || 'primary',
setValue: (widget, value) => ({
...widget,
properties: { ...widget.properties, variant: value }
})
}
]
};
// ✅ Export all groups
export const MY_WIDGET_PROPERTY_GROUPS = [
MY_WIDGET_GROUP_CONTENT,
MY_WIDGET_GROUP_APPEARANCE
];
3. Register Widget (Make it discoverable)
Location: Property registry or widget initializer
// In widget module initialization
import { MY_WIDGET_PROPERTY_GROUPS } from './my-widget-property-groups';
propertyRegistry.registerPropertyGroups(MY_WIDGET_PROPERTY_GROUPS, {
widgetTypes: ['my-widget'],
replace: false
});
🎨 Creating Property Applicators (For New Property Types)
When Needed: When you have a new category of properties not covered by existing applicators
Example: Animation Properties
// libs/libs-widgets/widgets/src/lib/services/property-applicators/animation.applicator.ts
import { BreakpointKey } from '@libs-foundational/core-models';
import { BasePropertyApplicator } from './property-applicator.interface';
export class AnimationPropertyApplicator extends BasePropertyApplicator {
apply(props: Record<string, any>, breakpoint: BreakpointKey): Record<string, string> {
const styles: Record<string, string> = {};
// Transform
if (this.exists(props['transform'])) {
styles['transform'] = props['transform'];
}
// Transition
if (this.exists(props['transition'])) {
styles['transition'] = props['transition'];
}
// Animation
if (this.exists(props['animation'])) {
styles['animation'] = props['animation'];
}
return styles;
}
getHandledKeys(): string[] {
return ['transform', 'transition', 'animation'];
}
}
Register in Registry:
// In PropertyApplicatorRegistry constructor
this.register('animation', new AnimationPropertyApplicator());
Use in Widget:
smartStyles = usePropertyStyles(this.widget, this.activeBreakpoint, ['font', 'appearance', 'animation']);
🎨 Creating Intelligent Property Components (For Better UX)
When Needed: When default property editor isn't sufficient (need presets, visual editors, etc.)
Pattern (Follow AppearancePropertiesGroupComponent):
@Component({
selector: 'lib-spacing-properties-group',
standalone: true,
imports: [CommonModule, FormsModule],
template: `
<div class="spacing-panel">
<!-- Visual box model -->
<div class="box-model-editor">
<!-- Click sides to edit -->
</div>
<!-- Margin inputs -->
<div class="margin-controls">...</div>
<!-- Padding inputs -->
<div class="padding-controls">...</div>
<!-- Quick presets -->
<button (click)="applyPreset('compact')">Compact</button>
<button (click)="applyPreset('spacious')">Spacious</button>
</div>
`
})
export class SpacingPropertiesGroupComponent {
widget = input.required<Widget>();
onChange = input.required<(value: Record<string, any>) => void>();
// Load from backend, provide presets, track usage
}
Use in Property Group:
{
key: 'spacing',
type: 'custom',
component: SpacingPropertiesGroupComponent,
getValue: (widget) => widget.properties,
setValue: (widget, value) => ({ ...widget, properties: { ...widget.properties, ...value }})
}
🌟 Singularity Integration (Add Custom Logic)
Pattern (Established in ButtonWidget):
// 1. Import
import type { IWidgetFlowExecutorService } from '@libs-foundational/core-models';
import { WIDGET_FLOW_EXECUTOR_SERVICE } from '@libs-foundational/core-angular';
// 2. Inject (optional - won't crash if singularity not loaded)
private readonly flowExecutor = inject<IWidgetFlowExecutorService>(
WIDGET_FLOW_EXECUTOR_SERVICE,
{ optional: true }
);
// 3. Execute on event
async onWidgetEvent(event: Event): Promise<void> {
if (this.flowExecutor && this.isPreview()) {
await this.flowExecutor.executeFromWidget(
this.widget().id,
'eventType', // 'click', 'change', 'submit', etc.
{
widgetId: this.widget().id,
widgetType: this.widget().type,
eventType: 'eventType',
eventData: { /* event-specific data */ }
}
);
}
}
Benefit: Widget can trigger Singularity flows for custom business logic!
⚡ Flash Persistence (ALWAYS Use!)
For Widget State:
// ✅ Use autoFlashState for reactive, persisted state
import { autoFlashState } from '@storage-system';
readonly myState = autoFlashState('my-widget-state', defaultValue, {
database: 'editor-system',
collection: 'widget-state',
priority: 'critical', // 'critical' | 'high' | 'normal' | 'low'
ttl: 30 * 24 * 60 * 60 * 1000, // 30 days
debounceMs: 50 // Fast persistence
});
Benefits:
- ✅ Instant UI updates (< 1ms)
- ✅ Automatic persistence (IndexedDB)
- ✅ Cross-tab sync (BroadcastChannel)
- ✅ Backend sync (debounced, non-blocking)
Rule: ALWAYS use autoFlashState, NEVER manual localStorage!
❌ Common Mistakes to AVOID
1. Hardcoded Dimensions
// ❌ WRONG
:host {
width: 100%;
height: 100%;
}
// ✅ CORRECT
:host {
display: block;
box-sizing: border-box;
}
2. Manual Property Checking
// ❌ WRONG
containerStyles = computed(() => {
const props = this.widget()?.properties || {};
return {
'font-family': props['fontFamily'] || 'inherit',
// ... 98 more lines
};
});
// ✅ CORRECT
smartStyles = usePropertyStyles(this.widget, this.activeBreakpoint, ['font', 'appearance']);
3. Not Using Embeddables
// ❌ WRONG
<button class="custom-button">{{ text }}</button>
// ✅ CORRECT
<ui-button [text]="config().text"></ui-button>
4. Manual Persistence
// ❌ WRONG
async saveState() {
await localStorage.setItem('key', JSON.stringify(value));
}
// ✅ CORRECT
readonly state = autoFlashState('key', defaultValue, { /* config */ });
📊 Widget Checklist
Before submitting a new widget, verify:
- ✅ Extends BaseWidgetComponent
- ✅ Uses embeddable from ui-core/ui-forms
- ✅ Uses
usePropertyStyles()for style application - ✅ NO hardcoded width/height: 100%
- ✅ Property groups defined
- ✅ Singularity integration (if interactive)
- ✅ autoFlashState for any widget state
- ✅ Standalone component (no NgModules)
- ✅ OnPush change detection
- ✅ Signals for inputs (not @Input decorators)
- ✅ TypeScript strict mode compliant
- ✅ Follows naming conventions (kebab-case files)
🚀 Quick Start Template
Use this as starting point:
# 1. Create widget folder
mkdir libs/libs-widgets/widgets/src/lib/components/my-widget
# 2. Create files (use generator if available)
touch my-widget.component.ts
touch my-widget.component.html
touch my-widget.component.scss
touch my-widget.types.ts
touch my-widget-property-groups.ts
touch index.ts
# 3. Follow patterns from ButtonWidget (reference implementation)
# 4. Register property groups
# 5. Test in editor
# 6. Document
💎 Best Practices
Styling:
- ✅ Use theme CSS variables (
var(--ux-primary)) - ✅ Use PropertyApplicators for dynamic styles
- ✅ Separate theme-based from custom styles
- ✅ Platform-agnostic (JSON-serializable)
Performance:
- ✅ Computed signals (not manual change detection)
- ✅ OnPush strategy
- ✅ autoFlashState for state
- ✅ Minimal DOM nesting
Reusability:
- ✅ Use embeddables (don't duplicate UI)
- ✅ Property groups (backend-synced)
- ✅ Applicators (automatic conversion)
- ✅ Clear separation of concerns
🔗 Integration Points
Widget → Editor:
- Widget appears in palette via WidgetEcosystemConnector
- Properties panel auto-loads from PropertyRegistry
- Flash-persisted via EditorService.editorState
Widget → Modularity:
- Can be promoted via WidgetModulePackager
- Becomes embeddable in modules
- Reusable across platform
Widget → Singularity:
- Flows attach via WidgetFlowConfigComponent
- Execute via WidgetFlowExecutor
- Custom logic integration
📚 Reference Implementations
Perfect Examples to Study:
- ButtonWidget - Complete smart system, Singularity, embeddable use
- TextWidget - Smart applicators, font handling
- CardWidget - Complex embeddable usage
- InputWidget - Form integration, Singularity onChange
Location: libs/libs-widgets/widgets/src/lib/components/
🎯 The Golden Rules
- "Embeddables are pure UI, Widgets add editor logic"
- "Always use autoFlashState, never manual storage"
- "PropertyApplicators over manual checking"
- "ONE applicator → ALL widgets"
- "WidgetHost controls size, Widget renders content"
Last Updated: December 6, 2024
Status: ✅ Complete and Ready for Use
Next: Apply pattern to all widgets