UI Kit Factory
Il UI Kit Factory è il cuore del sistema di astrazione che permette a Tess UI Library di essere UI Kit agnostic. Consente di passare da PrimeNG a Bootstrap o Material senza modificare il codice applicativo.
Panoramica
Problema
Le librerie UI come PrimeNG, Bootstrap e Material hanno API diverse:
// PrimeNG
this.dialogService.open(MyComponent, { header: 'Title' });
// Bootstrap (ng-bootstrap)
this.modalService.open(MyComponent, { title: 'Title' });
// Material
this.dialog.open(MyComponent, { title: 'Title' });
Cambiare UI Kit richiederebbe riscrivere tutto il codice.
Soluzione
Adapter Pattern + Factory Pattern:
- Definisci interfacce comuni per tutte le funzionalità UI
- Implementa adapter specifici per ogni UI Kit
- Usa una factory per fornire l'adapter corretto a runtime
- Il codice applicativo usa solo le interfacce
Architettura
┌─────────────────────────────────────────────┐
│ Application Code │
│ (usa solo DialogAdapter interface) │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ TessDialogService │
│ constructor(@Inject(DIALOG_ADAPTER) │
│ private adapter: DialogAdapter) │
└────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ DIALOG_ADAPTER Token │
│ (provided by UiKitFactory) │
└─────────────┬───────────────────────────────┘
│
┌─────────┴─────────┬─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌──────────────┐
│ PrimeNG │ │ Bootstrap │ │ Material │
│ Adapter │ │ Adapter │ │ Adapter │
└─────────┘ └─────────────┘ └──────────────┘
Interfacce Adapter
DialogAdapter
export interface DialogConfig {
title?: string;
content?: string;
component?: Type<any>;
data?: any;
width?: string;
height?: string;
closable?: boolean;
modal?: boolean;
}
export interface DialogResult {
confirmed: boolean;
data?: any;
}
export interface DialogAdapter {
/**
* Apre un dialog
*/
open(config: DialogConfig): Promise<DialogResult>;
/**
* Chiude un dialog specifico
*/
close(ref: any, data?: any): void;
/**
* Chiude tutti i dialog aperti
*/
closeAll(): void;
}
// Injection Token
export const DIALOG_ADAPTER = new InjectionToken<DialogAdapter>('DIALOG_ADAPTER');
ToastAdapter
export interface ToastMessage {
severity: 'success' | 'info' | 'warn' | 'error';
summary: string;
detail?: string;
life?: number; // ms
sticky?: boolean;
}
export interface ToastAdapter {
/**
* Mostra un messaggio toast
*/
show(message: ToastMessage): void;
/**
* Cancella tutti i messaggi
*/
clear(): void;
}
export const TOAST_ADAPTER = new InjectionToken<ToastAdapter>('TOAST_ADAPTER');
TableAdapter
export interface TableColumn {
field: string;
header: string;
sortable?: boolean;
filterable?: boolean;
width?: string;
type?: 'text' | 'number' | 'date' | 'boolean';
}
export interface TableConfig {
columns: TableColumn[];
data: any[];
paginator?: boolean;
rows?: number;
rowsPerPageOptions?: number[];
sortMode?: 'single' | 'multiple';
filterMode?: 'lenient' | 'strict';
}
export interface TableAdapter {
/**
* Configura la tabella
*/
configure(config: TableConfig): void;
/**
* Ottieni i dati correnti (filtrati/ordinati)
*/
getCurrentData(): any[];
/**
* Reset filtri e ordinamento
*/
reset(): void;
}
export const TABLE_ADAPTER = new InjectionToken<TableAdapter>('TABLE_ADAPTER');
MenuAdapter
export interface MenuItem {
label: string;
icon?: string;
routerLink?: string;
url?: string;
command?: () => void;
items?: MenuItem[];
badge?: string | number;
disabled?: boolean;
visible?: boolean;
}
export interface MenuAdapter {
/**
* Imposta gli item del menu
*/
setItems(items: MenuItem[]): void;
/**
* Aggiorna un singolo item
*/
updateItem(path: string, updates: Partial<MenuItem>): void;
/**
* Ottieni l'item attivo corrente
*/
getActiveItem(): MenuItem | null;
}
export const MENU_ADAPTER = new InjectionToken<MenuAdapter>('MENU_ADAPTER');
Implementazioni Adapter
PrimeNG Adapter
import { Injectable } from '@angular/core';
import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog';
import { DialogAdapter, DialogConfig, DialogResult } from '../interfaces';
@Injectable()
export class PrimeNgDialogAdapter implements DialogAdapter {
private refs: DynamicDialogRef[] = [];
constructor(private dialogService: DialogService) {}
async open(config: DialogConfig): Promise<DialogResult> {
const ref = this.dialogService.open(config.component!, {
header: config.title,
width: config.width || '50vw',
height: config.height,
closable: config.closable !== false,
modal: config.modal !== false,
data: config.data,
});
this.refs.push(ref);
return new Promise((resolve) => {
ref.onClose.subscribe((data: any) => {
this.refs = this.refs.filter(r => r !== ref);
resolve({
confirmed: !!data,
data: data,
});
});
});
}
close(ref: DynamicDialogRef, data?: any): void {
ref.close(data);
}
closeAll(): void {
this.refs.forEach(ref => ref.close());
this.refs = [];
}
}
Bootstrap Adapter
import { Injectable } from '@angular/core';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { DialogAdapter, DialogConfig, DialogResult } from '../interfaces';
@Injectable()
export class BootstrapDialogAdapter implements DialogAdapter {
private refs: NgbModalRef[] = [];
constructor(private modalService: NgbModal) {}
async open(config: DialogConfig): Promise<DialogResult> {
const ref = this.modalService.open(config.component!, {
size: this.mapSize(config.width),
backdrop: config.modal !== false ? 'static' : true,
keyboard: config.closable !== false,
});
// Passa data al componente
if (config.data) {
Object.assign(ref.componentInstance, config.data);
}
this.refs.push(ref);
return ref.result
.then((data) => {
this.refs = this.refs.filter(r => r !== ref);
return { confirmed: true, data };
})
.catch(() => {
this.refs = this.refs.filter(r => r !== ref);
return { confirmed: false };
});
}
close(ref: NgbModalRef, data?: any): void {
ref.close(data);
}
closeAll(): void {
this.refs.forEach(ref => ref.dismiss());
this.refs = [];
}
private mapSize(width?: string): 'sm' | 'lg' | 'xl' {
if (!width) return 'lg';
const numericWidth = parseInt(width);
if (numericWidth < 400) return 'sm';
if (numericWidth > 800) return 'xl';
return 'lg';
}
}
Material Adapter
import { Injectable } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DialogAdapter, DialogConfig, DialogResult } from '../interfaces';
@Injectable()
export class MaterialDialogAdapter implements DialogAdapter {
private refs: MatDialogRef<any>[] = [];
constructor(private dialog: MatDialog) {}
async open(config: DialogConfig): Promise<DialogResult> {
const ref = this.dialog.open(config.component!, {
width: config.width || '50vw',
height: config.height,
disableClose: config.closable === false,
data: config.data,
});
this.refs.push(ref);
return ref.afterClosed().toPromise().then((data) => {
this.refs = this.refs.filter(r => r !== ref);
return {
confirmed: data !== undefined,
data: data,
};
});
}
close(ref: MatDialogRef<any>, data?: any): void {
ref.close(data);
}
closeAll(): void {
this.dialog.closeAll();
this.refs = [];
}
}
UI Kit Factory
La factory seleziona l'adapter corretto in base alla configurazione:
import { Injectable, Inject } from '@angular/core';
import { UI_KIT } from '../tokens';
import { DialogAdapter, ToastAdapter, TableAdapter, MenuAdapter } from '../interfaces';
import { PrimeNgDialogAdapter, PrimeNgToastAdapter } from '../adapters/primeng';
import { BootstrapDialogAdapter, BootstrapToastAdapter } from '../adapters/bootstrap';
import { MaterialDialogAdapter, MaterialToastAdapter } from '../adapters/material';
export type UiKit = 'primeng' | 'bootstrap' | 'material';
@Injectable({ providedIn: 'root' })
export class UiKitFactory {
constructor(@Inject(UI_KIT) private uiKit: UiKit) {}
createDialogAdapter(): DialogAdapter {
switch (this.uiKit) {
case 'primeng':
return new PrimeNgDialogAdapter();
case 'bootstrap':
return new BootstrapDialogAdapter();
case 'material':
return new MaterialDialogAdapter();
default:
throw new Error(`Unsupported UI Kit: ${this.uiKit}`);
}
}
createToastAdapter(): ToastAdapter {
switch (this.uiKit) {
case 'primeng':
return new PrimeNgToastAdapter();
case 'bootstrap':
return new BootstrapToastAdapter();
case 'material':
return new MaterialToastAdapter();
default:
throw new Error(`Unsupported UI Kit: ${this.uiKit}`);
}
}
createTableAdapter(): TableAdapter {
// Similar implementation
}
createMenuAdapter(): MenuAdapter {
// Similar implementation
}
}
Configurazione
Setup Application
import { ApplicationConfig } from '@angular/core';
import { provideTessUiLibrary } from 'tess-ui-library';
export const appConfig: ApplicationConfig = {
providers: [
provideTessUiLibrary({
uiKit: 'primeng', // 'bootstrap' | 'material'
// ... altre configurazioni
}),
],
};
Provider Function
export function provideTessUiLibrary(config: TessUiLibraryConfig) {
return [
{ provide: UI_KIT, useValue: config.uiKit },
{
provide: DIALOG_ADAPTER,
useFactory: (factory: UiKitFactory) => factory.createDialogAdapter(),
deps: [UiKitFactory],
},
{
provide: TOAST_ADAPTER,
useFactory: (factory: UiKitFactory) => factory.createToastAdapter(),
deps: [UiKitFactory],
},
{
provide: TABLE_ADAPTER,
useFactory: (factory: UiKitFactory) => factory.createTableAdapter(),
deps: [UiKitFactory],
},
{
provide: MENU_ADAPTER,
useFactory: (factory: UiKitFactory) => factory.createMenuAdapter(),
deps: [UiKitFactory],
},
];
}
Utilizzo nei Componenti
I componenti usano solo le interfacce, ignorando l'implementazione:
import { Component, Inject } from '@angular/core';
import { DIALOG_ADAPTER, DialogAdapter } from 'tess-ui-library';
import { UserFormComponent } from './user-form.component';
@Component({
selector: 'app-user-list',
template: `
<button (click)="openUserDialog()">Crea Utente</button>
`,
})
export class UserListComponent {
constructor(
@Inject(DIALOG_ADAPTER) private dialogAdapter: DialogAdapter
) {}
async openUserDialog(): Promise<void> {
const result = await this.dialogAdapter.open({
title: 'Crea Nuovo Utente',
component: UserFormComponent,
width: '600px',
data: { mode: 'create' },
});
if (result.confirmed) {
console.log('Utente creato:', result.data);
}
}
}
Servizi Facade
Per semplificare ulteriormente, la libreria fornisce servizi facade:
import { Injectable, Inject } from '@angular/core';
import { DIALOG_ADAPTER, DialogAdapter, DialogConfig, DialogResult } from './interfaces';
@Injectable({ providedIn: 'root' })
export class TessDialogService {
constructor(
@Inject(DIALOG_ADAPTER) private adapter: DialogAdapter
) {}
open(config: DialogConfig): Promise<DialogResult> {
return this.adapter.open(config);
}
close(ref: any, data?: any): void {
this.adapter.close(ref, data);
}
closeAll(): void {
this.adapter.closeAll();
}
// Metodi di convenienza
confirm(title: string, message: string): Promise<boolean> {
return this.open({
title,
content: message,
// ... configurazione conferma
}).then(result => result.confirmed);
}
}
Uso semplificato:
export class MyComponent {
constructor(private dialogService: TessDialogService) {}
async deleteUser(): Promise<void> {
const confirmed = await this.dialogService.confirm(
'Conferma',
'Vuoi davvero eliminare l\'utente?'
);
if (confirmed) {
// elimina utente
}
}
}
Estensibilità
Aggiungere un Nuovo UI Kit
- Crea gli adapter:
// adapters/my-ui-kit/dialog.adapter.ts
export class MyUiKitDialogAdapter implements DialogAdapter {
// implementazione
}
- Aggiorna la factory:
export type UiKit = 'primeng' | 'bootstrap' | 'material' | 'myuikit';
createDialogAdapter(): DialogAdapter {
switch (this.uiKit) {
// ... existing cases
case 'myuikit':
return new MyUiKitDialogAdapter();
}
}
- Configura:
provideTessUiLibrary({ uiKit: 'myuikit' })
Custom Adapter
Puoi anche fornire un adapter personalizzato:
export const appConfig: ApplicationConfig = {
providers: [
provideTessUiLibrary({ uiKit: 'primeng' }),
// Override con adapter custom
{
provide: DIALOG_ADAPTER,
useClass: MyCustomDialogAdapter,
},
],
};
Vantaggi
- Portabilità: Cambi UI Kit modificando 1 riga
- Testabilità: Mock facili degli adapter
- Manutenibilità: Cambio API UI Kit isolato negli adapter
- Scalabilità: Aggiungi nuovi UI Kit senza modificare codice esistente
- Standardizzazione: API unificate per tutto il team
Limitazioni
- Common Denominator: Le interfacce supportano solo funzionalità comuni
- Features Specifiche: Funzionalità UI-Kit-specific richiedono estensioni
- Performance: Leggero overhead per l'indirection (trascurabile)
Best Practices
- Usa sempre le interfacce: Mai dipendere da adapter concreti
- Estendi con cautela: Aggiungi feature solo se comuni a tutti gli UI Kit
- Documenta differenze: Se un adapter ha comportamenti specifici, documentali
- Testa con tutti gli UI Kit: Assicurati che funzioni ovunque
- Usa TypeScript generics: Per type-safety completa