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:
- Create the widget component with demo variations
- Implement persistent storage using IndexedDB
- Add AI generation capabilities
- Document the widget properly
- 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
- Each widget should have its own database with a unique name
- Use meaningful object store names that describe the data type
- Always include proper error handling for IndexedDB operations
- Implement proper cleanup in the service's
ngOnDestroy
2. Component Architecture
- Use Angular 20 standalone components with signals
- Keep configuration separate from presentation
- Implement proper TypeScript interfaces for all data structures
- Use computed signals for derived state
3. AI Integration
- Design prompts that are specific to your widget's functionality
- Implement proper error handling for AI generation failures
- Provide meaningful fallbacks when AI services are unavailable
- Auto-save AI-generated configurations for user convenience
4. Documentation
- Include comprehensive component documentation
- Provide usage examples in the demo
- Document all configuration options
- Include troubleshooting information
Example Implementation
For a complete example, see the existing widget demos in:
apps/ux-test/src/app/ux-widgets-demos/ux-menu/apps/ux-test/src/app/ux-widgets-demos/ux-panel/
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.