@fsegurai/ngx-sidenav
Version:
Angular component for creating a resizeable sidenav with drag and drop functionality.
729 lines (630 loc) β’ 19.9 kB
Markdown
<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.