npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

npm Angular License: MIT

🔗 Demo live: https://lucapiciollo.github.io/pl-dynamicform-demo/
📦 npm: https://www.npmjs.com/package/pl-dynamicform


Indice

  1. Quick start per motori AI / agenti
  2. Panoramica
  3. Installazione e setup
  4. Concetti fondamentali
  5. DynamicFormBuilder — API fluente
  6. Contesto tipizzato
  7. Catalogo componenti
  8. Parametri comuni (FormActionBase)
  9. Sistema di eventi
  10. Oggetto Utility
  11. formCompletion — statistiche di completamento
  12. Combo statiche con tag e initialOptions
  13. Combo paginata remota — guida completa
  14. Rating — guida completa
  15. Color — guida completa
  16. Azioni (DynamicFormActionButton)
  17. Layout gruppi
  18. Utilizzo nel template
  19. Esempio completo reale
  20. 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-dynamicform con 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 un ConfigForm.
  • create(this) passa il contesto tipizzato: dentro addForm/addActions puoi usare la forma factory ctx => ({...}) per accedere a metodi/servizi del componente.
  • forModel<TModel>().create(this) vincola formName alle 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 class e 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 il DynamicFormBuilder.


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/common e @angular/core sono 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 parametro class dei 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

  • [ ] PlDynamicFormModule importato (NgModule) o importProvidersFrom(PlDynamicFormModule) (standalone)
  • [ ] Tema Angular Material incluso (custom in styles.scss oppure prebuilt in angular.json)
  • [ ] Griglia CSS inclusa (bootstrap-grid o 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 template
  • Group — sezione visiva del form con titolo, classi CSS e bottoni in fondo
  • Form — wrapper { formAction } di un singolo campo
  • FormAction — 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 mantiene formName: 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 di disabled: true sulla FormAction.
La proprietà disabled del 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 a null

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 chiamato formGroup.reset() o formControl.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: true mostra ✕ 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

groupForm vs totalForm:

  • groupForm è il FormGroup del solo gruppo che contiene il bottone. Usalo per reset() o getRawValue() scoped al gruppo.
  • totalForm è il FormGroup o FormArray dell'intero form (tutti i gruppi). Usalo per submit, validazione glob