Menu Service
Il MenuService fornisce gestione centralizzata della struttura di navigazione dell'applicazione, con supporto per menu gerarchici, permessi e aggiornamenti dinamici.
Panoramica
Il MenuService gestisce:
- ✅ Menu gerarchici: Struttura ad albero con sottomenu illimitati
- ✅ Routing integrato: Integrazione con Angular Router
- ✅ Permessi: Controllo accessi basato su ruoli/permessi
- ✅ Icone: Supporto per icon fonts (PrimeNG, FontAwesome, Material Icons)
- ✅ Observable: Reactive updates con RxJS
- ✅ Badge: Notifiche e contatori sui menu item
Modello Dati
MenuItem Interface
interface MenuItem {
id: string; // Identificatore univoco
label: string; // Etichetta visualizzata
icon?: string; // Icona (es. 'pi pi-home')
routerLink?: string | string[]; // Route Angular
url?: string; // URL esterno
items?: MenuItem[]; // Sottomenu
badge?: string; // Badge label
badgeClass?: string; // CSS class per badge
visible?: boolean; // Visibilità (default: true)
disabled?: boolean; // Stato disabilitato
permissions?: string[]; // Permessi richiesti
command?: (event?: any) => void; // Callback click
styleClass?: string; // CSS class custom
separator?: boolean; // Separatore visuale
expanded?: boolean; // Espanso per default (solo gruppi)
tooltip?: string; // Tooltip
target?: string; // Target per url esterni (_blank, ecc.)
}
Uso Base
Definire la Struttura Menu
import { Component, inject, OnInit } from '@angular/core';
import { MenuService, MenuItem } from '@tess-ui-library/core';
@Component({
selector: 'app-root',
standalone: true,
template: `...`,
})
export class AppComponent implements OnInit {
private menuService = inject(MenuService);
ngOnInit() {
const menuItems: MenuItem[] = [
{
id: 'home',
label: 'Home',
icon: 'pi pi-home',
routerLink: '/dashboard',
},
{
id: 'users',
label: 'Utenti',
icon: 'pi pi-users',
items: [
{
id: 'users-list',
label: 'Lista Utenti',
routerLink: '/users',
permissions: ['users.read'],
},
{
id: 'users-create',
label: 'Nuovo Utente',
routerLink: '/users/new',
permissions: ['users.create'],
},
],
},
{
id: 'settings',
label: 'Impostazioni',
icon: 'pi pi-cog',
routerLink: '/settings',
permissions: ['settings.access'],
},
];
this.menuService.setMenuItems(menuItems);
}
}
Accedere al Menu
import { Component, inject } from '@angular/core';
import { MenuService } from '@tess-ui-library/core';
import { SideMenuComponent } from '@tess-ui-library/shared';
@Component({
selector: 'app-layout',
standalone: true,
imports: [SideMenuComponent],
template: `
<div class="layout">
<app-side-menu [items]="menuItems$ | async"></app-side-menu>
<main>
<router-outlet></router-outlet>
</main>
</div>
`,
})
export class LayoutComponent {
private menuService = inject(MenuService);
menuItems$ = this.menuService.menuItems$;
}
API
setMenuItems(items: MenuItem[]): void
Imposta la struttura completa del menu.
this.menuService.setMenuItems(menuItems);
menuItems$: Observable<MenuItem[]>
Observable che emette la struttura del menu ogni volta che cambia.
this.menuService.menuItems$.subscribe(items => {
console.log('Menu aggiornato:', items);
});
getMenuItems(): MenuItem[]
Ritorna la struttura del menu corrente (snapshot).
const currentMenu = this.menuService.getMenuItems();
addMenuItem(item: MenuItem, parentId?: string): void
Aggiunge un nuovo item al menu.
// Aggiunge al root level
this.menuService.addMenuItem({
id: 'reports',
label: 'Report',
icon: 'pi pi-chart-bar',
routerLink: '/reports',
});
// Aggiunge come sottomenu
this.menuService.addMenuItem({
id: 'users-export',
label: 'Esporta Utenti',
routerLink: '/users/export',
}, 'users'); // parentId
removeMenuItem(itemId: string): void
Rimuove un item dal menu.
this.menuService.removeMenuItem('users-export');
updateMenuItem(itemId: string, updates: Partial<MenuItem>): void
Aggiorna un item esistente.
// Aggiorna badge
this.menuService.updateMenuItem('notifications', {
badge: '5',
badgeClass: 'p-badge-danger',
});
// Disabilita item
this.menuService.updateMenuItem('settings', {
disabled: true,
});
findMenuItem(itemId: string): MenuItem | undefined
Trova un item per ID (ricerca ricorsiva).
const item = this.menuService.findMenuItem('users-list');
if (item) {
console.log('Item trovato:', item.label);
}
setItemVisibility(itemId: string, visible: boolean): void
Cambia la visibilità di un item.
this.menuService.setItemVisibility('admin-section', false);
Esempi Pratici
Menu con Permessi
@Injectable({ providedIn: 'root' })
export class MenuInitializerService {
private menuService = inject(MenuService);
private authService = inject(AuthService);
initialize() {
const user = this.authService.getCurrentUser();
const menuItems: MenuItem[] = [
{
id: 'dashboard',
label: 'Dashboard',
icon: 'pi pi-home',
routerLink: '/dashboard',
},
{
id: 'admin',
label: 'Amministrazione',
icon: 'pi pi-shield',
permissions: ['admin'],
items: [
{
id: 'users',
label: 'Gestione Utenti',
routerLink: '/admin/users',
permissions: ['users.manage'],
},
{
id: 'roles',
label: 'Ruoli e Permessi',
routerLink: '/admin/roles',
permissions: ['roles.manage'],
},
],
},
];
// Filtra menu items in base ai permessi utente
const filteredMenu = this.filterByPermissions(menuItems, user.permissions);
this.menuService.setMenuItems(filteredMenu);
}
private filterByPermissions(items: MenuItem[], userPermissions: string[]): MenuItem[] {
return items
.filter(item => {
if (!item.permissions || item.permissions.length === 0) {
return true;
}
return item.permissions.some(p => userPermissions.includes(p));
})
.map(item => ({
...item,
items: item.items ? this.filterByPermissions(item.items, userPermissions) : undefined,
}));
}
}
Menu Dinamico con Badge
@Injectable({ providedIn: 'root' })
export class NotificationMenuService {
private menuService = inject(MenuService);
private notificationService = inject(NotificationService);
constructor() {
// Aggiorna badge quando arrivano nuove notifiche
this.notificationService.unreadCount$.subscribe(count => {
this.menuService.updateMenuItem('notifications', {
badge: count > 0 ? count.toString() : undefined,
badgeClass: count > 0 ? 'p-badge-danger' : undefined,
});
});
}
initializeMenu() {
const menuItems: MenuItem[] = [
{
id: 'notifications',
label: 'Notifiche',
icon: 'pi pi-bell',
routerLink: '/notifications',
badge: '0',
},
];
this.menuService.setMenuItems(menuItems);
}
}
Menu con Azioni Custom
@Component({
selector: 'app-layout',
standalone: true,
template: `...`,
})
export class LayoutComponent implements OnInit {
private menuService = inject(MenuService);
private router = inject(Router);
private dialogService = inject(TessDialogService);
ngOnInit() {
const menuItems: MenuItem[] = [
{
id: 'logout',
label: 'Esci',
icon: 'pi pi-sign-out',
command: () => this.confirmLogout(),
},
{
id: 'help',
label: 'Aiuto',
icon: 'pi pi-question-circle',
url: 'https://docs.example.com',
target: '_blank',
},
];
this.menuService.setMenuItems(menuItems);
}
async confirmLogout() {
const result = await this.dialogService.open({
header: 'Conferma Logout',
message: 'Sei sicuro di voler uscire?',
acceptLabel: 'Esci',
rejectLabel: 'Annulla',
});
if (result.accepted) {
this.authService.logout();
this.router.navigate(['/login']);
}
}
}
Menu con Separatori
const menuItems: MenuItem[] = [
{
id: 'dashboard',
label: 'Dashboard',
icon: 'pi pi-home',
routerLink: '/dashboard',
},
{
id: 'sep1',
separator: true,
},
{
id: 'settings',
label: 'Impostazioni',
icon: 'pi pi-cog',
routerLink: '/settings',
},
{
id: 'sep2',
separator: true,
},
{
id: 'logout',
label: 'Esci',
icon: 'pi pi-sign-out',
command: () => this.logout(),
},
];
Menu Multi-livello
const menuItems: MenuItem[] = [
{
id: 'products',
label: 'Prodotti',
icon: 'pi pi-box',
items: [
{
id: 'products-catalog',
label: 'Catalogo',
items: [
{
id: 'products-all',
label: 'Tutti i Prodotti',
routerLink: '/products',
},
{
id: 'products-categories',
label: 'Categorie',
routerLink: '/products/categories',
},
],
},
{
id: 'products-inventory',
label: 'Inventario',
routerLink: '/products/inventory',
},
],
},
];
Integrazione con SideMenu Component
Il MenuService è progettato per lavorare con il componente SideMenuComponent:
import { Component, inject } from '@angular/core';
import { MenuService } from '@tess-ui-library/core';
import { SideMenuComponent } from '@tess-ui-library/shared';
@Component({
selector: 'app-layout',
standalone: true,
imports: [SideMenuComponent],
template: `
<tess-side-menu
[items]="menuItems$ | async"
[collapsed]="sidebarCollapsed"
(itemClick)="onMenuItemClick($event)"
></tess-side-menu>
`,
})
export class LayoutComponent {
private menuService = inject(MenuService);
menuItems$ = this.menuService.menuItems$;
sidebarCollapsed = false;
onMenuItemClick(item: MenuItem) {
console.log('Menu item cliccato:', item.label);
}
}
Configurazione Avanzata
Menu da API
@Injectable({ providedIn: 'root' })
export class MenuLoaderService {
private http = inject(HttpClient);
private menuService = inject(MenuService);
loadMenuFromApi() {
this.http.get<MenuItem[]>('/api/menu')
.subscribe(menuItems => {
this.menuService.setMenuItems(menuItems);
});
}
}
Menu con Localizzazione
import { TranslateService } from '@ngx-translate/core';
@Injectable({ providedIn: 'root' })
export class LocalizedMenuService {
private menuService = inject(MenuService);
private translate = inject(TranslateService);
initializeMenu() {
const menuItems: MenuItem[] = [
{
id: 'dashboard',
label: this.translate.instant('menu.dashboard'),
icon: 'pi pi-home',
routerLink: '/dashboard',
},
{
id: 'users',
label: this.translate.instant('menu.users'),
icon: 'pi pi-users',
routerLink: '/users',
},
];
this.menuService.setMenuItems(menuItems);
// Aggiorna menu quando cambia lingua
this.translate.onLangChange.subscribe(() => {
this.initializeMenu();
});
}
}
Best Practices
- Inizializza una sola volta: Inizializza il menu nell'
APP_INITIALIZERo nel componente root - Usa ID univoci: Gli ID devono essere univoci in tutta la struttura
- Permessi granulari: Definisci permessi specifici per ogni item
- Badge informativi: Usa badge per informazioni importanti (notifiche, contatori)
- Routing consistency: Usa sempre
routerLinkper navigazione interna - Lazy loading: Considera lazy loading per menu molto grandi
- Accessibilità: Fornisci label descrittive e tooltip dove necessario
- Performance: Evita aggiornamenti frequenti del menu, usa
updateMenuItemper modifiche puntuali