Ux Menu

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

UX Menu Component

The UX Menu is a flexible, customizable navigation menu component that supports multiple levels of nested items, search functionality, and persistent configuration. It can be used anywhere in an application where hierarchical navigation is needed.

Core Features

Architecture

The UX Menu system consists of several interconnected components and services:

Components

Services

Models

Configuration Persistence

The UX Menu uses a similar persistence architecture to the UX Dashboard:

  1. IndexedDB Storage

    • Primary storage for menu configurations
    • Configurations identified by unique menu IDs
    • Observable updates when configurations change
  2. Source Tracking

    • Prevents circular updates between components
    • Each save operation includes a source identifier
    • Components can filter out updates from themselves

Implementation Details

Menu Item Structure

Menu items are structured hierarchically using the UxMenuItem interface:

interface UxMenuItem {
  id: string;
  label: string;
  icon?: string;
  route?: string;
  badge?: string;
  badgeColor?: string;
  expanded?: boolean;
  children?: UxMenuItem[];
  disabled?: boolean;
  visible?: boolean;
  data?: any;
}

This structure allows for:

Menu Configuration

The menu is configured using the UxMenuConfig interface:

interface UxMenuConfig {
  id: string;
  theme?: 'light' | 'dark';
  position?: 'left' | 'right' | 'top' | 'bottom';
  showIcons?: boolean;
  showBadges?: boolean;
  allowMultipleExpanded?: boolean;
  animationDuration?: number;
  closeOnNavigation?: boolean;
  hasOverlay?: boolean;
  overlayOpacity?: number;
  backdropBlur?: boolean;
  zIndex?: number;
  persistConfig?: boolean;
  autoLoadSavedConfig?: boolean;
}

Storage Integration

The menu integrates with the storage service to save and load configurations:

// Saving a configuration
this.menuStorageService.saveMenuConfig(
  this.menuId,
  this.menuConfig(),
  this.menuItems(),
  'menu-component-id'
).subscribe({
  next: success => {
    console.log('Menu configuration saved successfully');
  }
});

// Loading a configuration
this.menuStorageService.loadMenuConfig(this.menuId).subscribe({
  next: storedItem => {
    if (storedItem) {
      this.menuConfig.set(storedItem.config);
      this.menuItems.set(storedItem.items);
    }
  }
});

Preventing Circular Updates

Like the UX Dashboard, the UX Menu implements strategies to prevent circular updates:

  1. Source Tracking

    // When saving a configuration
    this.menuStorageService.saveMenuConfig(
      menuId,
      config,
      items,
      'menu-component-id'
    );
    
    // When receiving updates
    this.menuStorageService.configUpdated$.subscribe(update => {
      if (update.source === 'menu-component-id') {
        return; // Ignore own updates
      }
      // Process update
    });
    
  2. Hash-Based Change Detection

    // Generate hash of current config
    const configHash = this.generateConfigHash(config, items);
    
    // Skip save if unchanged
    if (configHash === this.lastSavedHash) {
      return;
    }
    

Event-Based Communication

The UX Menu uses RxJS subjects for event-based communication:

// In the storage service
private configUpdatedSubject = new Subject<{item: UxMenuStorageItem, source: string}>();
public configUpdated$ = this.configUpdatedSubject.asObservable();

// When saving
this.configUpdatedSubject.next({ item: storageItem, source });

// In the components
this.menuStorageService.configUpdated$.pipe(
  takeUntil(this.destroy$)
).subscribe(update => {
  // Handle update
});

Usage Example

Here's how to use the UX Menu component:

// In your component class
menuConfig = signal<UxMenuConfig>({
  id: 'main-menu',
  theme: 'light',
  showIcons: true,
  showBadges: true,
  persistConfig: true
});

menuItems = signal<UxMenuItem[]>([
  {
    id: 'dashboard',
    label: 'Dashboard',
    icon: 'dashboard',
    route: '/dashboard'
  },
  {
    id: 'users',
    label: 'Users',
    icon: 'people',
    children: [
      {
        id: 'user-list',
        label: 'User List',
        route: '/users/list'
      },
      {
        id: 'user-groups',
        label: 'User Groups',
        route: '/users/groups',
        badge: '3',
        badgeColor: '#ff4081'
      }
    ]
  }
]);

// Handle menu item selection
onMenuItemSelected(item: UxMenuItem): void {
  console.log('Selected menu item:', item);
  // Navigate or perform action
}
<!-- In your component template -->
<app-ux-menu
  [id]="'main-menu'"
  [config]="menuConfig()"
  [items]="menuItems()"
  (itemSelected)="onMenuItemSelected($event)">
</app-ux-menu>

Lessons Learned

Our implementation of the UX Menu system taught us several valuable lessons:

  1. Consistent patterns across widgets

    • Using similar patterns for the menu and dashboard simplified development
    • Common approaches to storage, configuration, and updates reduced code duplication
  2. Selective item expansion

    • Initially, expanding a menu item would collapse all others
    • Adding the allowMultipleExpanded option provided needed flexibility
  3. Proper cleanup is essential

    • Using takeUntil(destroy$) for all subscriptions prevents memory leaks
    • Ensuring all listeners are removed on component destruction is crucial

Troubleshooting

Menu Configuration Not Persisting

If menu configurations aren't persisting:

  1. Check that the persistConfig property is set to true in the menu configuration
  2. Ensure that the UxMenuStorageService is properly initialized
  3. Verify that the menu ID is consistent and unique

Menu Items Not Expanding/Collapsing

If menu items aren't expanding or collapsing properly:

  1. Check that the children property is correctly defined for parent items
  2. Verify that the expanded property is being properly toggled
  3. Ensure that the allowMultipleExpanded configuration is set appropriately

Best Practices

  1. Organize menu items hierarchically

    const menuItems = [
      {
        id: 'parent',
        label: 'Parent Item',
        children: [
          { id: 'child1', label: 'Child 1' },
          { id: 'child2', label: 'Child 2' }
        ]
      }
    ];
    
  2. Use badges sparingly

    • Badges should convey important information (like counts or status)
    • Too many badges can create visual noise
  3. Implement proper source tracking

    menuStorageService.saveMenuConfig(id, config, items, 'unique-component-id');
    
  4. Always unsubscribe from observables

    ngOnDestroy(): void {
      this.destroy$.next();
      this.destroy$.complete();
    }