UNPKG

@fsegurai/ngx-sidenav

Version:

Angular component for creating a resizeable sidenav with drag and drop functionality.

729 lines (630 loc) β€’ 19.9 kB
<p align="center" class="intro"> <img alt="NGX Sidenav Logo" src="https://raw.githubusercontent.com/fsegurai/ngx-sidenav/main/public/ngx-sidenav.png"> </p> <p align="center" class="intro"> <a href="https://github.com/fsegurai/ngx-sidenav"> <img src="https://img.shields.io/azure-devops/build/fsegurai/93779823-473d-4fb3-a5b1-27aaa1a88ea2/30/main?label=Build%20Status&" alt="Build Main Status"> </a> <a href="https://github.com/fsegurai/ngx-sidenav/releases/latest"> <img src="https://img.shields.io/github/v/release/fsegurai/ngx-sidenav" alt="Latest Release"> </a> <br> <img alt="GitHub contributors" src="https://img.shields.io/github/contributors/fsegurai/ngx-sidenav"> <img alt="Dependency status for repo" src="https://img.shields.io/librariesio/github/fsegurai/ngx-sidenav"> <a href="https://opensource.org/licenses/MIT"> <img alt="GitHub License" src="https://img.shields.io/github/license/fsegurai/ngx-sidenav"> </a> <br> <img alt="Stars" src="https://img.shields.io/github/stars/fsegurai/ngx-sidenav?style=square&labelColor=343b41"/> <img alt="Forks" src="https://img.shields.io/github/forks/fsegurai/ngx-sidenav?style=square&labelColor=343b41"/> <a href="https://www.npmjs.com/package/@fsegurai/ngx-sidenav"> <img alt="NPM Downloads" src="https://img.shields.io/npm/dt/@fsegurai/ngx-sidenav"> </a> </p> # NGX Sidenav A modern, flexible, and feature-rich sidenav component library for Angular applications. Built with Angular's latest features, including signals, standalone components, and the new control flow syntax. ## ✨ Features - 🎯 **Modern Angular**: Built with Angular 20+ features (signals, standalone components, control flow) - πŸ“± **Responsive Design**: Adaptive layout with collapsible and resizable functionality - πŸ”„ **Dynamic Content**: Support for both declarative HTML and programmatic content management - 🎨 **Customizable**: Extensive theming and styling options - πŸš€ **Performance**: Optimized with OnPush change detection and signals - πŸ’Ύ **Persistent State**: Auto-save/restore sidenav state and navigation stack - 🧩 **Component Support**: Render custom Angular components within the sidenav - πŸŽͺ **Animations**: Smooth transitions and visual feedback ## πŸ“‹ Table of Contents - [Installation](#installation) - [Quick Start](#quick-start) - [Usage Patterns](#usage-patterns) - [Declarative HTML Usage](#declarative-html-usage) - [Programmatic Service Usage](#programmatic-service-usage) - [API Reference](#api-reference) - [Advanced Examples](#advanced-examples) - [Demo Application](#demo-application) - [Contributing](#contributing) - [License](#license) ## πŸš€ Installation ```bash npm install @fsegurai/ngx-sidenav --save ``` ## ⚑ Quick Start ```typescript import { Component, signal } from '@angular/core'; import { Sidenav, SidenavMenu, StackMenuItem } from '@fsegurai/ngx-sidenav'; @Component({ selector: 'app-root', imports: [Sidenav, SidenavMenu], template: ` <ngx-sidenav [minWidth]="200" [maxWidth]="400" [canResize]="true" [canCollapse]="true"> <ngx-sidenav-menu [items]="menuItems()"></ngx-sidenav-menu> </ngx-sidenav> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent { protected readonly menuItems = signal<StackMenuItem[]>([ { label: 'Dashboard', icon: 'dashboard', route: '/dashboard', iconType: 'material' }, { label: 'Settings', icon: 'settings', iconType: 'material', children: [ { label: 'Profile', route: '/settings/profile' }, { label: 'Security', route: '/settings/security' } ] } ]); } ``` ## πŸ”§ Usage Patterns ### Declarative HTML Usage Perfect for static menu structures defined at compile time: ```typescript import { Component, signal } from '@angular/core'; import { Sidenav, SidenavMenu, StackMenuItem } from '@fsegurai/ngx-sidenav'; @Component({ selector: 'app-declarative', imports: [Sidenav, SidenavMenu], template: ` <ngx-sidenav [minWidth]="250" [maxWidth]="500" [initialWidth]="320" [canResize]="true" [canCollapse]="true" [position]="'left'" [backdrop]="false" (opened)="onSidenavOpened()" (closed)="onSidenavClosed()" (resized)="onSidenavResized($event)"> <!-- Static menu content --> <ngx-sidenav-menu [items]="menuItems()" [parentLabel]="'Main Menu'"> </ngx-sidenav-menu> <!-- Custom content can also be added --> <div class="user-profile"> <img [src]="userAvatar()" alt="User Avatar"> <span>{{ userName() }}</span> </div> </ngx-sidenav> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class DeclarativeComponent { protected readonly menuItems = signal<StackMenuItem[]>([ { label: 'Navigation', isTitle: true }, { label: 'Analytics', icon: 'analytics', iconType: 'material', badge: '5', badgeColor: 'primary', children: [ { label: 'Overview', route: '/analytics/overview', summary: 'View key metrics and insights' }, { label: 'Reports', route: '/analytics/reports', icon: 'assessment', badge: 'NEW' }, { label: 'Performance', icon: 'speed', children: [ { label: 'Load Times', route: '/analytics/performance/load' }, { label: 'Memory Usage', route: '/analytics/performance/memory' } ] } ] }, { label: 'User Management', icon: 'people', route: '/users', tooltip: 'Manage application users' }, { label: 'Tools', isTitle: true }, { label: 'Settings', icon: 'settings', disabled: false, children: [ { label: 'General', route: '/settings/general' }, { label: 'Security', route: '/settings/security' }, { label: 'Notifications', route: '/settings/notifications' } ] } ]); protected readonly userAvatar = signal('/assets/user-avatar.jpg'); protected readonly userName = signal('John Doe'); onSidenavOpened() { console.log('Sidenav opened'); } onSidenavClosed() { console.log('Sidenav closed'); } onSidenavResized(width: number) { console.log('Sidenav resized to:', width); } } ``` ### Programmatic Service Usage Ideal for dynamic content management and complex navigation flows: ```typescript import { Component, inject, signal } from '@angular/core'; import { Sidenav, SidenavService, MenuStackItem, ComponentStackItem } from '@fsegurai/ngx-sidenav'; @Component({ selector: 'app-programmatic', imports: [Sidenav], template: ` <div class="app-container"> <!-- Sidenav will be populated programmatically --> <ngx-sidenav [canResize]="true"> <!-- Content will be dynamically injected here --> </ngx-sidenav> <main class="main-content"> <div class="controls"> <button (click)="showDashboard()">Dashboard Menu</button> <button (click)="showUserProfile()">User Profile</button> <button (click)="showSettings()">Settings</button> <button (click)="showCustomWidget()">Custom Widget</button> <button (click)="goBack()" [disabled]="!canGoBack()"> ← Back </button> </div> <router-outlet /> </main> </div> `, styles: [` .app-container { display: flex; height: 100vh; } .main-content { flex: 1; padding: 2rem; } .controls { display: flex; gap: 1rem; margin-bottom: 2rem; } .controls button { padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer; } .controls button:disabled { opacity: 0.5; cursor: not-allowed; } `], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProgrammaticComponent { private readonly sidenavService = inject(SidenavService); // Computed signal to track navigation state protected readonly canGoBack = this.sidenavService.canGoBack; showDashboard() { const dashboardMenu: MenuStackItem = { type: 'menu', title: 'Dashboard', items: [ { label: 'Overview', icon: 'dashboard', route: '/dashboard/overview', iconType: 'material' }, { label: 'Analytics', icon: 'analytics', badge: '12', badgeColor: 'accent', children: [ { label: 'Traffic', route: '/dashboard/traffic' }, { label: 'Conversions', route: '/dashboard/conversions' }, { label: 'Revenue', route: '/dashboard/revenue' } ] }, { label: 'Reports', icon: 'assessment', route: '/dashboard/reports' } ] }; this.sidenavService.push({ type: 'menu', items: dashboardMenu}); } showUserProfile() { const profileMenu: MenuStackItem = { type: 'menu', title: 'User Profile', badge: '3', badgeColor: 'primary', items: [ { label: 'Personal Info', icon: 'person', route: '/profile/personal', summary: 'Manage your personal information' }, { label: 'Account Settings', icon: 'settings', children: [ { label: 'Password', route: '/profile/password' }, { label: 'Two-Factor Auth', route: '/profile/2fa' }, { label: 'API Keys', route: '/profile/api-keys' } ] }, { label: 'Preferences', icon: 'tune', route: '/profile/preferences' }, { label: 'Danger Zone', isTitle: true }, { label: 'Delete Account', icon: 'delete_forever', route: '/profile/delete', disabled: false } ] }; this.sidenavService.push({ type: 'menu', items: profileMenu}); } showSettings() { const settingsMenu: MenuStackItem = { type: 'menu', title: 'Application Settings', items: [ { label: 'General', isTitle: true }, { label: 'Theme', icon: 'palette', children: [ { label: 'Light Mode', route: '/settings/theme/light' }, { label: 'Dark Mode', route: '/settings/theme/dark' }, { label: 'Auto', route: '/settings/theme/auto' } ] }, { label: 'Language', icon: 'language', route: '/settings/language' }, { label: 'Advanced', isTitle: true }, { label: 'Developer Tools', icon: 'code', children: [ { label: 'API Console', route: '/settings/api-console' }, { label: 'Debug Mode', route: '/settings/debug' }, { label: 'Feature Flags', route: '/settings/features' } ] } ] }; this.sidenavService.push({ type: 'menu', items: settingsMenu}); } showCustomWidget() { // Example of pushing a custom component const widgetComponent: ComponentStackItem = { type: 'component', component: CustomWidgetComponent, componentData: { title: 'System Status', data: { uptime: '99.9%', activeUsers: 1234, systemHealth: 'excellent' } } }; this.sidenavService.push({ type: 'component', component: widgetComponent}); } async goBack() { await this.sidenavService.pop(); } // Initialize with default menu ngOnInit() { this.showDashboard(); } } // Example custom component for the sidenav @Component({ selector: 'custom-widget', template: ` <div class="widget-container"> <h3>{{ title() }}</h3> <div class="metrics"> @for (metric of metrics(); track metric.label) { <div class="metric"> <span class="label">{{ metric.label }}</span> <span class="value" [class]="metric.status">{{ metric.value }}</span> </div> } </div> <div class="actions"> <button (click)="refresh()">Refresh</button> <button (click)="exportData()">Export</button> </div> </div> `, styles: [` .widget-container { padding: 1.5rem; } .metrics { margin: 1rem 0; } .metric { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #eee; } .value.excellent { color: #4caf50; } .value.good { color: #ff9800; } .value.poor { color: #f44336; } .actions { display: flex; gap: 0.5rem; margin-top: 1rem; } .actions button { padding: 0.5rem 1rem; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer; } `], changeDetection: ChangeDetectionStrategy.OnPush, }) export class CustomWidgetComponent { readonly title = input.required<string>(); readonly data = input.required<any>(); protected readonly metrics = computed(() => [ { label: 'Uptime', value: this.data().uptime, status: 'excellent' }, { label: 'Active Users', value: this.data().activeUsers.toLocaleString(), status: 'good' }, { label: 'System Health', value: this.data().systemHealth, status: 'excellent' } ]); refresh() { console.log('Refreshing widget data...'); } exportData() { console.log('Exporting data...'); } } ``` ## πŸ“š API Reference ### Components #### `<ngx-sidenav>` The main sidenav container component. **Properties:** - `minWidth: number` - Minimum width (default: 250) - `maxWidth: number` - Maximum width (default: 600) - `initialWidth: number` - Initial width (default: 300) - `collapseWidth: number` - Width when collapsed (default: 30) - `canResize: boolean` - Enable resizing (default: true) - `canCollapse: boolean` - Enable collapsing (default: true) - `position: 'left' | 'right'` - Position (default: 'left') - `direction: 'ltr' | 'rtl'` - Text direction (default: 'ltr') - `backdrop: boolean` - Show backdrop (default: false) - `clearCacheOnReload: boolean` - Clear cache on reload (default: false) - `menuIconCollapse: string` - Collapse icon (default: 'menu_open') - `menuIconExpand: string` - Expand icon (default: 'menu') **Events:** - `opened()` - Sidenav opened - `closed()` - Sidenav closed - `resized(width: number)` - Sidenav resized #### `<ngx-sidenav-menu>` Menu component for displaying navigation items. **Properties:** - `items: StackMenuItem[]` - Menu items array - `parentLabel: string` - Parent menu label - `parentBadge: string` - Parent menu badge - `parentBadgeColor: string` - Parent badge color ### Interfaces #### `StackMenuItem` ```typescript interface StackMenuItem { label: string; icon?: string; iconType?: 'material' | 'svg' | 'custom'; route?: string; children?: StackMenuItem[]; isParent?: boolean; isTitle?: boolean; tooltip?: string; badge?: string | number; badgeColor?: string; disabled?: boolean; summary?: string; tags?: string[]; [key: string]: unknown; // Custom properties } ``` #### `MenuStackItem` ```typescript interface MenuStackItem { type: 'menu'; title?: string; badge?: string; badgeColor?: string; items: StackMenuItem[]; } ``` #### `ComponentStackItem` ```typescript interface ComponentStackItem { type: 'component'; component: Type<any>; componentData?: Record<string, any>; } ``` ### SidenavService The service for programmatic sidenav management. #### Methods - `push(item: MenuStackItem | ComponentStackItem): Promise<void>` - Add item to navigation stack - `pop(): Promise<void>` - Remove current item from stack - `toggleSidenav(): void` - Toggle sidenav open/closed state - `setSidenavWidth(width: number): void` - Set sidenav width - `setConfig(config: SidenavConfig): void` - Configure sidenav settings - `clearOnNextReload(): void` - Clear state on next page reload #### Signals - `isExpanded: Signal<boolean>` - Sidenav expanded state - `sidenavWidth: Signal<number>` - Current sidenav width - `currentMenu: Signal<MenuStackItem | undefined>` - Current menu - `canGoBack: Signal<boolean>` - Whether back navigation is available - `minWidth: Signal<number>` - Minimum width - `maxWidth: Signal<number>` - Maximum width ## 🎯 Advanced Examples ### Custom Component with Data Binding ```typescript @Component({ selector: 'notification-center', template: ` <div class="notification-center"> <div class="header"> <h3>Notifications</h3> <button (click)="markAllRead()">Mark All Read</button> </div> @for (notification of notifications(); track notification.id) { <div class="notification" [class.unread]="!notification.read"> <div class="content"> <h4>{{ notification.title }}</h4> <p>{{ notification.message }}</p> <small>{{ notification.timestamp | date:'short' }}</small> </div> @if (!notification.read) { <button (click)="markAsRead(notification.id)">Mark Read</button> } </div> } </div> `, changeDetection: ChangeDetectionStrategy.OnPush, }) export class NotificationCenterComponent { readonly notifications = input.required<Notification[]>(); markAsRead(id: string) { // Handle mark as read logic } markAllRead() { // Handle mark all as read logic } } // Usage in parent component showNotifications() { const notificationComponent: ComponentStackItem = { type: 'component', component: NotificationCenterComponent, componentData: { notifications: this.notificationService.getNotifications() } }; this.sidenavService.push({ type: 'component', component: notificationComponent}); } ``` ### Multi-Level Navigation ```typescript const complexMenu: MenuStackItem = { type: 'menu', title: 'Project Management', items: [ { label: 'Projects', icon: 'folder', children: [ { label: 'Active Projects', children: [ { label: 'Web App Redesign', route: '/projects/web-redesign' }, { label: 'Mobile App', route: '/projects/mobile-app' }, { label: 'API Modernization', route: '/projects/api-modernization' } ] }, { label: 'Archived Projects', children: [ { label: 'Legacy System', route: '/projects/legacy' }, { label: 'Old Website', route: '/projects/old-website' } ] } ] }, { label: 'Team', icon: 'group', children: [ { label: 'Developers', route: '/team/developers' }, { label: 'Designers', route: '/team/designers' }, { label: 'Project Managers', route: '/team/managers' } ] } ] }; ``` ## πŸŽͺ Demo Application Explore the full functionality in our interactive demo: **[πŸš€ Live Demo](https://fsegurai.github.io/ngx-sidenav)** ### Run Demo Locally ```bash git clone https://github.com/fsegurai/ngx-sidenav.git cd ngx-sidenav bun install bun start ``` The demo showcases: - All configuration options - Both HTML and service usage patterns - Custom component integration - Multi-level navigation - Responsive behavior - Theme customization ## 🀝 Contributing Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. ## πŸ“„ License This project is licensed under the MIT Licenseβ€”see the [LICENSE](LICENSE) file for details.