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:

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:


🚀 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:

Performance:

Reusability:


🔗 Integration Points

Widget → Editor:

Widget → Modularity:

Widget → Singularity:


📚 Reference Implementations

Perfect Examples to Study:

  1. ButtonWidget - Complete smart system, Singularity, embeddable use
  2. TextWidget - Smart applicators, font handling
  3. CardWidget - Complex embeddable usage
  4. InputWidget - Form integration, Singularity onChange

Location: libs/libs-widgets/widgets/src/lib/components/


🎯 The Golden Rules

  1. "Embeddables are pure UI, Widgets add editor logic"
  2. "Always use autoFlashState, never manual storage"
  3. "PropertyApplicators over manual checking"
  4. "ONE applicator → ALL widgets"
  5. "WidgetHost controls size, Widget renders content"

Last Updated: December 6, 2024
Status: ✅ Complete and Ready for Use
Next: Apply pattern to all widgets