Componenti Avanzati
Tess UI Library include componenti avanzati per visualizzazione dati, navigazione multi-step e layout complessi.
TessChart
Wrapper per Chart.js con lazy loading e supporto SSR.
Caratteristiche
- ✅ Lazy Loading: Chart.js caricato solo quando necessario
- ✅ SSR Compatible: Funziona con Angular Universal
- ✅ Responsive: Si adatta automaticamente al container
- ✅ Tutti i tipi: Line, Bar, Pie, Doughnut, Radar, Polar, Bubble, Scatter
- ✅ Type-Safe: Full TypeScript support con Chart.js types
Installazione
npm install chart.js
Proprietà
interface TessChartProps {
type: ChartType; // 'line' | 'bar' | 'pie' | 'doughnut' | etc.
data: ChartData; // Dati chart
options?: ChartOptions; // Configurazione chart
width?: string | number; // Larghezza (default: '100%')
height?: string | number; // Altezza (default: '400px')
ariaLabel?: string; // Label accessibilità
}
Esempi
Line Chart
import { Component } from '@angular/core';
import { TessChartComponent } from 'tess-ui-library';
import { ChartData, ChartOptions } from 'chart.js';
@Component({
selector: 'app-sales-chart',
standalone: true,
imports: [TessChartComponent],
template: `
<tess-chart
type="line"
[data]="chartData"
[options]="chartOptions"
ariaLabel="Grafico vendite mensili">
</tess-chart>
`,
})
export class SalesChartComponent {
chartData: ChartData<'line'> = {
labels: ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu'],
datasets: [
{
label: 'Vendite 2024',
data: [65, 59, 80, 81, 56, 55],
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.4,
},
{
label: 'Vendite 2023',
data: [45, 49, 60, 71, 46, 45],
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
tension: 0.4,
},
],
};
chartOptions: ChartOptions<'line'> = {
responsive: true,
maintainAspectRatio: true,
plugins: {
legend: {
position: 'top',
},
title: {
display: true,
text: 'Vendite Mensili',
},
},
scales: {
y: {
beginAtZero: true,
},
},
};
}
Bar Chart
chartData: ChartData<'bar'> = {
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{
label: 'Revenue',
data: [120000, 150000, 180000, 200000],
backgroundColor: [
'rgba(255, 99, 132, 0.5)',
'rgba(54, 162, 235, 0.5)',
'rgba(255, 206, 86, 0.5)',
'rgba(75, 192, 192, 0.5)',
],
borderColor: [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 206, 86)',
'rgb(75, 192, 192)',
],
borderWidth: 1,
},
],
};
chartOptions: ChartOptions<'bar'> = {
responsive: true,
plugins: {
legend: {
display: false,
},
tooltip: {
callbacks: {
label: (context) => {
return `€ ${context.parsed.y.toLocaleString()}`;
},
},
},
},
};
Pie Chart
chartData: ChartData<'pie'> = {
labels: ['Desktop', 'Mobile', 'Tablet'],
datasets: [
{
data: [60, 30, 10],
backgroundColor: [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 206, 86)',
],
hoverOffset: 4,
},
],
};
chartOptions: ChartOptions<'pie'> = {
responsive: true,
plugins: {
legend: {
position: 'bottom',
},
title: {
display: true,
text: 'Traffic by Device',
},
},
};
Doughnut Chart
chartData: ChartData<'doughnut'> = {
labels: ['Completati', 'In Progress', 'In Attesa'],
datasets: [
{
data: [45, 30, 25],
backgroundColor: [
'rgb(75, 192, 192)',
'rgb(255, 205, 86)',
'rgb(255, 99, 132)',
],
},
],
};
Eventi
@Component({
template: `
<tess-chart
type="bar"
[data]="chartData"
[options]="chartOptions"
(chartClick)="onChartClick($event)">
</tess-chart>
`,
})
export class MyChartComponent {
onChartClick(event: { element: any; dataset: any }): void {
if (event.element) {
console.log('Clicked on:', event.dataset);
}
}
}
Aggiornamento Dinamico
export class RealtimeChartComponent implements OnInit, OnDestroy {
chartData = signal<ChartData<'line'>>({
labels: [],
datasets: [{
label: 'Real-time Data',
data: [],
borderColor: 'rgb(75, 192, 192)',
}],
});
private interval?: any;
ngOnInit(): void {
// Simula dati real-time
this.interval = setInterval(() => {
this.updateChart();
}, 2000);
}
ngOnDestroy(): void {
if (this.interval) {
clearInterval(this.interval);
}
}
private updateChart(): void {
const current = this.chartData();
const newLabel = new Date().toLocaleTimeString();
const newValue = Math.random() * 100;
this.chartData.set({
labels: [...current.labels, newLabel].slice(-10), // Ultimi 10
datasets: [{
...current.datasets[0],
data: [...current.datasets[0].data, newValue].slice(-10),
}],
});
}
}
TessStepper
Componente wizard multi-step con navigazione guidata.
Caratteristiche
- ✅ Linear/Non-linear: Navigazione sequenziale o libera
- ✅ Validazione: Valida step prima di procedere
- ✅ Accessibilità: Keyboard navigation e ARIA
- ✅ Customizzabile: Template personalizzabili per header/content
- ✅ Events: Hook per ogni cambio step
Proprietà
interface TessStepperProps {
linear?: boolean; // Forza navigazione sequenziale
activeIndex?: number; // Step attivo corrente
showStepNumbers?: boolean; // Mostra numeri step
}
interface TessStepProps {
label: string; // Label step
description?: string; // Descrizione opzionale
completed?: boolean; // Step completato
optional?: boolean; // Step opzionale
disabled?: boolean; // Step disabilitato
}
Esempi
Stepper Base
import { Component } from '@angular/core';
import { TessStepperComponent, TessStepComponent } from 'tess-ui-library';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-user-wizard',
standalone: true,
imports: [
TessStepperComponent,
TessStepComponent,
ReactiveFormsModule,
],
template: `
<tess-stepper [linear]="true" (stepChange)="onStepChange($event)">
<!-- Step 1: Dati personali -->
<tess-step label="Dati Personali" [completed]="step1Valid">
<form [formGroup]="personalForm">
<tess-input
formControlName="firstName"
placeholder="Nome">
</tess-input>
<tess-input
formControlName="lastName"
placeholder="Cognome">
</tess-input>
<tess-input
type="email"
formControlName="email"
placeholder="Email">
</tess-input>
</form>
<div class="step-actions">
<tess-button
label="Avanti"
(clicked)="nextStep()"
[disabled]="personalForm.invalid">
</tess-button>
</div>
</tess-step>
<!-- Step 2: Indirizzo -->
<tess-step label="Indirizzo" [completed]="step2Valid">
<form [formGroup]="addressForm">
<tess-input
formControlName="street"
placeholder="Via">
</tess-input>
<tess-input
formControlName="city"
placeholder="Città">
</tess-input>
<tess-input
formControlName="zipCode"
placeholder="CAP">
</tess-input>
</form>
<div class="step-actions">
<tess-button
label="Indietro"
severity="secondary"
(clicked)="previousStep()">
</tess-button>
<tess-button
label="Avanti"
(clicked)="nextStep()"
[disabled]="addressForm.invalid">
</tess-button>
</div>
</tess-step>
<!-- Step 3: Conferma -->
<tess-step label="Conferma" [completed]="false">
<div class="summary">
<h3>Riepilogo</h3>
<p><strong>Nome:</strong> {{ personalForm.value.firstName }} {{ personalForm.value.lastName }}</p>
<p><strong>Email:</strong> {{ personalForm.value.email }}</p>
<p><strong>Indirizzo:</strong> {{ addressForm.value.street }}, {{ addressForm.value.city }}</p>
</div>
<div class="step-actions">
<tess-button
label="Indietro"
severity="secondary"
(clicked)="previousStep()">
</tess-button>
<tess-button
label="Completa"
severity="success"
(clicked)="complete()">
</tess-button>
</div>
</tess-step>
</tess-stepper>
`,
})
export class UserWizardComponent {
personalForm = this.fb.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
});
addressForm = this.fb.group({
street: ['', Validators.required],
city: ['', Validators.required],
zipCode: ['', Validators.required],
});
step1Valid = false;
step2Valid = false;
currentStep = 0;
constructor(private fb: FormBuilder) {}
nextStep(): void {
if (this.currentStep === 0 && this.personalForm.valid) {
this.step1Valid = true;
this.currentStep++;
} else if (this.currentStep === 1 && this.addressForm.valid) {
this.step2Valid = true;
this.currentStep++;
}
}
previousStep(): void {
if (this.currentStep > 0) {
this.currentStep--;
}
}
onStepChange(index: number): void {
this.currentStep = index;
}
complete(): void {
const data = {
...this.personalForm.value,
...this.addressForm.value,
};
console.log('Completed:', data);
}
}
Stepper Non-linear
<tess-stepper [linear]="false">
<tess-step label="Configurazione" [optional]="true">
<!-- Contenuto opzionale -->
</tess-step>
<tess-step label="Dati Obbligatori">
<!-- Contenuto obbligatorio -->
</tess-step>
<tess-step label="Opzioni Avanzate" [optional]="true">
<!-- Contenuto opzionale -->
</tess-step>
</tess-stepper>
Eventi
@Output() stepChange = new EventEmitter<number>();
@Output() completed = new EventEmitter<void>();
TessTabs
Componente tabs per organizzare contenuti correlati.
Caratteristiche
- ✅ Lazy Loading: Carica contenuto tab solo quando attivato
- ✅ Closable: Tab chiudibili dinamicamente
- ✅ Icons: Supporto icone nei tab header
- ✅ Scrollable: Scroll automatico per molti tab
- ✅ Accessibilità: Keyboard navigation
Proprietà
interface TessTabsProps {
activeIndex?: number; // Tab attivo (0-based)
scrollable?: boolean; // Abilita scroll header
showAddButton?: boolean; // Mostra bottone "aggiungi tab"
}
interface TessTabProps {
label: string; // Label tab
icon?: string; // Icona tab
disabled?: boolean; // Tab disabilitato
closable?: boolean; // Tab chiudibile
}
Esempi
Tabs Base
import { Component } from '@angular/core';
import { TessTabsComponent, TessTabComponent } from 'tess-ui-library';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [TessTabsComponent, TessTabComponent],
template: `
<tess-tabs [activeIndex]="0" (tabChange)="onTabChange($event)">
<tess-tab label="Profilo" icon="pi pi-user">
<div class="profile-content">
<h3>Informazioni Profilo</h3>
<p>Nome: Mario Rossi</p>
<p>Email: mario@example.com</p>
</div>
</tess-tab>
<tess-tab label="Impostazioni" icon="pi pi-cog">
<div class="settings-content">
<h3>Impostazioni</h3>
<tess-switch label="Notifiche email"></tess-switch>
<tess-switch label="Dark mode"></tess-switch>
</div>
</tess-tab>
<tess-tab label="Statistiche" icon="pi pi-chart-bar">
<div class="stats-content">
<h3>Statistiche Utilizzo</h3>
<tess-chart type="line" [data]="chartData"></tess-chart>
</div>
</tess-tab>
<tess-tab label="Avanzate" icon="pi pi-sliders-h" [disabled]="!isAdmin">
<div class="advanced-content">
<h3>Configurazione Avanzata</h3>
<!-- Solo per admin -->
</div>
</tess-tab>
</tess-tabs>
`,
})
export class UserProfileComponent {
isAdmin = false;
onTabChange(index: number): void {
console.log('Active tab:', index);
}
}
Tabs Dinamici
export class DynamicTabsComponent {
tabs = signal([
{ id: 1, label: 'Dashboard', icon: 'pi pi-home', closable: false },
{ id: 2, label: 'Users', icon: 'pi pi-users', closable: true },
]);
activeTab = signal(0);
addTab(): void {
const newId = Math.max(...this.tabs().map(t => t.id)) + 1;
this.tabs.update(tabs => [
...tabs,
{
id: newId,
label: `Tab ${newId}`,
icon: 'pi pi-file',
closable: true,
},
]);
}
closeTab(index: number): void {
if (this.tabs()[index].closable) {
this.tabs.update(tabs => tabs.filter((_, i) => i !== index));
// Aggiusta activeTab se necessario
if (this.activeTab() >= this.tabs().length) {
this.activeTab.set(this.tabs().length - 1);
}
}
}
}
<div class="tabs-container">
<tess-tabs
[activeIndex]="activeTab()"
[scrollable]="true"
(tabChange)="activeTab.set($event)">
@for (tab of tabs(); track tab.id; let i = $index) {
<tess-tab
[label]="tab.label"
[icon]="tab.icon"
[closable]="tab.closable"
(close)="closeTab(i)">
<div class="tab-content">
Contenuto {{ tab.label }}
</div>
</tess-tab>
}
</tess-tabs>
<tess-button
icon="pi pi-plus"
label="Aggiungi Tab"
(clicked)="addTab()">
</tess-button>
</div>
Lazy Loading Content
export class LazyTabsComponent {
// Carica contenuto solo quando tab è attivato
loadedTabs = new Set<number>();
onTabChange(index: number): void {
if (!this.loadedTabs.has(index)) {
this.loadTabContent(index);
this.loadedTabs.add(index);
}
}
private loadTabContent(index: number): void {
// Simula caricamento async
console.log(`Loading content for tab ${index}`);
}
}
<tess-tabs (tabChange)="onTabChange($event)">
<tess-tab label="Tab 1">
@if (loadedTabs.has(0)) {
<app-heavy-component></app-heavy-component>
}
</tess-tab>
<tess-tab label="Tab 2">
@if (loadedTabs.has(1)) {
<app-another-heavy-component></app-another-heavy-component>
}
</tess-tab>
</tess-tabs>
Eventi
@Output() tabChange = new EventEmitter<number>();
@Output() tabClose = new EventEmitter<number>();
Styling
Tutti i componenti avanzati supportano personalizzazione CSS:
/* TessChart */
.tess-chart-container {
padding: 1rem;
background: var(--surface-card);
border-radius: var(--border-radius);
}
/* TessStepper */
.tess-stepper {
padding: 2rem;
}
.tess-step-header {
margin-bottom: 1rem;
}
.step-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 2rem;
}
/* TessTabs */
.tess-tabs-header {
border-bottom: 1px solid var(--surface-border);
}
.tess-tab-panel {
padding: 1.5rem;
}
Best Practices
TessChart
- Memoizza chartData: Usa signals o memorizza per evitare re-render
- Responsive: Usa opzioni responsive di Chart.js
- Performance: Limita numero di data points per grandi dataset
- Accessibility: Fornisci sempre
ariaLabel
TessStepper
- Validazione: Valida form in ogni step prima di procedere
- Feedback: Marca step completati visivamente
- Persistence: Salva progress per wizards lunghi
- Linear mode: Usa per processi che richiedono ordine specifico
TessTabs
- Lazy loading: Carica contenuto pesante solo quando necessario
- Limit tabs: Usa scrollable per molti tab
- Keyboard: Test keyboard navigation (Arrow keys, Home, End)
- State management: Persisti activeTab se necessario