Sidebar Module
Multi-instance sidebar navigation with intelligent toggle directives
Sidebar Module
Multi-instance sidebar navigation system with intelligent toggle directives for the GoGrab Space application.
This section documents the sidebar module that powers multiple independent sidebar instances across different layouts, each with its own state management and toggle controls.
🎯 Quick Start
New to Sidebar Module?
The sidebar system supports two independent sidebar instances:
- Main-Layout Sidebar ("Tools" Navigation) - Controlled by top-nav button
- Main-Shell Sidebar ("Platform Navigation") - Controlled by breadcrumb button
Architecture Overview
Two Sidebar Instances
1. Main-Layout Sidebar ("Tools" Navigation)
- Location:
apps/gograb-space/src/app/components/layout/main-layout/ - Purpose: Shows development tools (Editor, Singularity, Space Map, etc.)
- Storage Key:
gograb-space:navigation:state:main-layout - Instance ID:
main-layout - Controlled by: Top-nav control button
- When Active: On main layout routes
2. Main-Shell Sidebar ("Platform Navigation")
- Location:
apps/gograb-space/src/app/core/layout/main-shell/ - Purpose: Shows platform features for space routes
- Storage Key:
gograb-space:navigation:state:main-shell - Instance ID:
main-shell - Controlled by: Breadcrumb sidebar toggle button
- When Active: On space routes (
/space/:spaceSlug/*)
Key Components
1. SidebarNavToggleDirective
Location: libs/libs-ui/ux-ui-core/src/lib/directives/sidebar-nav-toggle/
Purpose: Reusable directive for toggling sidebar navigation with instance-specific targeting.
Features:
- ✅ Instance-specific storage keys via
targetId - ✅ Three behaviors:
restore,cycle,toggle - ✅ Event-based reactivity (no continuous polling)
- ✅ Automatic button visibility based on sidebar state
- ✅ Custom styling and accessibility support
- ✅ Cross-tab synchronization
- ✅ Same-tab custom events
Basic Usage:
<button sidebarNavToggle
[sidebarNavToggleConfig]="{
targetId: 'main-shell',
behavior: 'restore',
styling: {
icon: 'fas fa-bars',
text: 'Navigation',
showText: true
}
}"
(navigationStateChange)="onNavigationStateChange($event)">
</button>
Configuration Interface:
export interface SidebarNavToggleConfig {
targetId?: string; // Instance ID (default: 'main-shell')
behavior?: 'restore' | 'cycle' | 'toggle';
enabled?: boolean;
styling?: {
icon?: string;
text?: string;
showText?: boolean;
buttonClasses?: string;
};
accessibility?: {
ariaLabel?: string;
tooltip?: string;
};
}
Behavior Types:
restore- Shows sidebar when hidden (one-way toggle)cycle- Cycles through: visible → collapsed → hidden → visibletoggle- Toggles between visible and hidden only
2. Sidebar-Nav Component
Location: modules/structural/sidebar-nav/
Key Feature: Smart state management that:
- Uses external state when provided via
[navigationState]input - Disables its own persistence when externally controlled
- Only persists state when managing itself (no parent control)
Implementation:
// Persistence effect - only saves if NOT externally controlled
this.statePersistenceEffectRef = effect(() => {
const cfg = this.effectiveConfig();
const externalState = this.navigationState();
// Only persist if persistState is enabled AND we're NOT controlled by parent
if (cfg.threeState?.persistState && externalState === undefined) {
const state = this._state(); // Use internal state
const storageKey = cfg.threeState?.storageKey || 'sidebar-nav:state';
localStorage.setItem(storageKey, JSON.stringify({
state,
timestamp: Date.now()
}));
}
});
Usage:
<!-- Parent-controlled (no self-persistence) -->
<module-sidebar-nav
[navigationState]="parentNavigationState()"
(navigationStateChange)="onNavigationStateChange($event)"
[config]="sidebarConfig()">
</module-sidebar-nav>
<!-- Self-managed (with persistence) -->
<module-sidebar-nav
[config]="{ threeState: { persistState: true } }">
</module-sidebar-nav>
3. ResponsiveHeightDirective
Location: libs/libs-ui/ux-ui-core/src/lib/directives/responsive-height/
Purpose: Dynamically adjusts element height based on top-nav state.
Usage:
<div class="structure-layout-redesigned"
responsiveHeight
[responsiveHeight]="{
property: 'min-height',
visibleOffset: 396,
hiddenOffset: 316
}">
</div>
Configuration:
export interface ResponsiveHeightConfig {
property?: 'height' | 'min-height' | 'max-height' | 'width' | 'min-width' | 'max-width';
baseValue?: string; // Default: '100vh'
visibleOffset?: number; // Pixels to subtract when top-nav visible
hiddenOffset?: number; // Pixels to subtract when top-nav hidden
storageKey?: string; // Default: 'gograb-space:top-nav:state'
reactive?: boolean; // Default: true
}
State Management
Storage Keys (Instance-Specific)
// Main-layout sidebar
'gograb-space:navigation:state:main-layout'
// Main-shell sidebar
'gograb-space:navigation:state:main-shell'
// Top navigation (global)
'gograb-space:top-nav:state'
Why Instance-Specific?
- Prevents conflicts between multiple sidebars
- Each sidebar can have different state
- Enables independent control
- Supports future expansion (more sidebars)
Event Flow Diagrams
Main-Shell Sidebar Toggle (Breadcrumb Button)
┌─────────────────────────────────────────────────────────────┐
│ User clicks breadcrumb sidebar toggle button │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Directive saves to: │
│ gograb-space:navigation:state:main-shell │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Directive dispatches: │
│ CustomEvent('navigation-state-changed') │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Directive emits: │
│ navigationStateChange event │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Main-layout receives event │
│ → onSidebarNavToggle() forwards to main-shell storage │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Main-shell listens to: │
│ - CustomEvent('navigation-state-changed') │
│ - StorageEvent (cross-tab) │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Main-shell updates: navigationState signal │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 7. Sidebar-nav receives: updated [navigationState] input │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 8. Sidebar-nav reacts: updates display (NO self-persist) │
└─────────────────────────────────────────────────────────────┘
Main-Layout Sidebar Toggle (Top-Nav Button)
┌─────────────────────────────────────────────────────────────┐
│ User clicks top-nav control button │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 1. Top-nav calls: toggleNavigation() │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. Top-nav emits: navigationToggle event │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. Main-layout receives: onNavigationToggle() │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. Main-layout calls: setNavigationState() │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. Saves to: gograb-space:navigation:state:main-layout │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. Updates: navigationState signal │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 7. Sidebar-nav receives: updated [navigationState] input │
└─────────────────────┬───────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 8. Sidebar-nav reacts: updates display │
└─────────────────────────────────────────────────────────────┘
Implementation Details
Main-Shell Component
File: apps/gograb-space/src/app/core/layout/main-shell/main-shell.component.ts
export class MainShellComponent implements OnInit, OnDestroy {
private storageKey = 'gograb-space:navigation:state:main-shell';
readonly navigationState = signal<NavigationState>(this.loadInitialState());
constructor() {
// Listen to StorageEvent (cross-tab)
window.addEventListener('storage', this.handleStorageChange);
// Listen to custom event (same-tab)
window.addEventListener('navigation-state-changed', this.handleCustomEvent);
}
onNavigationStateChange(newState: NavigationState): void {
console.log('🔄 [MainShell] Navigation state changed:', newState);
this.navigationState.set(newState);
// Save to instance-specific storage
try {
localStorage.setItem(this.storageKey, JSON.stringify({
state: newState,
timestamp: Date.now()
}));
// Dispatch custom event for same-tab reactivity
window.dispatchEvent(new CustomEvent('navigation-state-changed', {
detail: { state: newState, source: 'main-shell', timestamp: Date.now() }
}));
} catch (error) {
console.warn('⚠️ [MainShell] Failed to save navigation state:', error);
}
}
private loadInitialState(): NavigationState {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
const { state } = JSON.parse(stored);
return state;
}
} catch (error) {
console.warn('⚠️ [MainShell] Failed to load initial state:', error);
}
return 'visible'; // Default
}
}
Template: apps/gograb-space/src/app/core/layout/main-shell/main-shell.component.html
<module-sidebar-nav
[navigationState]="navigationState()"
(navigationStateChange)="onNavigationStateChange($event)"
[config]="sidebarConfig()">
</module-sidebar-nav>
Main-Layout Bridge
File: apps/gograb-space/src/app/components/layout/main-layout/main-layout.component.ts
/**
* Handle top-nav sidebar toggle (main-layout's own sidebar)
*/
onNavigationToggle(newState: NavigationState): void {
console.log('🔄 [MainLayout] Navigation toggle received:', newState);
this.setNavigationState(newState);
}
/**
* Handle sidebar nav toggle from breadcrumb (targets main-shell's sidebar)
*/
onSidebarNavToggle(newState: NavigationState): void {
console.log('🔄 [MainLayout] Sidebar nav toggle received (for main-shell):', newState);
// Save to main-shell's storage key so main-shell picks it up
try {
const stateInfo = {
state: newState,
timestamp: Date.now()
};
localStorage.setItem('gograb-space:navigation:state:main-shell', JSON.stringify(stateInfo));
// Dispatch custom event for main-shell to pick up immediately
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('navigation-state-changed', {
detail: { state: newState, source: 'main-layout-breadcrumb', timestamp: Date.now() }
}));
}
} catch (error) {
console.warn('[MainLayout] Failed to save main-shell navigation state:', error);
}
}
Template: apps/gograb-space/src/app/components/layout/main-layout/main-layout.component.html
<lib-breadcrumb
[config]="{ /* ... */ }"
[topNavToggleConfig]="{ /* ... */ }"
(topNavStateChange)="onTopNavToggle($event)"
(navigationStateChange)="onSidebarNavToggle($event)">
</lib-breadcrumb>
Breadcrumb Integration
File: libs/libs-ui/ux-ui-core/src/lib/breadcrumb/components/breadcrumb.component.ts
export class BreadcrumbComponent {
readonly _sidebarNavToggleConfig = input<Partial<SidebarNavToggleConfig>>({});
// Explicitly target main-shell's sidebar
readonly sidebarNavToggleConfigValue = computed(() => ({
...this._sidebarNavToggleConfig(),
targetId: 'main-shell', // Explicit targeting
}));
}
Template: libs/libs-ui/ux-ui-core/src/lib/breadcrumb/components/breadcrumb.component.html
<div class="context-breadcrumb" role="navigation">
<!-- Sidebar Navigation Toggle Button - Left side -->
<button sidebarNavToggle
class="breadcrumb-sidebar-toggle-btn"
[sidebarNavToggleConfig]="sidebarNavToggleConfigValue()"
(navigationStateChange)="onNavigationStateChange($event)">
</button>
<!-- Breadcrumb Navigation -->
<nav class="breadcrumb-nav">
<!-- breadcrumb items -->
</nav>
<!-- Top Nav Toggle -->
@if (shouldShowMetrics() && currentFeature()) {
<div class="feature-info">
<button topNavToggle [topNavToggleConfig]="topNavToggleConfig()">
</button>
</div>
}
</div>
Reactivity System
Event-Based Architecture (No Polling!)
The system uses pure event-based reactivity with NO continuous polling:
- StorageEvent - Cross-tab/window communication
- CustomEvent - Same-tab communication
- Angular Events - Component-to-component communication
- Signals - Reactive state updates
Exception: Top-Nav Toggle Button
The top-nav-toggle directive uses throttled polling (100ms) only for reactive UI updates:
ngOnInit(): void {
// Trigger recomputation for reactive button visibility
setInterval(() => {
this.stateRefresh.update(v => v + 1);
}, 100);
}
Why? Button visibility and styling need to react to external state changes immediately for smooth UX.
Testing Checklist
✅ Functionality Tests
- Breadcrumb button toggles main-shell sidebar (Platform Navigation)
- Top-nav button toggles main-layout sidebar (Tools)
- Both sidebars operate independently
- State persists across page reloads
- Button visibility reacts to sidebar state
- Icons and tooltips update correctly
✅ Performance Tests
- No infinite loops
- No excessive storage reads
- Cross-tab synchronization works
- Same-tab reactivity is immediate
- Responsive height adjusts with top-nav state
✅ UX Tests
- Buttons are accessible (aria-labels, tooltips)
- Smooth animations
- Clear visual feedback
- Keyboard shortcuts work
Common Issues & Solutions
Issue: Button Toggles Wrong Sidebar
Cause: Missing or incorrect targetId in directive config.
Solution: Explicitly set targetId:
sidebarNavToggleConfig: {
targetId: 'main-shell' // or 'main-layout'
}
Issue: Button Not Reacting to State Changes
Cause: Directive not listening to custom events.
Solution: Ensure setupReactivity() listens to both StorageEvent and CustomEvent:
setupReactivity(): void {
window.addEventListener('storage', this.handleStorageChange);
window.addEventListener('navigation-state-changed', this.handleCustomEvent);
}
Issue: Sidebar Persisting When Externally Controlled
Cause: Sidebar-nav persisting even with external state.
Solution: Check for external state before persisting:
effect(() => {
const externalState = this.navigationState();
// Only persist if NOT externally controlled
if (cfg.threeState?.persistState && externalState === undefined) {
localStorage.setItem(storageKey, JSON.stringify({ state, timestamp }));
}
});
Issue: Infinite Storage Reads
Cause: Polling instead of events, or effect triggering itself.
Solution: Use event-based reactivity and guard effects:
effect(() => {
const externalState = this.navigationState();
// Guard: only run if NOT externally controlled
if (externalState === undefined) {
// Safe to persist
}
});
Issue: InvalidCharacterError with Icons
Cause: Adding icon string with spaces (e.g., "fas fa-bars") as single class.
Solution: Split icon string and add each class individually:
private updateButtonContent(): void {
const icon = this.buttonIcon();
if (icon) {
// Split icon string and add each class separately
const iconClasses = icon.split(' ').filter(c => c.trim());
iconClasses.forEach(cls => {
this.renderer.addClass(iconEl, cls);
});
}
}
Files Modified
Core Directives
libs/libs-ui/ux-ui-core/src/lib/directives/sidebar-nav-toggle/sidebar-nav-toggle.directive.tslibs/libs-ui/ux-ui-core/src/lib/directives/responsive-height/responsive-height.directive.tslibs/libs-ui/ux-ui-core/src/lib/directives/top-nav-toggle/top-nav-toggle.directive.ts
Layout Components
apps/gograb-space/src/app/core/layout/main-shell/main-shell.component.tsapps/gograb-space/src/app/core/layout/main-shell/main-shell.component.htmlapps/gograb-space/src/app/components/layout/main-layout/main-layout.component.tsapps/gograb-space/src/app/components/layout/main-layout/main-layout.component.htmlapps/gograb-space/src/app/components/layout/main-layout/top-nav/top-nav.component.ts
UI Components
libs/libs-ui/ux-ui-core/src/lib/breadcrumb/components/breadcrumb.component.tslibs/libs-ui/ux-ui-core/src/lib/breadcrumb/components/breadcrumb.component.htmllibs/libs-ui/ux-ui-core/src/lib/breadcrumb/components/breadcrumb.component.scss
Modules
modules/structural/sidebar-nav/src/lib/components/sidebar-nav/sidebar-nav.component.tsmodules/structural/sidebar-nav/src/lib/components/sidebar-nav/sidebar-nav.component.html
Future Improvements
1. Storage System Integration
Replace direct localStorage with @storage-system API for:
- Type safety
- Better error handling
- Flash persistence
- Unified storage interface
2. Directive Registry
Create central registry for all sidebar instances:
- Auto-discovery of sidebars
- Dynamic targetId validation
- Global state coordination
- Debugging tools
3. Animation Sync
Coordinate animations across multiple sidebars:
- Prevent layout shift
- Smooth transitions
- Performance optimization
- Consistent timing
4. Smart Defaults
Auto-detect which sidebar to toggle based on:
- Current route
- Active layout
- User preferences
- Historical usage
5. Keyboard Shortcuts
Global shortcuts for each sidebar instance:
Ctrl+\\- Toggle main sidebarCtrl+Shift+\\- Toggle secondary sidebarCtrl+B- Show breadcrumb sidebar- Custom shortcuts per instance
Architecture Benefits
1. Separation of Concerns
- Directives handle DOM manipulation and events
- Components manage business logic and state
- Services handle persistence (future)
2. Reusability
- Same directive works for multiple sidebars
- Configuration-driven behavior
- Easy to add new instances
3. Performance
- Event-based (no continuous polling except button UI)
- Minimal re-renders
- Efficient DOM updates
- Throttled computations
4. Maintainability
- Clear naming conventions
- Instance-specific storage keys
- Self-documenting code
- Easy debugging
5. Scalability
- Add new sidebars without refactoring
- Independent state management
- No conflicts between instances
- Future-proof architecture
Status
Status: ✅ PRODUCTION READY Last Updated: November 12, 2025 Contributors: AI Assistant, User
What Works
- ✅ Breadcrumb button toggles main-shell sidebar
- ✅ Top-nav button toggles main-layout sidebar
- ✅ Independent state management
- ✅ Cross-tab synchronization
- ✅ Same-tab reactivity
- ✅ Responsive height calculations
- ✅ Accessibility support
- ✅ Error handling
Known Limitations
- Direct
localStorageusage (not using@storage-systemyet) - No directive registry
- Manual
targetIdconfiguration required - No global keyboard shortcuts
Next Steps
- Integrate with
@storage-system - Create directive registry
- Add keyboard shortcuts
- Implement smart defaults
- Add animation synchronization
For detailed implementation examples, see the source files listed above.