Add New Widget

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

How to Add a New UX Widget to the UX Test App

This guide explains how to add new UX widgets to our monorepo's ux-test application, including proper storage integration, AI generation capabilities, and documentation.

Overview

The UX Test app (apps/ux-test) is our showcase and testing ground for UI components. When adding a new widget, you should:

  1. Create the widget component with demo variations
  2. Implement persistent storage using IndexedDB
  3. Add AI generation capabilities
  4. Document the widget properly
  5. Integrate with the existing navigation

Step 1: Create the Widget Demo Component

1.1 Generate the Component

# Navigate to the ux-widgets-demos directory
cd apps/ux-test/src/app/ux-widgets-demos

# Create a new directory for your widget
mkdir ux-your-widget
cd ux-your-widget

# Create the component file
touch ux-your-widget-demo.component.ts

1.2 Basic Component Structure

import { Component, signal, computed, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { UxYourWidgetComponent } from 'libs/ux-ui-extended/src/lib/components'; // or wherever your widget is
import type { SafeAny } from '@libs-foundational/core-models'; 

@Component({
  selector: 'app-ux-your-widget-demo',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    UxYourWidgetComponent
  ],
  template: `
    <div class="widget-demo-container">
      <header class="demo-header">
        <h1>🎨 UX Your Widget Demo</h1>
        <p>Interactive showcase of the Your Widget component</p>
      </header>

      <!-- Configuration Panel -->
      <section class="config-section">
        <h2>⚙️ Configuration</h2>
        <div class="config-grid">
          <!-- Add your configuration controls here -->
        </div>
      </section>

      <!-- Widget Demo -->
      <section class="demo-section">
        <h2>📱 Live Demo</h2>
        <div class="demo-container">
          <ux-your-widget
            [config]="widgetConfig()"
            [data]="widgetData()"
            (change)="onWidgetChange($event)">
          </ux-your-widget>
        </div>
      </section>

      <!-- Variations -->
      <section class="variations-section">
        <h2>🎭 Variations</h2>
        <div class="variations-grid">
          @for (variation of savedVariations(); track variation.id) {
            <div class="variation-card">
              <h3>{{ variation.name }}</h3>
              <button (click)="loadVariation(variation)">Load</button>
              <button (click)="deleteVariation(variation.id)">Delete</button>
            </div>
          }
        </div>
      </section>

      <!-- AI Generation -->
      <section class="ai-section">
        <h2>🤖 AI Generation</h2>
        <div class="ai-controls">
          <input
            type="text"
            [(ngModel)]="aiPrompt"
            placeholder="Describe your widget configuration..."
            (keyup.enter)="generateWithAI()">
          <button (click)="generateWithAI()" [disabled]="isGenerating()">
            {{ isGenerating() ? 'Generating...' : 'Generate' }}
          </button>
        </div>
      </section>
    </div>
  `,
  styleUrls: ['./ux-your-widget-demo.component.scss']
})
export class UxYourWidgetDemoComponent {
  // Services
  private storageService = inject(UxYourWidgetStorageService);
  private aiService = inject(YourWidgetAIService);

  // Configuration
  widgetConfig = signal({
    // Your widget configuration
  });

  widgetData = signal<SafeAny[]>([
    // Your widget data
  ]);

  // Storage
  savedVariations = signal<WidgetVariation[]>([]);
  
  // AI
  aiPrompt = '';
  isGenerating = signal(false);

  constructor() {
    this.loadVariations();
  }

  // Widget event handlers
  onWidgetChange(event: SafeAny): void {
    console.log('Widget changed:', event);
  }

  // Storage methods
  private loadVariations(): void {
    this.storageService.getVariations().subscribe(variations => {
      this.savedVariations.set(variations);
    });
  }

  saveCurrentVariation(name: string): void {
    const variation: WidgetVariation = {
      id: `variation-${Date.now()}`,
      name,
      config: this.widgetConfig(),
      data: this.widgetData(),
      createdAt: Date.now()
    };

    this.storageService.saveVariation(variation).subscribe(() => {
      this.loadVariations();
    });
  }

  loadVariation(variation: WidgetVariation): void {
    this.widgetConfig.set(variation.config);
    this.widgetData.set(variation.data);
  }

  deleteVariation(id: string): void {
    this.storageService.deleteVariation(id).subscribe(() => {
      this.loadVariations();
    });
  }

  // AI generation
  async generateWithAI(): Promise<void> {
    if (!this.aiPrompt.trim() || this.isGenerating()) return;

    this.isGenerating.set(true);
    try {
      const result = await this.aiService.generateConfiguration(this.aiPrompt);
      if (result) {
        this.widgetConfig.set(result.config);
        this.widgetData.set(result.data);
        
        // Auto-save AI-generated variation
        this.saveCurrentVariation(`AI: ${this.aiPrompt.slice(0, 30)}...`);
      }
    } catch (error) {
      console.error('AI generation failed:', error);
    } finally {
      this.isGenerating.set(false);
    }
  }
}

interface WidgetVariation {
  id: string;
  name: string;
  config: SafeAny;
  data: SafeAny[];
  createdAt: number;
}

Step 2: Create Storage Service

Every widget should have its own IndexedDB storage for persistence.

2.1 Create the Storage Service

// apps/ux-test/src/app/ux-widgets-demos/shared/ux-your-widget-storage.service.ts

import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, from, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import type { SafeAny } from '@libs-foundational/core-models'; 

@Injectable({
  providedIn: 'root'
})
export class UxYourWidgetStorageService implements OnDestroy {
  private readonly DB_NAME = 'ux-your-widget-db';
  private readonly DB_VERSION = 1;
  private readonly STORE_NAME = 'widget-variations';

  private db: IDBDatabase | null = null;
  private dbReady$ = new BehaviorSubject<boolean>(false);
  private variations$ = new BehaviorSubject<WidgetVariation[]>([]);

  constructor() {
    this.initDatabase();
  }

  ngOnDestroy(): void {
    if (this.db) {
      this.db.close();
    }
  }

  private async initDatabase(): Promise<void> {
    try {
      const request = indexedDB.open(this.DB_NAME, this.DB_VERSION);

      request.onupgradeneeded = (event) => {
        const db = (event.target as IDBOpenDBRequest).result;
        
        if (!db.objectStoreNames.contains(this.STORE_NAME)) {
          const store = db.createObjectStore(this.STORE_NAME, { keyPath: 'id' });
          store.createIndex('createdAt', 'createdAt', { unique: false });
          store.createIndex('name', 'name', { unique: false });
        }
      };

      request.onsuccess = (event) => {
        this.db = (event.target as IDBOpenDBRequest).result;
        this.dbReady$.next(true);
        this.loadAllVariations();
      };

      request.onerror = (event) => {
        console.error('Failed to open database:', event);
        this.dbReady$.next(false);
      };
    } catch (error) {
      console.error('Error initializing database:', error);
      this.dbReady$.next(false);
    }
  }

  getVariations(): Observable<WidgetVariation[]> {
    return this.variations$.asObservable();
  }

  saveVariation(variation: WidgetVariation): Observable<boolean> {
    return from(this.saveToDb(variation)).pipe(
      tap(() => this.loadAllVariations()),
      map(() => true),
      catchError(error => {
        console.error('Error saving variation:', error);
        return of(false);
      })
    );
  }

  deleteVariation(id: string): Observable<boolean> {
    return from(this.deleteFromDb(id)).pipe(
      tap(() => this.loadAllVariations()),
      map(() => true),
      catchError(error => {
        console.error('Error deleting variation:', error);
        return of(false);
      })
    );
  }

  private async saveToDb(variation: WidgetVariation): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');
    
    const transaction = this.db.transaction([this.STORE_NAME], 'readwrite');
    const store = transaction.objectStore(this.STORE_NAME);
    
    return new Promise((resolve, reject) => {
      const request = store.put(variation);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  private async deleteFromDb(id: string): Promise<void> {
    if (!this.db) throw new Error('Database not initialized');
    
    const transaction = this.db.transaction([this.STORE_NAME], 'readwrite');
    const store = transaction.objectStore(this.STORE_NAME);
    
    return new Promise((resolve, reject) => {
      const request = store.delete(id);
      request.onsuccess = () => resolve();
      request.onerror = () => reject(request.error);
    });
  }

  private loadAllVariations(): void {
    if (!this.db) return;
    
    const transaction = this.db.transaction([this.STORE_NAME], 'readonly');
    const store = transaction.objectStore(this.STORE_NAME);
    const request = store.getAll();
    
    request.onsuccess = () => {
      this.variations$.next(request.result || []);
    };
  }
}

Step 3: Create AI Service

Add AI generation capabilities for your widget.

3.1 Create the AI Service

// apps/ux-test/src/app/ux-widgets-demos/shared/your-widget-ai.service.ts

import { Injectable, inject } from '@angular/core';
import { MenuGenerationService } from '@gograb-ai';
import type { SafeAny } from '@libs-foundational/core-models'; 

@Injectable({
  providedIn: 'root'
})
export class YourWidgetAIService {
  private menuGenService = inject(MenuGenerationService);

  async generateConfiguration(prompt: string): Promise<{
    config: SafeAny;
    data: SafeAny[];
  } | null> {
    try {
      // Implement your AI generation logic
      // This can use the existing AI services or implement custom logic
      
      // For now, return a mock implementation
      return this.generateMockConfiguration(prompt);
    } catch (error) {
      console.error('AI generation error:', error);
      return null;
    }
  }

  private generateMockConfiguration(prompt: string): {
    config: SafeAny;
    data: SafeAny[];
  } {
    // Analyze the prompt and generate appropriate configuration
    const lowerPrompt = prompt.toLowerCase();
    
    // Return configuration based on prompt analysis
    return {
      config: {
        // Generated configuration based on prompt
        theme: lowerPrompt.includes('dark') ? 'dark' : 'light',
        animated: lowerPrompt.includes('animated') || lowerPrompt.includes('animation'),
        size: lowerPrompt.includes('large') ? 'lg' : lowerPrompt.includes('small') ? 'sm' : 'md'
      },
      data: [
        // Generated data based on prompt
        { id: 1, label: 'Generated Item 1', value: Math.random() * 100 },
        { id: 2, label: 'Generated Item 2', value: Math.random() * 100 }
      ]
    };
  }
}

Step 4: Add to Navigation

4.1 Update the Routes

Add your widget demo to the app routes:

// apps/ux-test/src/app/app.routes.ts

export const appRoutes: Route[] = [
  // ... existing routes
  {
    path: 'ux-widgets',
    children: [
      // ... existing widget routes
      {
        path: 'your-widget',
        loadComponent: () =>
          import('./ux-widgets-demos/ux-your-widget/ux-your-widget-demo.component')
            .then(m => m.UxYourWidgetDemoComponent),
        data: { title: 'Your Widget Demo' }
      }
    ]
  }
];

4.2 Update the Navigation Menu

Add your widget to the navigation menu in app.component.ts:

// In the uxWidgetsMenuItems array
{
  id: 'your-widget',
  title: 'Your Widget',
  icon: '🎨',
  route: '/ux-widgets/your-widget',
  badge: 'New'
}

Step 5: Style Your Demo

Create appropriate styles for your demo:

// ux-your-widget-demo.component.scss

.widget-demo-container {
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
}

.demo-header {
  text-align: center;
  margin-bottom: 3rem;

  h1 {
    font-size: 2.5rem;
    margin-bottom: 0.5rem;
  }

  p {
    color: var(--ux-text-secondary);
    font-size: 1.1rem;
  }
}

.config-section,
.demo-section,
.variations-section,
.ai-section {
  margin-bottom: 3rem;

  h2 {
    margin-bottom: 1.5rem;
    display: flex;
    align-items: center;
    gap: 0.5rem;
  }
}

.config-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 1rem;
}

.demo-container {
  background: var(--ux-surface);
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
}

.variations-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 1rem;
}

.variation-card {
  background: var(--ux-surface);
  padding: 1rem;
  border-radius: 8px;
  border: 1px solid var(--ux-border);

  h3 {
    margin-bottom: 0.5rem;
    font-size: 1rem;
  }

  button {
    margin-right: 0.5rem;
  }
}

.ai-controls {
  display: flex;
  gap: 1rem;
  align-items: center;

  input {
    flex: 1;
    padding: 0.75rem;
    border-radius: 8px;
    border: 1px solid var(--ux-border);
  }

  button {
    padding: 0.75rem 1.5rem;
    background: var(--ux-primary);
    color: white;
    border: none;
    border-radius: 8px;
    cursor: pointer;

    &:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }
  }
}

Best Practices

1. Storage Organization

2. Component Architecture

3. AI Integration

4. Documentation

Example Implementation

For a complete example, see the existing widget demos in:

These implementations follow all the patterns described in this guide and can serve as templates for new widgets.


Last Updated: 2025-01-12 This guide ensures consistency across all UX widget implementations in the monorepo.