pl-dynamicform
v1.0.20
Published
> Libreria Angular per la creazione dichiarativa di form reattivi complessi. > Niente template HTML manuali — descrivi il form in TypeScript, la libreria fa il resto.
Readme
PlDynamicForm
Libreria Angular per la creazione dichiarativa di form reattivi complessi.
Niente template HTML manuali — descrivi il form in TypeScript, la libreria fa il resto.
🔗 Demo live: https://lucapiciollo.github.io/pl-dynamicform-demo/
📦 npm: https://www.npmjs.com/package/pl-dynamicform
Indice
- Quick start per motori AI / agenti
- Panoramica
- Installazione e setup
- Concetti fondamentali
- DynamicFormBuilder — API fluente
- Contesto tipizzato
- Catalogo componenti
- Parametri comuni (FormActionBase)
- Sistema di eventi
- Oggetto Utility
- formCompletion — statistiche di completamento
- Combo statiche con tag e initialOptions
- Combo paginata remota — guida completa
- Rating — guida completa
- Color — guida completa
- Azioni (DynamicFormActionButton)
- Layout gruppi
- Utilizzo nel template
- Esempio completo reale
- Riferimento tipi TypeScript
Quick start per motori AI / agenti
Sezione pensata per assistenti di codice (Copilot, Cursor, Claude, ecc.). Copia il blocco qui sotto: contiene tutto il necessario per importare correttamente
pl-dynamicformcon tema light, senza dover ispezionare il codice della libreria.
SEI UN ASSISTENTE CHE INTEGRA LA LIBRERIA ANGULAR `pl-dynamicform` (Angular 19, Material).
Obiettivo: form reattivi dichiarativi, descritti in TypeScript, con TEMA LIGHT.
## 1. INSTALLA
npm install pl-dynamicform
npm install @angular/material @angular/cdk @angular/material-moment-adapter moment ux-directives @ionic-native/camera bootstrap
## 2. PROVIDER (scegli UNO)
// NgModule (app.module.ts)
import { PlDynamicFormModule, provideDynamicFormForModule } from 'pl-dynamicform';
@NgModule({
imports: [PlDynamicFormModule],
providers: [...provideDynamicFormForModule({
theme: { name: 'modern-light', mode: 'light' }, // TEMA LIGHT
matFormField: { appearance: 'outline' },
})],
})
export class AppModule {}
// OPPURE standalone (app.config.ts)
import { importProvidersFrom } from '@angular/core';
import { PlDynamicFormModule, provideDynamicForm } from 'pl-dynamicform';
export const appConfig = {
providers: [
importProvidersFrom(PlDynamicFormModule), // OBBLIGATORIO: inietta font/stili runtime
provideDynamicForm({ theme: { name: 'modern-light', mode: 'light' }, matFormField: { appearance: 'outline' } }),
],
};
## 3. STYLES (src/styles.scss) — OBBLIGATORIO
// La lib inietta da sola Material Icons + token --df-\* (tema light di default).
// Tu DEVI aggiungere SOLO: tema Material + griglia bootstrap.
@use '@angular/material' as mat;
@use 'bootstrap/scss/bootstrap-grid';
html {
@include mat.core();
$theme: mat.define-theme((color: (theme-type: light, primary: mat.$azure-palette)));
@include mat.all-component-themes($theme);
}
## 4. USO NEL COMPONENTE
import { DynamicFormBuilder, TYPE_CONTROL_FORM, ConfigForm } from 'pl-dynamicform';
import { FormControl, Validators } from '@angular/forms';
config: ConfigForm = DynamicFormBuilder.create(this)
.addGroup('Dati', ['col-12']) // 1° SEMPRE addGroup
.addForm({ formName: 'nome', title: 'Nome', type: TYPE_CONTROL_FORM.TEXT,
formControl: new FormControl('', Validators.required), class: ['col-md-6'] })
.addForm({ formName: 'email', title: 'Email', type: TYPE_CONTROL_FORM.TEXT,
formControl: new FormControl(''), class: ['col-md-6'] })
.addActions([{ label: 'Salva', visible: true, action: () => this.save() }])
.build();
## 5. TEMPLATE
<dynamic-form [configForm]="config"></dynamic-form>
## REGOLE INVIOLABILI
- addGroup() DEVE precedere addForm() (altrimenti errore runtime).
- Layout a colonne = classi bootstrap nel campo `class` (es. ['col-md-6']); senza bootstrap-grid i campi si impilano.
- NON aggiungere <link> per Material Icons: già iniettati dalla lib.
- Tema light = default; per dark usa theme.name:'modern-dark', mode:'dark'.
- Tipi campo (TYPE_CONTROL_FORM): TEXT, TEXTAREA, NUMBER, CURRENCY, CHECKBOX, RADIOGROUP, COMBO,
COMBOPAGINATE, ARRAYSTRING, DATA, DATARANGE, DATETIME, TIME, YEAR, RATING, COLOR, FILE, BUTTON, GROUP.Cosa deve sapere il motore AI sul DynamicFormBuilder
- È l'API fluente per costruire la configurazione:
DynamicFormBuilder.create(this)→.addGroup(...)→.addForm(...)→.addActions(...)→.build()che restituisce unConfigForm. create(this)passa il contesto tipizzato: dentroaddForm/addActionspuoi usare la forma factoryctx => ({...})per accedere a metodi/servizi del componente.forModel<TModel>().create(this)vincolaformNamealle chiavi del modello (autocompletamento + errori a compile-time).- Ogni campo è un
FormAction:{ formName, type, formControl, title, class, eventi, opzioni }. - I gruppi sono sezioni visive; il loro
classe quello dei campi usano la griglia bootstrap (col-*).
Cosa deve sapere il motore AI sui servizi
| Servizio / API | Ruolo | Serve azione manuale? |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------- |
| PlDynamicFormModule (costruttore) | All'avvio chiama DynamicFormAssetsService.loadDefaultAssets() + DynamicFormThemeService.init() | No: automatico |
| DynamicFormAssetsService | Inietta nel <head> i font Material Icons/Symbols e gli stili globali runtime (--df-*) | No |
| DynamicFormThemeService | Applica la classe df-theme-modern-light (o -dark) sul body e i custom token | No (config via theme) |
| provideDynamicForm() / ...ForModule() | Registra la configurazione runtime (theme, matFormField, events, actions) | Sì: passa la config |
| getFormByName / getActionByName / formCompletion (Utility) | Accesso runtime a campi, azioni e percentuale di completamento | Solo se necessario |
In sintesi per l'AI: font e tema della lib sono automatici (light di default); l'app host deve fornire solo tema Material + griglia in
styles.scss, importare il modulo e usare ilDynamicFormBuilder.
Panoramica
PlDynamicForm elimina la duplicazione nella scrittura di form Angular. Invece di un template HTML per ogni form, si descrive la struttura con oggetti TypeScript; la libreria genera automaticamente tutti i controlli, la validazione, gli eventi e il layout.
Funzionalità principali:
| Funzionalità | Descrizione |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 20 tipi di campo | TEXT, NUMBER, CURRENCY, COMBO, COMBOPAGINATE, DATA, DATARANGE, DATETIME, TIME, YEAR, RATING, COLOR, CHECKBOX, RADIOGROUP, TEXTAREA, FILE, ARRAYSTRING, SORTACTION, LABEL, GROUP |
| Builder fluente generico | DynamicFormBuilder.create(this) inferisce il tipo del componente |
| Contesto tipizzato | factory (ctx: TCtx) => FormAction con autocompletamento pieno |
| Tutti gli eventi | onChange, onInitialize, onFocus, onBlur, opened, closed, onSearch, onScrollEnd |
| Utility globale | getFormByName, getActionByName, setDefaultOptions, getSelectedOptions, formCompletion |
| Combo remote paginate | infinite scroll, Signal-based, ricerca debounced |
| initialOptions + tag | opzioni fisse in cima alla lista con badge SVG colorati |
| formCompletion Signal | percentuale di completamento reattiva (totale + required) |
| Stato disabled corretto | new FormControl({ value, disabled: true }) per bloccare interazioni |
Installazione e setup
L'integrazione richiede tre passi: (1) installare i pacchetti, (2) configurare i provider Angular, (3) configurare gli stili (SCSS). I passi 1 e 2 non bastano da soli: senza il setup SCSS il form viene renderizzato ma senza tema Material e senza layout a colonne.
1. Installazione pacchetti
# Libreria
npm install pl-dynamicform
# Peer dependencies obbligatorie
npm install @angular/material @angular/cdk @angular/material-moment-adapter moment
# Dipendenze runtime richieste dalla libreria
npm install ux-directives @ionic-native/camera
# Stili: tema Material + griglia per il layout a colonne
npm install bootstrap
@angular/forms,@angular/commone@angular/coresono già presenti in ogni progetto Angular.bootstrapè usato solo per la griglia (row,col-*,g-0): puoi sostituirlo con qualunque sistema a griglia che esponga le stesse classi, ma senza una griglia i campi si impilano a tutta larghezza.
| Pacchetto | Perché serve |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| @angular/material + @angular/cdk | Tutti i campi sono basati su componenti Material (mat-form-field, mat-select, mat-datepicker, mat-tab-group, …) |
| @angular/material-moment-adapter + moment | Adapter delle date per i campi DATA, DATARANGE, DATETIME, YEAR |
| ux-directives | Direttive UX importate internamente dal modulo |
| @ionic-native/camera | Provider Camera usato dal campo FILE (scan da fotocamera) |
| bootstrap | Griglia CSS per il layout a colonne dei gruppi/campi |
2. Configurazione dei provider
La libreria carica automaticamente (alla costruzione di PlDynamicFormModule):
- i font Material Icons / Material Symbols (via
<link>iniettato nel<head>); - gli stili globali runtime (CSS custom properties
--df-*e regole per gli overlay Material/CDK).
Quindi a livello di configurazione devi solo importare il modulo e (opzionalmente) passare la configurazione.
NgModule
import { PlDynamicFormModule, provideDynamicFormForModule } from 'pl-dynamicform';
@NgModule({
imports: [PlDynamicFormModule],
providers: [
...provideDynamicFormForModule({
matFormField: { appearance: 'outline' }, // default: 'outline'
}),
],
})
export class AppModule {}In alternativa, usa forRoot() (equivalente):
@NgModule({
imports: [PlDynamicFormModule.forRoot({ matFormField: { appearance: 'outline' } })],
})
export class AppModule {}Standalone
import { importProvidersFrom } from '@angular/core';
import { PlDynamicFormModule, provideDynamicForm } from 'pl-dynamicform';
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
importProvidersFrom(PlDynamicFormModule), // necessario: inizializza font e stili runtime
provideDynamicForm({
matFormField: { appearance: 'fill' }, // override opzionale
}),
],
};⚠️
importProvidersFrom(PlDynamicFormModule)è obbligatorio anche in standalone: è la costruzione del modulo a iniettare i font Material Icons e gli stili runtime.
Opzioni matFormField
| Proprietà | Tipo | Default | Descrizione |
| -------------------- | ---------------------- | ----------- | -------------------------------------------- |
| appearance | 'outline' \| 'fill' | 'outline' | Stile dei mat-form-field della libreria |
| subscriptSizing | 'fixed' \| 'dynamic' | 'fixed' | Gestione dello spazio per messaggi di errore |
| floatLabel | 'always' \| 'auto' | — | Comportamento del label flottante |
| hideRequiredMarker | boolean | — | Nasconde l'asterisco obbligatorio |
3. Configurazione SCSS / stili
Questa è la parte che genera più confusione. Distinguere tra ciò che la libreria fa da sola e ciò che devi fornire tu nell'app host.
Cosa fa già la libreria (non devi fare nulla)
| Aspetto | Gestito automaticamente | Come disabilitarlo |
| ------------------------------------------------------------------------- | ----------------------------------------- | ------------------------------------------------------------------ |
| Font Material Icons / Symbols | ✅ link iniettato nel <head> | provideDynamicForm({ theme: { loadMaterialIcons: false } }) |
| Tema interno della libreria (token --df-*, overlay Material/CDK) | ✅ <style> iniettato una volta | provideDynamicForm({ theme: { injectRuntimeStyles: false } }) |
| Classe tema (df-theme-modern-light / df-theme-modern-dark) sul body | ✅ applicata da DynamicFormThemeService | theme: { applyToBody: false } o theme: { rootSelector: '...' } |
Cosa devi aggiungere tu in styles.scss
La libreria non include un tema Angular Material né la griglia: questi vivono nell'app host e vanno aggiunti una volta nel file di stili globale (src/styles.scss):
/* 1. Tema Angular Material — OBBLIGATORIO.
Senza questo i mat-form-field, mat-select, mat-datepicker, ecc. risultano "rotti". */
@use '@angular/material' as mat;
html {
@include mat.core();
// Tema custom (consigliato)…
$theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
)
);
@include mat.all-component-themes($theme);
}
/* 2. Griglia CSS — OBBLIGATORIA per il layout a colonne (row / col-* / g-0). */
@use 'bootstrap/scss/bootstrap-grid';
/* 3. (Facoltativo) override dei token del tema della libreria. */
:root {
--df-primary: #2563eb;
--df-radius-md: 14px;
}In alternativa al tema custom, puoi usare un tema prebuilt Material (più rapido) tramite angular.json:
// angular.json → projects.<app>.architect.build.options.styles
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css", // tema Material prebuilt
"bootstrap/dist/css/bootstrap-grid.min.css", // solo griglia (o bootstrap.min.css completo)
"src/styles.scss"
]Perché serve la griglia? Il layout dei gruppi/campi usa classi Bootstrap (
row,g-0,col-12,col-md-6, …) passate tramite il parametroclassdei gruppi e dei campi. Senza una griglia che definisca queste classi, ogni campo occupa una riga intera.Perché serve il tema Material? Tutti i campi sono componenti Angular Material. La libreria tematizza solo le proprie estensioni (
--df-*) e gli overlay, ma il rendering base dei componenti Material richiede un tema fornito dall'app.
Esempio minimo completo di styles.scss
@use '@angular/material' as mat;
@use 'bootstrap/scss/bootstrap-grid';
html {
@include mat.core();
$theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$azure-palette,
),
)
);
@include mat.all-component-themes($theme);
}Checklist rapida
- [ ]
PlDynamicFormModuleimportato (NgModule) oimportProvidersFrom(PlDynamicFormModule)(standalone) - [ ] Tema Angular Material incluso (custom in
styles.scssoppure prebuilt inangular.json) - [ ] Griglia CSS inclusa (
bootstrap-grido equivalente) - [ ] Material Icons: già caricati dalla libreria (nessuna azione, salvo disabilitazione esplicita)
Concetti fondamentali
ConfigForm = Array<Group>
└── Group { id, title, class, formGroup[], actions[] }
└── Form { formAction: FormAction }
└── FormAction { formName, type, formControl, eventi, opzioni, ... }ConfigForm— array di gruppi, è ciò che si passa al componente templateGroup— sezione visiva del form con titolo, classi CSS e bottoni in fondoForm— wrapper{ formAction }di un singolo campoFormAction— configurazione completa di un campo: tipo,FormControl, eventi, opzioni, validazioni, stile
DynamicFormBuilder — API fluente
Metodi
| Metodo | Firma | Descrizione |
| -------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| create() | DynamicFormBuilder.create() | Builder senza contesto |
| create(ctx) | DynamicFormBuilder.create(this) | Builder con contesto tipizzato (tipo inferito) |
| forModel<TModel>() | DynamicFormBuilder.forModel<TModel>().create(ctx?) | Builder tipizzato sul modello: formName vincolato alle chiavi di TModel |
| addGroup | (title, classList?, id?) => this | Apre un nuovo gruppo |
| addForm | (formAction \| (ctx) => formAction) => this | Aggiunge un campo al gruppo corrente |
| addActions | (actions[] \| (ctx) => actions[]) => this | Aggiunge bottoni al gruppo corrente |
| build | () => ConfigForm | Restituisce la configurazione finale |
Regola base
addGroup deve sempre precedere addForm. Il builder lancia un errore esplicito altrimenti.
const config = DynamicFormBuilder.create()
.addGroup('Dati personali', ['col-6']) // apre gruppo
.addForm({ formName: 'nome', ... }) // campo nel gruppo
.addForm({ formName: 'email', ... }) // altro campo
.addActions([{ label: 'Salva', ... }]) // bottoni del gruppo
.addGroup('Note', ['col-12']) // nuovo gruppo
.addForm({ formName: 'note', ... })
.build();Tipizzazione sul modello (forModel)
forModel<TModel>() restituisce un builder in cui addForm({ formName }) accetta solo
le chiavi di TModel: i typo diventano errori di compilazione e l'editor offre
l'autocompletamento dei nomi campo. Il tipo del campo passa da FormAction a
TypedFormAction<TModel> (FormAction con formName: keyof TModel & string).
Usa il pattern a due step forModel<TModel>().create(ctx?): in questo modo il contesto
(this) resta inferito esattamente come con create(ctx).
interface AnagraficaModel {
firstName: string | null;
lastName: string | null;
email: string | null;
}
config = DynamicFormBuilder.forModel<AnagraficaModel>()
.create(this)
.addGroup('Dati anagrafici')
.addForm({
formName: 'firstName', // ✅ chiave del modello (autocompletata)
type: TYPE_CONTROL_FORM.TEXT,
formControl: new FormControl(''),
})
// .addForm({ formName: 'firstNam', ... }) // ❌ errore TS: non è una chiave di AnagraficaModel
.build();
create()senza modello mantieneformName: string. La tipizzazione sul modello e quella sul contesto sono indipendenti: puoi combinarle (forModel<TModel>().create(this)) oppure usarne una sola.
Contesto tipizzato
Il generico TCtx di DynamicFormBuilder<TCtx> viene inferito automaticamente dal valore passato a create(). Quando presente, addForm, addGroup e addActions accettano una factory function (ctx: TCtx) => valore che riceve il componente tipizzato.
Come si usa
// my.component.ts
@Component({ ... })
export class MyComponent {
userName = 'Mario';
myService = inject(MyService);
config = DynamicFormBuilder.create(this) // TCtx = MyComponent
.addGroup(ctx => `Ciao ${ctx.userName}`) // factory per il titolo
.addForm(ctx => ({ // factory per il campo
formName: 'preferenza',
type: TYPE_CONTROL_FORM.COMBO,
formControl: new FormControl(null),
options: ctx.myService.getOptions(), // servizio del componente
onChange: () => ctx.onPreferenzaChange(), // metodo del componente
}))
.addActions(ctx => [{
label: 'Salva',
visible: true,
action: () => ctx.save(), // metodo del componente
}])
.build();
}Pattern consigliato: builder esterno
Per form complessi è preferibile estrarre il builder in un file separato:
// form.builder.ts
export function buildMyForm<T extends MyComponent>(context: T): ConfigForm {
return DynamicFormBuilder.create(context)
.addGroup('Sezione')
.addForm(ctx => ({
formName: 'nome',
type: TYPE_CONTROL_FORM.TEXT,
formControl: new FormControl(''),
onChange: () => ctx.onNomeChange(), // ctx tipizzato come MyComponent
}))
.build();
}
// my.component.ts
export class MyComponent {
config = buildMyForm(this);
onNomeChange() {
/* ... */
}
}Accesso all'injector dal contesto
Quando si usa effect() o inject() dentro onInitialize, passare l'injector dal contesto:
.addForm(ctx => ({
formName: 'rating_automatico',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl({ value: null, disabled: true }),
optionRating: { max: 10 },
onInitialize: (_ig, _if, fc, _fn, _fg, _t, _all, _p, _o, utility) => {
effect(() => {
const stats = utility?.formCompletion?.();
fc.setValue(Math.round((stats?.percentage ?? 0) / 10));
}, { injector: ctx['injector'], allowSignalWrites: true });
},
}))Catalogo componenti
TEXT
Input testo a riga singola. Supporta modalità password e lunghezza massima.
{
formName: 'nome',
title: 'Nome completo',
type: TYPE_CONTROL_FORM.TEXT,
formControl: new FormControl('', Validators.required),
placeholder: 'Es. Mario Rossi',
optionInputText: {
maxlength: 100,
password: false, // true per mascherare il testo
},
resetButton: true, // mostra X per svuotare
autocomplete: false,
}TEXTAREA
Area di testo multi-riga.
{
formName: 'descrizione',
title: 'Descrizione',
type: TYPE_CONTROL_FORM.TEXTAREA,
formControl: new FormControl(''),
rows: 5, // altezza in righe (default: 3)
placeholder: 'Inserisci una descrizione...',
}NUMBER
Campo numerico con vincoli min/max/step. Mostra frecce su/giù.
{
formName: 'eta',
title: 'Età',
type: TYPE_CONTROL_FORM.NUMBER,
formControl: new FormControl(null, [Validators.min(0), Validators.max(120)]),
optionNumber: {
min: 0,
max: 120,
step: 1,
},
}CURRENCY
Come NUMBER ma formattato come valuta. Gestisce separatori decimali e migliaia.
{
formName: 'stipendio',
title: 'Stipendio mensile',
type: TYPE_CONTROL_FORM.CURRENCY,
formControl: new FormControl(null),
currency: 'EUR', // simbolo usato nell'input (EUR, USD, £, ...)
optionNumber: { min: 0 },
}CHECKBOX
Casella di spunta booleana. Il FormControl contiene true o false.
{
formName: 'accetta_termini',
title: 'Accetto i termini e condizioni',
type: TYPE_CONTROL_FORM.CHECKBOX,
formControl: new FormControl(false, Validators.requiredTrue),
}RADIOGROUP
Gruppo di opzioni mutuamente esclusive. Le opzioni sono fornite come Signal.
{
formName: 'genere',
title: 'Genere',
type: TYPE_CONTROL_FORM.RADIOGROUP,
formControl: new FormControl(null, Validators.required),
options: signal([
{ id: 'M', description: 'Maschio' },
{ id: 'F', description: 'Femmina' },
{ id: 'A', description: 'Preferisco non specificare' },
{ id: 'X', description: 'Altro', disabled: true }, // opzione non selezionabile
]),
}COMBO
Select con lista di opzioni. Supporta singola selezione, selezione multipla e ricerca inline.
import { signal } from '@angular/core';
const categorieOptions = signal([
{ id: 1, description: 'Sport', tag: { bgTag: 'tag-blue', bgText: 'tag-text-blue', name: 'A' } },
{ id: 2, description: 'Nutrizione' },
{ id: 3, description: 'Benessere', disabled: true },
]);
// Singola selezione
{
formName: 'categoria',
title: 'Categoria',
type: TYPE_CONTROL_FORM.COMBO,
formControl: new FormControl(null, Validators.required),
options: categorieOptions,
multiple: false,
resetButton: true,
keyCombo: {
keyId: 'id', // campo usato come valore del FormControl
keyDescription: 'description', // campo mostrato nella lista
keySearch: 'search', // parametro inviato all'API (solo COMBOPAGINATE)
},
initialOptions: [
{ id: null, description: 'Nessuna selezione', tag: { bgTag: 'tag-gray', bgText: 'tag-text-gray', name: 'Default' } },
],
opened: (_ig, _if, _fc, fn) => console.log(`${fn} aperto`),
closed: (_ig, _if, _fc, fn) => console.log(`${fn} chiuso`),
}
// Selezione multipla
{
formName: 'interessi',
title: 'Interessi',
type: TYPE_CONTROL_FORM.COMBO,
formControl: new FormControl([]),
options: categorieOptions,
multiple: true,
autocomplete: true, // mostra campo di ricerca nella lista
}Aggiornamento dinamico delle opzioni:
// Dal callback di un altro campo:
onChange: (_ig, _if, fc, _fn, _fg, _t, _prev, _all, utility) => {
utility.setDefaultOptions('sottocategoria', () => [
{ id: 1, description: `Sotto A di ${fc.value}` },
{ id: 2, description: `Sotto B di ${fc.value}` },
]);
};COMBOPAGINATE
Select con caricamento dati remoto, paginazione e infinite scroll.
Usa un Signal per le opzioni correnti e riceve la funzione remoteData per il caricamento.
Struttura base
import { signal } from '@angular/core';
// 1. Stato condiviso tra i callback
const options = signal<any[]>([]);
let total = 0;
let currentPage = 1;
let currentSearch = '';
const PAGE_SIZE = 10;
// 2. Funzione di caricamento
function loadItems(
{ page = 1, search = '', pageSize = PAGE_SIZE } = {},
append = false
) {
myApi.getItems({ page, pageSize, search }).subscribe(result => {
total = result.totalCount;
currentPage = page;
currentSearch = search;
if (append) {
// Infinite scroll: aggiunge senza duplicati
const existingIds = new Set(options().map(o => o.id));
options.set([
...options(),
...result.items.filter(o => !existingIds.has(o.id))
]);
} else {
// Reset lista (nuova ricerca o prima pagina)
options.set(result.items);
}
});
}
// 3. Campo nel form
{
formName: 'utente',
title: 'Utente',
type: TYPE_CONTROL_FORM.COMBOPAGINATE,
formControl: new FormControl(null, Validators.required),
options, // Signal con i dati correnti
totalCount: () => total, // funzione che restituisce il totale
enableInfiniteScroll: true,
autocomplete: true, // mostra campo di ricerca
multiple: false, // true per selezione multipla
keyCombo: {
keyId: 'id',
keyDescription: 'description',
keySearch: 'search', // nome del parametro di ricerca per l'API
},
pageSize: PAGE_SIZE,
paging: { page: 1, count: PAGE_SIZE, totalCount: 0 },
// Chiamata remota — param contiene { page, count, search }
remoteData: ({ param, append }) =>
new Promise(resolve => {
loadItems({ page: param.page, search: param.search, pageSize: param.count }, append);
setTimeout(() => resolve({ items: options(), totalCount: total }), 100);
}),
// Caricamento iniziale
onInitialize: () => loadItems({ page: 1 }),
// Ricerca: l'utente digita nel campo di ricerca
onSearch: (_ig, _if, _fc, _fn, _fg, search) => {
loadItems({ page: 1, search }); // reset alla pagina 1 con nuovo testo
},
// Infinite scroll: raggiunto il fondo della lista
onScrollEnd: () => {
if (options().length < total) {
loadItems({ page: currentPage + 1, search: currentSearch }, true);
}
},
onChange: (_ig, _if, fc, fn, _fg, _t, prev, _all, utility) => {
console.log(`${fn}:`, prev, '->', fc.value);
},
}Selezione multipla paginata
{
formName: 'tags',
title: 'Tag',
type: TYPE_CONTROL_FORM.COMBOPAGINATE,
formControl: new FormControl([]), // array per multipla
options: tagsOptions,
multiple: true, // selezione multipla
autocomplete: true,
enableInfiniteScroll: true,
keyCombo: { keyId: 'id', keyDescription: 'nome', keySearch: 'q' },
pageSize: 15,
paging: { page: 1, count: 15, totalCount: 0 },
remoteData: ({ param, append }) => /* ... */,
onInitialize: () => loadTags({ page: 1 }),
onSearch: (_ig, _if, _fc, _fn, _fg, search) => loadTags({ page: 1, search }),
onScrollEnd: () => { if (tagsOptions().length < totalTags) loadTags({ page: tagsPage + 1 }, true); },
}Parametri specifici di COMBOPAGINATE
| Proprietà | Tipo | Obbligatorio | Descrizione |
| ---------------------- | ----------------------------- | ------------ | -------------------------------- |
| options | Signal<any[]> | Sì | Signal con le opzioni correnti |
| remoteData | function | Sì | Funzione di caricamento dati |
| paging | { page, count, totalCount } | Sì | Stato iniziale della paginazione |
| pageSize | number | Raccomandato | Elementi per pagina |
| totalCount | () => number | Raccomandato | Totale elementi disponibili |
| enableInfiniteScroll | boolean | No | Caricamento auto allo scroll |
| autocomplete | boolean | No | Mostra input di ricerca |
| keyCombo.keySearch | string | No | Nome parametro ricerca per l'API |
ARRAYSTRING
Input per inserimento di stringhe multiple come chip/token. Ogni Enter o , aggiunge un elemento. Il FormControl contiene un array di stringhe.
{
formName: 'tags_liberi',
title: 'Tag liberi',
type: TYPE_CONTROL_FORM.ARRAYSTRING,
formControl: new FormControl([]),
placeholder: 'Scrivi e premi Invio...',
onChange: (_ig, _if, fc) => console.log('Array:', fc.value), // es. ['tag1','tag2']
}DATA
Datepicker Angular Material. Il FormControl contiene un oggetto Date o moment.
{
formName: 'data_nascita',
title: 'Data di nascita',
type: TYPE_CONTROL_FORM.DATA,
formControl: new FormControl(null, Validators.required),
optionDate: {
min: '1920-01-01', // stringa ISO o moment
max: new Date().toISOString().slice(0,10), // data massima = oggi
},
readonly: false, // se true non è editabile ma è apribile
}DATARANGE
Selettore di intervallo date (from/to). Il FormControl contiene { start: Date | null, end: Date | null }.
{
formName: 'periodo',
title: 'Periodo di riferimento',
type: TYPE_CONTROL_FORM.DATARANGE,
formControl: new FormControl(null),
optionDate: { min: '2000-01-01', max: '2030-12-31' },
onClose: (value, fc, utility) => {
// chiamato quando si chiude il pannello del range
console.log('Range:', value?.start, '->', value?.end);
},
}DATETIME
Selettore combinato di data e ora.
{
formName: 'appuntamento',
title: 'Data e ora appuntamento',
type: TYPE_CONTROL_FORM.DATETIME,
formControl: new FormControl(null, Validators.required),
optionDate: { min: new Date().toISOString() },
}TIME
Selettore solo orario. Il FormControl contiene una stringa 'HH:MM'.
{
formName: 'orario_apertura',
title: 'Orario apertura',
type: TYPE_CONTROL_FORM.TIME,
formControl: new FormControl(null),
optionsTime: {
min: '07:00',
max: '22:00',
},
}YEAR
Selettore solo anno con frecce prev/next. Il FormControl contiene un numero (es. 2024).
{
formName: 'anno_diploma',
title: 'Anno di diploma',
type: TYPE_CONTROL_FORM.YEAR,
formControl: new FormControl(null),
optionDate: {
min: '1970', // anno minimo come stringa
max: '2030', // anno massimo come stringa
},
}RATING
Valutazione a stelle. Il FormControl contiene un numero da 0 a max (0 = nessuna stella).
// Base — modificabile
{
formName: 'valutazione',
title: 'Valutazione',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl(null),
optionRating: { max: 5 }, // numero di stelle (default: 5)
resetButton: true, // mostra X per tornare a null
}
// Sola lettura — non cliccabile, senza hover
{
formName: 'punteggio_readonly',
title: 'Punteggio calcolato',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl({ value: 3, disabled: true }), // IMPORTANTE: disabled nel FormControl
optionRating: { max: 10 },
}
// Rating reattivo — aggiornato automaticamente tramite effect
// Mostra la percentuale di completamento del form come stelle
.addForm(ctx => ({
formName: 'completamento',
title: 'Completamento form',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl({ value: 0, disabled: true }),
optionRating: { max: 10 },
onInitialize: (_ig, _if, fc, _fn, _fg, _t, _all, _p, _o, utility) => {
effect(() => {
const stats = utility?.formCompletion?.();
const stelle = Math.round((stats?.percentage ?? 0) / 10);
fc.setValue(stelle);
}, { injector: ctx['injector'], allowSignalWrites: true });
},
}))Importante: per disabilitare interamente il componente rating (no hover, no click), usare
new FormControl({ value: ..., disabled: true })invece didisabled: truesulla FormAction.
La proprietàdisableddel FormAction è solo visiva e non blocca le interazioni.
Comportamento click:
- Click su stella non selezionata → seleziona quella stella
- Click sulla stessa stella già selezionata → porta il valore a
0(nessuna stella) resetButton: true→ mostra X che imposta il valore anull
FILE
Upload file con validazione tipo e dimensione.
{
formName: 'documento',
title: 'Allega documento',
type: TYPE_CONTROL_FORM.FILE,
formControl: new FormControl(null),
accept: 'application/pdf,image/*', // MIME types accettati
size: 10, // dimensione massima in MB
hint: 'Max 10MB — PDF o immagini',
onChange: (_ig, _if, fc) => {
const file: File = fc.value;
console.log('File:', file?.name, file?.size);
},
}Upload multiplo:
{
formName: 'allegati',
title: 'Allegati',
type: TYPE_CONTROL_FORM.FILE,
formControl: new FormControl([]),
multiple: true,
accept: '.pdf,.png,.jpg,.jpeg',
onChange: (_ig, _if, fc) => {
const files: File[] = fc.value;
console.log('File selezionati:', files.map(f => f.name));
},
}Comportamento reset:
Dopo aver chiamatoformGroup.reset()oformControl.reset()il campo si svuota correttamente e il file browser può essere riaperto selezionando lo stesso file precedentemente scelto — il componente gestisce automaticamente il reset del DOM nativo per evitare il bug del browser che ignora la riselezione dello stesso file.
COLOR
Selettore colore con supporto gradiente lineare. Il FormControl contiene una stringa CSS:
- colore solido: es.
"#FF0000" - gradiente: es.
"linear-gradient(90deg, #FF0000, #0000FF)"
// Solido (gradiente abilitato di default)
{
formName: 'colore_sfondo',
title: 'Colore sfondo',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl('#1976d2'),
resetButton: true,
hint: 'Seleziona colore o gradiente',
}
// Solo colore solido (gradiente disabilitato)
{
formName: 'colore_testo',
title: 'Colore testo',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl('#000000'),
optionColor: { enableGradient: false },
}
// Gradiente con angolo predefinito
{
formName: 'sfondo_gradiente',
title: 'Sfondo gradiente',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl('linear-gradient(135deg, #1976d2, #42a5f5)'),
optionColor: { enableGradient: true, defaultAngle: 135 },
}opzioni optionColor:
| Proprietà | Tipo | Default | Descrizione |
| --------------- | --------- | ------- | ------------------------------------------ |
| enableGradient| boolean | true | Mostra/nasconde la scheda gradiente |
| defaultAngle | number | 90 | Angolo iniziale del gradiente (0–360 gradi)|
Comportamento:
- Click sul campo apre un pannello a tendina con selettore solido e (se abilitato) selettore gradiente
- Modalità Solido: input nativo
type="color"del browser - Modalità Gradiente: due color picker + slider angolo + anteprima live
- Click fuori dal pannello chiude senza annullare la selezione corrente
resetButton: truemostra ✕ per azzerare il valore
BUTTON
Bottone con azione custom inline nel form (diverso dalle azioni di gruppo).
{
formName: 'carica_dati',
title: 'Carica dal profilo',
type: TYPE_CONTROL_FORM.BUTTON,
formControl: new FormControl(null),
action: (fc) => {
// logica al click
console.log('Bottone premuto');
},
css: { class: ['btn-outline-primary'] },
}GROUP
Campo che contiene un sotto-form annidato (espandibile o sempre visibile).
{
formName: 'indirizzo',
title: 'Indirizzo',
type: TYPE_CONTROL_FORM.GROUP,
formControl: new FormControl(null),
formGroup: DynamicFormBuilder.create()
.addGroup('Indirizzo', ['col-12'])
.addForm({ formName: 'via', title: 'Via', type: TYPE_CONTROL_FORM.TEXT, formControl: new FormControl('') })
.addForm({ formName: 'cap', title: 'CAP', type: TYPE_CONTROL_FORM.NUMBER, formControl: new FormControl(null) })
.addForm({ formName: 'citta', title: 'Città', type: TYPE_CONTROL_FORM.TEXT, formControl: new FormControl('') })
.build(),
}Parametri comuni (FormActionBase)
Tutte le FormAction ereditano questi parametri:
| Proprietà | Tipo | Descrizione |
| ---------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| formName | string | Identificatore univoco del campo — usato da getFormByName |
| title | string | Label visualizzata sopra il campo |
| type | TYPE_CONTROL_FORM | Tipo del campo |
| formControl | FormControl / FormArray / FormGroup | Control Angular Reactive Forms |
| disabled | boolean | Visivo — non blocca le interazioni; per bloccare usare new FormControl({ value, disabled: true }) |
| readonly | boolean | Campo visibile ma non modificabile |
| hidden | boolean | Esclude il campo dal DOM; il valore esiste comunque nel FormGroup (equivalente a css.hide = true) |
| placeholder | string | Testo segnaposto |
| hint | string | Testo di suggerimento sotto il campo |
| info | { msg: string; color: string } | Icona info con tooltip |
| tipContent | string | Tooltip sull'intero campo |
| resetButton | boolean | Mostra bottone X per azzerare il valore |
| autocomplete | boolean | Abilita autocomplete browser / ricerca inline combo |
| multiple | boolean | Selezione multipla (COMBO / COMBOPAGINATE) |
| css | TypeCss | Classi CSS custom, colore font, icone, ecc. |
| formGroup | ConfigForm | Sotto-form annidato |
| rows | number | Righe (TEXTAREA) |
| options | Signal / Array | Opzioni per COMBO, COMBOPAGINATE, RADIOGROUP |
| initialOptions | TypeComboOption | Opzioni fisse sempre in cima alla lista |
| onChange | DynamicFormOnChange | Callback al cambio valore |
| onInitialize | DynamicFormOnInitialize | Callback all'init del campo |
| onFocus | DynamicFormFocusBlur | Callback al focus |
| onBlur | DynamicFormFocusBlur | Callback alla perdita del focus |
| opened | DynamicFormOpenClose | Callback apertura pannello |
| closed | DynamicFormOpenClose | Callback chiusura pannello |
| onSearch | DynamicFormSearch | Callback digitazione nella ricerca |
| dateFilter | (date: Date) => boolean | Funzione di filtraggio calendario — disabilita le date che restituiscono false (DATA, DATARANGE, DATETIME) |
| dateFilterCSS | (date: Date) => string | Funzione che restituisce una classe CSS per personalizzare visivamente le date nel calendario |
| onScrollEnd | DynamicFormScrollEnd | Callback fondo lista paginata |
Sistema di eventi
Tutti i callback hanno una firma consistente. L'ultimo parametro è sempre utility.
Firme complete
// onChange — cambio valore
onChange: (
idGroup: number, // indice del Group nel ConfigForm
idForm: number, // indice del Form nel Group
formControl: FormControl, // FormControl con il NUOVO valore
formName: string, // formName del campo
formGroup: Form[], // campi del gruppo corrente
type: TYPE_CONTROL_FORM, // tipo del campo
prevValue: any, // valore PRECEDENTE
allGroup: ConfigForm, // intero ConfigForm
utility: Utility,
) => void | Promise<void>
// onInitialize — montaggio del campo nel DOM
onInitialize: (
idGroup: number,
idForm: number,
formControl: FormControl,
formName: string,
formGroup: Form[],
type: TYPE_CONTROL_FORM,
allGroup: ConfigForm,
paging?: { count: number; page: number; totalCount?: number } | null,
onOptionSetted?: Signal<any[]> | null,
utility?: Utility,
) => void | Promise<void>
// onFocus / onBlur — focus sul campo
onFocus: (
idGroup: number,
idForm: number,
formControl: FormControl,
formName: string,
formGroup: Form[],
allGroup: ConfigForm,
utility: Utility,
) => void | Promise<void>
// opened / closed — pannello aperto/chiuso (stessa firma di onFocus)
// onSearch — testo digitato nella ricerca della combo
onSearch: (
idGroup: number,
idForm: number,
formControl: FormControl,
formName: string,
formGroup: Form[],
search: string, // testo corrente nel campo di ricerca
utility: Utility,
) => void | Promise<void>
// onScrollEnd — fondo lista raggiunto (infinite scroll)
onScrollEnd: (
idGroup: number,
idForm: number,
formControl: FormControl,
formName: string,
formGroup: Form[],
paging: { count: number; page: number; totalCount?: number },
utility: Utility,
) => void | Promise<void>Convenzione di nomenclatura parametri inutilizzati
// Usare il prefisso _ per i parametri non usati — chiarisce l'intenzione
onChange: (_ig, _if, fc, formName, _fg, _t, prevValue, _all, utility) => {
console.log(`${formName}: ${prevValue} → ${fc.value}`);
};Oggetto Utility
Iniettato come ultimo parametro in tutti gli eventi. Fornisce accesso diretto ai campi e alle azioni del form senza navigare manualmente il ConfigForm.
getFormByName(formName, parse)
Legge o modifica un altro campo del form per nome.
// Disabilitare un campo in base al valore di un altro
onChange: (_ig, _if, fc, _fn, _fg, _t, _prev, _all, utility) => {
utility.getFormByName('campo_dipendente', fa => {
if (fc.value === 'speciale') {
fa.hidden = false;
fa.formControl?.setValidators(Validators.required);
fa.formControl?.updateValueAndValidity();
} else {
fa.hidden = true;
fa.formControl?.clearValidators();
fa.formControl?.updateValueAndValidity();
}
});
};setDefaultOptions(formName, parse)
Imposta dinamicamente le opzioni di una combo da un evento di un altro campo.
onChange: (_ig, _if, fc, _fn, _fg, _t, _prev, _all, utility) => {
const regioneId = fc.value;
myApi.getProvince(regioneId).subscribe(province => {
utility.setDefaultOptions('provincia', () => province);
});
};getSelectedOptions(formName, parse)
Recupera gli oggetti completi delle opzioni selezionate (non solo l'id).
utility.getSelectedOptions('categorie', opts => {
const selezionati = opts(); // Signal — chiamare come funzione
console.log('Selezionati completi:', selezionati);
const nomi = selezionati.map(o => o.description).join(', ');
});onSettedOptions(formName, parse)
Si sottoscrive all'evento di settaggio delle opzioni di una combo.
getActionByName(name, parse)
Recupera un bottone del form per nome e ne modifica lo stato.
// Disabilita il bottone Salva dinamicamente
onChange: (_ig, _if, _fc, _fn, _fg, _t, _prev, _all, utility) => {
const stats = utility.formCompletion();
utility.getActionByName('salva', action => {
action.disabled = stats.required.percentage < 100;
});
};formCompletion
Vedi sezione dedicata.
formCompletion — statistiche di completamento
utility.formCompletion è un Signal<FormCompletionStats> condiviso tra tutti i campi dello stesso form. Si aggiorna automaticamente a ogni valueChange.
Struttura
type FormCompletionStats = {
total: number; // campi tracciati (globale)
filled: number; // campi con valore non vuoto (globale)
percentage: number; // percentuale (0–100, intera) (globale)
required: {
total: number;
filled: number;
percentage: number;
};
/** Statistiche suddivise per gruppo */
groups: Array<{
id: string; // UUID del gruppo (assegnato dal builder)
title: string; // titolo del gruppo
total: number;
filled: number;
percentage: number;
required: {
total: number;
filled: number;
percentage: number;
};
}>;
};Campi esclusi dal conteggio
GROUP (i sotto-form annidati vengono conteggiati al loro interno)
Un campo è "compilato" quando
- Non è
null - Non è
undefined - Non è stringa vuota
'' - Se array: ha almeno un elemento
Un campo è "required" quando
Il suo FormControl ha Validators.required tra i validatori.
Utilizzo tipico
onChange: (_ig, _if, _fc, _fn, _fg, _t, _prev, _all, utility) => {
const s = utility.formCompletion();
// ── Statistiche globali ───────────────────────────────────────────────────
console.log(`Completamento: ${s.percentage}% (${s.filled}/${s.total} campi)`);
console.log(`Obbligatori: ${s.required.percentage}% (${s.required.filled}/${s.required.total})`);
// ── Statistiche per gruppo ───────────────────────────────────────────────
s.groups.forEach(g => {
console.log(
`Gruppo [${g.id}] "${g.title}":`,
`${g.percentage}% (${g.filled}/${g.total})`,
`| req: ${g.required.percentage}%`,
);
});
// Esempio: trovare un gruppo per titolo
const datiPersonali = s.groups.find(g => g.title === 'Dati Personali');
if (datiPersonali) {
console.log('Dati personali completati al', datiPersonali.percentage + '%');
}
// Abilita/disabilita il bottone Salva
utility.getActionByName('salva', action => {
action.disabled = s.required.percentage < 100;
});
// Aggiorna un campo di testo con la percentuale
utility.getFormByName('campo_info', fa => {
fa.formControl?.setValue(`Completato al ${s.percentage}%`, { emitEvent: false });
});
};Rating reattivo come progress indicator
.addForm(ctx => ({
formName: 'progress_indicator',
title: 'Completamento',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl({ value: 0, disabled: true }),
optionRating: { max: 10 },
onInitialize: (_ig, _if, fc, _fn, _fg, _t, _all, _p, _o, utility) => {
effect(
() => {
const stats = utility?.formCompletion?.();
fc.setValue(Math.round((stats?.percentage ?? 0) / 10));
},
{ injector: ctx['injector'], allowSignalWrites: true }
);
},
}))Combo statiche con tag e initialOptions
Struttura TypeComboOption
type TypeComboOption = Array<{
id: any;
description: string;
img?: string; // URL immagine (mostrata accanto alla descrizione)
extra?: any; // dati extra non visualizzati
disabled?: boolean; // opzione non selezionabile
default?: boolean; // preselezione automatica
hide?: boolean; // nascosta dalla lista
selected?: boolean; // selezionata correntemente
tag?: {
bgTag: string; // classe CSS fill del rettangolo SVG
bgText: string; // classe CSS fill del testo SVG
name: string; // testo del badge
};
}>;Tag colorati
I tag sono badge SVG affiancati alla descrizione. Le classi CSS devono essere definite globalmente in styles.scss:
// styles.scss
.tag-gray {
fill: #6b7280;
}
.tag-blue {
fill: #3b82f6;
}
.tag-green {
fill: #16a34a;
}
.tag-red {
fill: #dc2626;
}
.tag-orange {
fill: #ea580c;
}
.tag-indigo {
fill: #4f46e5;
}
.tag-purple {
fill: #9333ea;
}
.tag-yellow {
fill: #ca8a04;
}
.tag-text-gray,
.tag-text-blue,
.tag-text-green,
.tag-text-red,
.tag-text-orange,
.tag-text-indigo,
.tag-text-purple,
.tag-text-yellow {
fill: #ffffff;
}initialOptions
Opzioni sempre visibili in cima alla lista, indipendentemente da ricerca o paginazione. Ideali per "Nessuna selezione", "Tutti", "Default".
{
formName: 'stato',
type: TYPE_CONTROL_FORM.COMBO,
options: statiOptions,
formControl: new FormControl(null),
initialOptions: [
{
id: null,
description: 'Qualsiasi stato',
tag: { bgTag: 'tag-gray', bgText: 'tag-text-gray', name: 'Tutti' },
},
{
id: 'urgente',
description: 'Urgente',
tag: { bgTag: 'tag-red', bgText: 'tag-text-red', name: 'HOT' },
},
{
id: 'prioritario',
description: 'Prioritario',
tag: { bgTag: 'tag-orange', bgText: 'tag-text-orange', name: 'TOP' },
},
],
}Combo paginata remota — guida completa
Pattern con classe di stato
Per form complessi con più combo paginate è consigliabile incapsulare lo stato:
// combo-state.ts
export class ComboPaginatedState<T extends { id: any }> {
private _options = signal<T[]>([]);
private _total = 0;
private _page = 1;
private _search = '';
readonly options = this._options.asReadonly();
get total() { return this._total; }
get page() { return this._page; }
load(
items: T[],
total: number,
page: number,
search: string,
append: boolean
) {
this._total = total;
this._page = page;
this._search = search;
if (append) {
const ids = new Set(this._options().map(o => o.id));
this._options.set([...this._options(), ...items.filter(o => !ids.has(o.id))]);
} else {
this._options.set(items);
}
}
canLoadMore() {
return this._options().length < this._total;
}
}
// Nel builder:
const utentiState = new ComboPaginatedState<UserDto>();
const api = inject(UserApiService);
function fetchUtenti(page: number, search: string, append: boolean) {
api.getUsers({ page, pageSize: 10, search }).subscribe(r =>
utentiState.load(r.items, r.totalCount, page, search, append)
);
}
{
formName: 'utente_assegnato',
type: TYPE_CONTROL_FORM.COMBOPAGINATE,
formControl: new FormControl(null),
options: utentiState.options,
totalCount: () => utentiState.total,
enableInfiniteScroll: true,
autocomplete: true,
keyCombo: { keyId: 'id', keyDescription: 'fullName', keySearch: 'q' },
pageSize: 10,
paging: { page: 1, count: 10, totalCount: 0 },
remoteData: ({ param, append }) =>
new Promise(resolve => {
fetchUtenti(param.page, param.search ?? '', append);
setTimeout(() => resolve({ items: utentiState.options(), totalCount: utentiState.total }), 80);
}),
onInitialize: () => fetchUtenti(1, '', false),
onSearch: (_ig, _if, _fc, _fn, _fg, search) => fetchUtenti(1, search, false),
onScrollEnd: () => {
if (utentiState.canLoadMore()) fetchUtenti(utentiState.page + 1, '', true);
},
}Rating — guida completa
Modalità
| Modalità | FormControl | Descrizione |
| ------------------- | ----------------------------------------------- | ------------------------------------------ |
| Editabile | new FormControl(null) | L'utente può cliccare e cambiare il valore |
| Sola lettura visiva | new FormControl({ value: 3, disabled: true }) | No hover, no click — solo visualizzazione |
| Progress indicator | disabled: true + effect() | Aggiornato da un effect reattivo |
Comportamento click
| Situazione | Risultato |
| ---------------------------------- | -------------------------------------------------- |
| Click su stella non selezionata | Seleziona quella stella (valore = N) |
| Click sulla stella già selezionata | Deseleziona → valore 0 (nessuna stella illuminata) |
| resetButton: true + click X | Imposta valore a null |
| disabled: true (FormControl) | Nessun hover, nessun click possibile |
Valori del FormControl
| Valore | Stelle visualizzate |
| ----------- | ----------------------------------------------------------- |
| null | Nessuna stella (tutte spente) — campo non compilato |
| 0 | Nessuna stella (tutte spente) — campo considerato compilato |
| N (1–max) | N stelle illuminate |
Esempio completo
// Rating base — 5 stelle
{
formName: 'soddisfazione',
title: 'Soddisfazione',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl(null, Validators.required),
optionRating: { max: 5 },
resetButton: true,
hint: 'Da 1 (pessimo) a 5 (ottimo)',
onChange: (_ig, _if, fc, _fn, _fg, _t, prev, _all, utility) => {
const s = utility.formCompletion();
console.log(`Rating: ${prev} -> ${fc.value} | Form: ${s.percentage}%`);
},
}
// Rating 10 stelle — sola lettura con valore dinamico
.addForm(ctx => ({
formName: 'completamento_visivo',
title: 'Completamento form',
type: TYPE_CONTROL_FORM.RATING,
formControl: new FormControl({ value: 0, disabled: true }),
optionRating: { max: 10 },
onInitialize: (_ig, _if, fc, _fn, _fg, _t, _all, _p, _o, utility) => {
effect(
() => {
const pct = utility?.formCompletion?.()?.percentage ?? 0;
fc.setValue(Math.round(pct / 10)); // 0–100% → 0–10 stelle
},
{ injector: ctx['injector'], allowSignalWrites: true }
);
},
}))Color — guida completa
Valore del FormControl
Il componente COLOR memorizza il colore come stringa CSS pura:
| Modalità | Esempio di valore |
| -------- | ------------------------------------------------------ |
| Solido | "#1976d2" |
| Gradiente| "linear-gradient(90deg, #1976d2, #42a5f5)" |
Questo semplifica la lettura e l'uso diretto nel CSS:
const sfondo: string = this.formGroup.get('colore')?.value;
// uso diretto
this.renderer.setStyle(el, 'background', sfondo);Configurazione completa
.addForm({
formName: 'tema_colore',
title: 'Tema cromatico',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl('linear-gradient(90deg, #1976d2, #42a5f5)'),
resetButton: true,
placeholder: 'Nessun colore selezionato',
hint: 'Puoi scegliere un colore solido o un gradiente lineare',
optionColor: {
enableGradient: true, // abilita tab gradiente (default: true)
defaultAngle: 90, // angolo iniziale slider
},
onChange: (_ig, _if, fc) => {
console.log('Colore selezionato:', fc.value);
},
})Sola lettura
Per mostrare il colore senza consentire modifiche, usa disabled nel FormControl:
{
formName: 'colore_brand',
title: 'Colore brand',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl({ value: '#1976d2', disabled: true }),
optionColor: { enableGradient: false },
}Lettura del valore con onInitialize
.addForm(ctx => ({
formName: 'colore_sfondo',
title: 'Colore sfondo',
type: TYPE_CONTROL_FORM.COLOR,
formControl: new FormControl(null),
optionColor: { enableGradient: true },
onInitialize: (_ig, _if, fc) => {
// carica valore salvato dal backend
ctx.myService.getColor().subscribe(c => fc.setValue(c));
},
}))Azioni (DynamicFormActionButton)
I bottoni delle azioni vengono visualizzati nel piede del gruppo a cui appartengono, definiti con .addActions() dopo .addGroup().
Parametri action — firma completa
action: (
questions: Array<Form>, // campi del gruppo corrente
idForm: string | number, // id del gruppo (UUID o indice)
groupForm: FormGroup | FormArray, // reactive form del solo gruppo — usare per reset, validazione locale
group?: Group, // oggetto Group corrente
idGroup?: number, // indice del gruppo nel ConfigForm
allGroup?: ConfigForm, // intera struttura ConfigForm
totalForm?: FormGroup | FormArray,// reactive form di TUTTO il form (tutti i gruppi) — usare per submit
utility?: Utility // helper — formCompletion, getFormByName, ecc.
) => void
groupFormvstotalForm:
groupFormè ilFormGroupdel solo gruppo che contiene il bottone. Usalo perreset()ogetRawValue()scoped al gruppo.totalFormè ilFormGroupoFormArraydell'intero form (tutti i gruppi). Usalo per submit, validazione glob
