@wemake4u/form-controls
v2.0.2
Published
Angular UI components (data entry, layout and presentation)
Readme
@wemake4u/form-controls
Componenti UI Angular (data entry, layout e presentazione) basati sul Sirio web kit.
Installazione
npm install @wemake4u/form-controlsRequisiti: Angular 22 (@angular/core, @angular/common, @angular/forms) e il Sirio web kit 9.1.0.
Sirio web kit (prerequisito, non incluso)
La libreria non include e non distribuisce il Sirio web kit: genera il markup e le classi del kit, ma CSS,
font e immagini li deve fornire l'applicazione. Versione richiesta: sirio-kit-web 9.1.0 (ultima versione rilasciata).
Sola lettura: aspetto dei campi
Nel kit un campo in sola lettura ha lo stesso aspetto di un campo disabilitato (sfondo e bordo grigi). Con
readonlyAppearance i campi in sola lettura possono avere invece lo sfondo bianco e il bordo chiaro del kit, così
si distinguono dai disabilitati (che restano grigi):
| Valore | Aspetto in sola lettura |
| --- | --- |
| filled (default) | come il kit: sfondo e bordo grigi, uguale al disabilitato (anche la select, che il kit non copre) |
| outline | sfondo bianco, bordo chiaro del kit (#e3e5e8), testo nel colore normale del campo |
// Default per tutta l'applicazione (app.config.ts) o per un componente (providers)
providers: [provideSirioReadonlyAppearance('outline')]<!-- Override per il singolo controllo -->
<wm-sirio-select label="Provincia" readonly readonlyAppearance="outline" ... />Vale per textbox, textarea, number, date, time, datetime, select (anche searchable), autocomplete, searchbar e
slider (campo numerico); prefissi e suffissi dei campi seguono lo stesso aspetto. Con outline restano invariati i
bordi degli stati di validazione (errore, valido, avviso) e del focus da tastiera. Checkbox, radio, checklist e
file upload non hanno un campo con sfondo: l'impostazione non li modifica.
Select con ricerca: pulsante di azzeramento
Di default la select con searchable non mostra il pulsante "X" del kit: il valore si cambia scegliendo un'altra
opzione, o si azzera da codice. Con clearable il pulsante viene mostrato e svuota ricerca e selezione.
<wm-sirio-select label="Comune" searchable clearable [options]="comuni" formControlName="comune" />Select: opzioni in caricamento
Con loading la select mostra che le opzioni stanno arrivando (es. da un servizio): il campo chiuso mostra
loadingText ("Caricamento in corso") al posto del placeholder con lo spinner del kit, la lista contiene solo la riga
"Caricamento in corso" con lo spinner, il combobox ha aria-busy e il messaggio è annunciato agli screen reader.
<wm-sirio-select label="Provincia" [loading]="province() === null" [options]="province() ?? []" formControlName="prov" />Data grid
wm-sirio-data-grid genera la tabella del kit (table.sirio-table); filtri, ordinamento e paginazione dei dati
sono elaborati da TanStack Table (@tanstack/angular-table 9, dipendenza installata con la
libreria; l'istanza è esposta come table per usi avanzati). La griglia ha ordinamento anche su più colonne,
paginazione (wm-sirio-pagination), selezione delle righe, colonne ridimensionabili e riordinabili, righe espandibili
(dettaglio e dati ad albero), raggruppamento con totali e i layout del kit. Le colonne si dichiarano con
wm-sirio-grid-column; celle e intestazioni personalizzate con i template wmSirioGridCell e
wmSirioGridHeader, la riga vuota con wmSirioGridEmpty, il dettaglio delle righe con wmSirioGridDetail.
Import unico: imports: [SIRIO_DATA_GRID].
<wm-sirio-data-grid caption="Pratiche" [data]="pratiche" rowId="id" selection="multiple" [(selected)]="scelte">
<wm-sirio-grid-column field="numero" header="Numero" sortable filter="text" />
<wm-sirio-grid-column field="importo" header="Importo" align="end" sortable [format]="euro" filter="number" />
<wm-sirio-grid-column field="stato" header="Stato">
<ng-template wmSirioGridCell let-row><span [wmSirioTag]="tipo(row)">{{ row.stato }}</span></ng-template>
</wm-sirio-grid-column>
</wm-sirio-data-grid>Dati in memoria (
[data]): la griglia ordina (confronto numerico, per data o testo con le regole della lingua diLOCALE_ID) e pagina.Formati predefiniti:
formataccetta una funzione(valore, riga) => testooppure il nome di un formato, con le opzioni informatOptions(tutti seguonoLOCALE_ID;empty= testo dei valori vuoti):number(decimals,minDecimals/maxDecimals,grouping),currency(currency, default EUR),percentage(decimals,scale: 'percent'se il valore è già 15),date(precision:day,month,year),dateTimeetime(seconds),boolean(trueLabel,falseLabel, default Sì/No),map(values: oggetto oSirioOption[]). Numeri e date si leggono anche da testo ('89.9','2026-01-31','2026-01-31T14:30'). I formati numerici sono allineati a destra (sealignnon è indicato), numeri e date usano il font del kit e si ordinano per valore; conmapil filtroselectelenca tutti i codici con la loro etichetta.provideSirioGridFormats({ defaults, formatDefaults, formats })imposta le opzioni di default e registra formati propri per nome (o sostituisce quelli predefiniti).Formati di
LOCALE_ID: senzaformat, i numeri delle celle usano il separatore decimale della lingua (senza separatore delle migliaia, così codici e anni restano invariati) e i valoriDateil formato data; conteggi, numeri e date nei chip dei filtri e nei risultati seguonoLOCALE_ID(es.it-IT: "1234,5", "31/01/2026", "10.000").Font dei numeri del kit: le celle numeriche e di data (colonne con filtro
number/date, o con valori numero oDate), i numeri di riga, i conteggi e i totali dei gruppi usano la classe del kitfont-mono(Roboto Mono, come campi numerici, date e paginazione del kit);monosulla colonna lo forza (true) o lo toglie (false).Intestazioni su una riga: il titolo è tagliato con i puntini (testo completo nel tooltip), l'indice di ordinamento e l'imbuto del filtro restano visibili.
filterButton="hover"mostra l'imbuto solo al passaggio del mouse, con il focus o quando il filtro è attivo (come ag-grid);filterButton="none"toglie i controlli di filtro dalla griglia (anche il pannello del layout a card): i filtri si impostano solo da codice ([(filters)],setFilter); defaultalways.Riga di filtri (
filterRow): una riga sotto le intestazioni, come il floating filter di ag-grid. Ogni colonna con filtro ha un campo per la condizione semplice (testo: contiene, numero e data: uguale, select e booleano: un valore; confilterOperatorsl'operatore scelto) e l'imbuto con il popover degli operatori; una condizione che non sta nel campo (es. "compreso tra", "vuoto", più valori) è mostrata come riepilogo. Esc svuota il filtro della colonna. Il campo si chiama "Filtra per " (filterLabel), l'imbuto "Altre condizioni per " (filterMoreLabel). Con la riga di filtri i chip dei filtri non sono mostrati; nel layout a card resta il pannello "Filtri".Testo su una riga: i testi lunghi vanno a capo (default);
[wrap]="false"sulla colonna tiene il testo su una riga (la colonna si allarga, o il testo è tagliato con i puntini se la colonna ha una larghezza). Lo stessofieldpuò comparire in più colonne (es. una data mostrata come data e come ora), ognuna con il proprio filtro: infiltersla prima colonna usa ilfield, le altrefield~2,field~3(o il lorofilterKey).Colonna larga quanto il contenuto:
fitsulla colonna (es. una colonna di azioni): non prende parte dello spazio libero della tabella, le celle restano su una riga e il titolo non viene tagliato.Dati da un FormArray (
[formArray]): unFormGroupper riga; la griglia mostragetRawValue()di ogni gruppo e segue le modifiche del FormArray (valori, righe aggiunte o tolte da codice). Le righe si modificano in una finestra di dialogo del kit con il form di<ng-template wmSirioGridEditor let-group>, aperta da "Modifica" nella colonna azioni (fissa a destra) o con il doppio clic (editOnDoubleClick). Il form lavora su una copia della riga creata darowFactory(valore)(senzarowFactory: copia generica con gli stessi controlli, valori e validatori): "Annulla" scarta tutto, "Salva" segna i campi come toccati, attende i validatori asincroni e applica i valori solo se la copia è valida (la riga diventa dirty).saveHandler/deleteHandler(funzioni che restituiscono un valore, una Promise o un Observable) controllano la modifica prima di applicarla, ad esempio con una chiamata al server:falseo un errore la bloccano e il messaggio d'errore compare nella finestra.addableaggiunge "Aggiungi" (nuova riga darowFactory(null), oaddRow(valoriIniziali), aggiunta in fondo),deletable"Elimina" sulle righe (con conferma,confirmDelete) ed "Elimina selezionati" con la selezione multipla;rowEditable/rowDeletablescelgono le righe. Con il FormArray disabilitato le righe si aprono in sola lettura ("Visualizza"). DopomarkAllAsTouched()le righe non valide hanno il marcatore e l'icona di errore, gli errori del FormArray compaiono sotto la griglia (errorMessages; per "almeno una riga" usareValidators.required, perchéValidators.minLengthignora un array vuoto). Eventi(rowSave)e(rowDelete); metodieditRow,addRow,deleteRow,deleteSelected,groupOf(riga). SenzarowIdla selezione segue il FormGroup della riga. Non si usa insieme a[dataSource].<wm-sirio-data-grid [formArray]="righe" [rowFactory]="creaRiga" addable deletable (rowSave)="salvata($event)"> <wm-sirio-grid-column field="descrizione" header="Descrizione" /> <wm-sirio-grid-column field="importo" header="Importo" format="currency" /> <ng-template wmSirioGridEditor let-group let-isNew="isNew"> <div [formGroup]="group"> <wm-sirio-textbox formControlName="descrizione" label="Descrizione" /> <wm-sirio-number formControlName="importo" label="Importo" /> </div> </ng-template> </wm-sirio-data-grid>Dati lato server (
[dataSource]): una funzione che riceve{ page, pageSize, offset, sort, filters }e restituisce{ rows, total }(Observable, Promise o valore). Viene richiamata a ogni cambio di pagina o ordinamento e conreload(); le risposte superate vengono ignorate (un Observable precedente viene annullato).- Totale facoltativo: senza
totalil totale si ricava dalle pagine, come lo scroll infinito di ag-grid. Una pagina piena significa che ce ne sono altre: "Successiva" resta attiva, la paginazione mostra "di 30+ elementi" ("oltre 30" per gli screen reader, testomoreLabel) e il conteggio dei risultati "oltre 30". Una pagina più corta è l'ultima e fissa il totale. Cambiando ordinamento, filtri o righe per pagina si riparte da capo.wm-sirio-paginationha lo stesso comportamento conhasMore. - Errori: oltre a
(loadError), la tabella mostraloadErrorText("Non è stato possibile caricare i dati.") con il pulsante "Riprova" (retryLabel), che richiama il data source; la paginazione resta per tornare indietro. Con<ng-template wmSirioGridError let-error let-retry="retry">il contenuto è personalizzato.
- Totale facoltativo: senza
Filtri per colonna (
filtersulla colonna): pulsante nell'intestazione che apre il popover del kit con i controlli della libreria. Tipi:text(contiene, senza distinzione di maiuscole e accenti),numberedate(intervallo Da / A),select(confilterOptions, anchefilterMultiple; senzafilterOptionsla scelta è tra i valori presenti nella colonna, con il numero di righe, calcolati con gli altri filtri applicati),boolean(Sì / No). I filtri attivi compaiono sopra la tabella come chip del kit, rimovibili uno a uno o tutti insieme; nel layout a card sono in un pannello "Filtri". Stato two-way[(filters)](per chiave di colonna, es.{ comune: 'Roma', importo: { from: 1000 } }), filtro personalizzato confilterPredicate, ritardo dei filtri di testo confilterDebounce(300 ms). Con[dataSource]i filtri attivi arrivano nella query (query.filters).Operatori dei filtri: con
filterOperatorssulla griglia (o sulla colonna:true,falseo l'elenco degli operatori ammessi) il popover ha una scelta "Condizione" sopra il valore e il filtro diventa{ operator, value, to? }(SirioGridFilterCondition). Testo: contiene, non contiene, uguale a, diverso da, inizia con, finisce con; numero: uguale a, diverso da, maggiore di, maggiore o uguale a, minore di, minore o uguale a, compreso tra; data: uguale a, diverso da, prima del, dopo il, compreso tra; elenco: è uno di, non è uno di; per tutti vuoto e non vuoto. Chip come "Importo: compreso tra 10.000 e 30.000"; etichette infilterOperatorLabels,filterConditionLabel,filterValueLabel,filterAndLabel. Un valore semplice (es. uno stato salvato) viene letto come condizione equivalente (testo → contiene, intervallo → compreso tra, elenco → è uno di).Ordinamento:
[(sort)]è l'elenco dei criteri in ordine di priorità ([{ field: 'comune', direction: 'asc' }]). ConsortMode="multiple"Maiusc+clic (o Maiusc+Invio) aggiunge una colonna ai criteri, mostrando la priorità nell'intestazione; il clic semplice sostituisce l'ordinamento.Colonne ridimensionabili (
resizable): maniglia sul bordo destro delle intestazioni per il mouse (trascinamento, doppio clic per ripristinare); da tastiera Alt+frecce sull'intestazione, come ag-grid;minWidth/maxWidtheresizable="false"sulla colonna; larghezze two-way[(columnWidths)](px per chiave di colonna).Colonne riordinabili (
reorderable): trascinamento delle intestazioni o Shift+frecce su un'intestazione, con annuncio della nuova posizione; ordine two-way[(columnOrder)]. "Ripristina colonne" nella barra sopra la tabella.Righe espandibili: con
<ng-template wmSirioGridDetail let-row>ogni riga ha un pulsante che apre un pannello di dettaglio sotto di essa (rowExpandableper sceglierle); consubRows(proprietà o funzione) i dati sono un albero, con rientro per livello. Stato two-way[(expanded)],expandAll(), "Espandi tutto" / "Comprimi tutto".Numero di riga (
rowNumbers): colonna "N." all'inizio, numero continuo tra le pagine nell'ordine e con i filtri correnti (lato server dall'offset della query); le righe di gruppo non sono numerate, i figli degli alberi sì per livello (1, 1.1, 1.2). È l'intestazione di riga (role="rowheader"); testirowNumberHeadererowNumberLabel.Intestazioni raggruppate:
groupsulla colonna con i nomi dei gruppi dal livello più alto, separati da/(group="Richiedente/Residenza"); una riga d'intestazione per livello (scope="colgroup"), le colonne vicine con gli stessi gruppi condividono la cella, un gruppo si divide se le sue colonne non sono vicine o stanno in blocchi fissi diversi. ConstickyHeaderle righe restano impilate.Colonne flex:
flexsulla colonna (flex="2",flexsenza valore = 1) come in CSS / ag-grid: le colonne conflexsi dividono in proporzione la larghezza lasciata libera dalle altre (che tengonowidtho la larghezza del contenuto), entrominWidth/maxWidth; si ricalcola quando cambia la larghezza della griglia. Una colonna flex ridimensionata a mano tiene la sua larghezza fino al doppio clic sulla maniglia o a "Ripristina colonne".Colonne fisse:
fixed="left"ofixed="right"sulla colonna la tiene ferma durante lo scroll orizzontale; più colonne per lato, mostrate su quel lato della tabella (le colonne si spostano solo all'interno del proprio blocco). Selezione, espansione e numero di riga restano fissi con le colonne di sinistra.fixedFirstColumn/fixedLastColumnsono scorciatoie per la prima e l'ultima colonna. Attive nel contenitore responsive, non nel layout a card; al bordo del blocco fisso compare l'indicatore di scroll del kit.Classi e stili:
rowClass/rowStylesulla griglia,cellClass/cellStylesulla colonna (valori fissi o funzioni: della riga e del suo indice per le righe, della riga e del valore per le celle),headerClass/headerStylesull'intestazione della colonna. Le classi sono comengClass(testo, elenco o{ classe: condizione }), gli stili comengStyle(testo CSS o oggetto, anche'width.px': 80). Uno sfondo dirowStyle/cellStylevale anche sulle righe a strisce e sulle colonne fisse; la selezione copre lo sfondo della riga, non quello della cella. Per gli sfondi è meglio usare gli stili: le classi del kitsirio-bg-*sulle righe non battono le strisce del kit.<wm-sirio-data-grid [data]="pratiche" [rowClass]="classeRiga" [rowStyle]="stileRiga"> <wm-sirio-grid-column field="importo" header="Importo" format="currency" headerClass="sirio-text-primary" [cellClass]="importoNegativo" /> </wm-sirio-data-grid>classeRiga = (riga: Pratica) => ({ 'sirio-text-disabled': riga.stato === 'Chiusa' }); stileRiga = (riga: Pratica) => (riga.scaduta ? { 'background-color': '#f7e4ea' } : null); importoNegativo = (_riga: Pratica, valore: unknown) => ({ 'sirio-text-danger': Number(valore) < 0 });Raggruppamento (solo con
[data]): colonnegroupablee "Raggruppa per" sopra la tabella ([(grouping)]); le righe di gruppo mostrano valore e numero di righe, le altre colonne il valore diaggregate(sum,mean,min,max,count). Con gruppi o alberi la paginazione conta le righe di primo livello.Paginazione:
pageable,[(page)],[(pageSize)],pageSizeOptions; aspetto diwm-sirio-paginationconpaginationVariant(full,numberscon i numeri di pagina e i puntini, oppurecompact, una barra sottile con "Pagina 1 di 6" e i pulsanti prima / precedente / successiva / ultima),paginationSiblingsepaginationShowItems.Stato two-way:
[(sort)],[(page)],[(pageSize)],[(filters)],[(grouping)],[(expanded)],[(columnWidths)],[(columnOrder)],[(selected)](righe confrontate perrowId).Salvataggio dello stato:
getState()restituisce lo stato scelto dall'utente come JSON (SirioGridState: ordinamento, filtri, pagina e righe per pagina, ordine e larghezze delle colonne, raggruppamento, righe espanse,rowIddelle righe selezionate),setState(stato)lo ripristina (anche in parte; i filtri si applicano subito senza tornare alla prima pagina, criteri di colonne non più presenti vengono ignorati),resetState()torna allo stato iniziale;(stateChange)e il signalstateseguono ogni modifica, per il salvataggio automatico.Selezione:
selection="single"(radio) o"multiple"(checkbox del kit con "seleziona tutte";selectAllsceglie le righe prese:pagequelle della pagina, default,filteredtutte quelle che passano i filtri su tutte le pagine, comeselectAll: 'filtered'di ag-grid,alltutte; con[dataSource]sempre la pagina caricata);selectOnRowClickseleziona anche al clic sulla riga; con[selectionColumn]="false"niente colonna di checkbox: si seleziona con il clic sulla riga (inmultipleogni clic aggiunge o toglie la riga) o con Spazio/Invio sulla riga con il focus;rowSelectable(funzione della riga, comeisRowSelectabledi ag-grid) indica le righe selezionabili: le altre hanno la casella disabilitata (nascosta conhideUnselectable), sono escluse da "seleziona tutte" e dalla selezione con clic o tastiera;(rowClick)per il dettaglio. Con la selezione singola l'intestazione della colonna di selezione è letta dagli screen reader comeselectionHeaderLabel("Selezione"). I campi dei filtri non si registrano nel form che contiene la griglia.Altezza: senza impostazioni la griglia è alta quanto il contenuto (tutte le righe);
height(comestyle.height) la fissa,minHeightemaxHeightla limitano. Con un'altezza impostata le righe scorrono dentro la griglia con l'intestazione sempre visibile, mentre "Raggruppa per", chip dei filtri e paginazione restano fuori dallo scroll e contano nell'altezza. ConstickyHeadere senza altezza vale l'altezza del kit (450px per le righe).Aspetto del kit:
variant(default,dark,striped),size(xs,sm,md,lg), contenitore responsive con gli indicatori di scroll,stickyHeader,fixedFirstColumn,fixedLastColumn,cardOnMobile(righe a card fino a 768px, con l'intestazione come etichetta di ogni cella).Accessibilità:
caption(nascosta di default; dà anche il nome al paginatore, "Paginazione della tabella ", per distinguere più griglie nella stessa pagina),aria-sorte tastiera (Invio/Spazio) sulle intestazioni ordinabili,aria-selected,aria-expandedsui pulsanti di espansione,aria-levelper alberi e gruppi,aria-busydurante il caricamento; numero di risultati, criteri di ordinamento e spostamenti di colonna annunciati.Navigazione da tastiera (
keyboardNavigation, defaulttrue, come ag-grid e il pattern ARIA "grid"): la griglia è una sola tappa del Tab (si entra sulla prima intestazione, poi sull'ultima cella visitata); le frecce si spostano tra intestazioni e celle, Home/Fine all'inizio e alla fine della riga, Ctrl+Home/Ctrl+Fine alla prima e all'ultima cella; Tab esce dalla griglia. Una cella con pulsanti o campi (es. le azioni, un template di cella) passa il focus ai suoi controlli, uno per tappa. La cella raggiunta viene portata in vista fuori dalle colonne fisse e dall'intestazione fissa; Spazio/Invio su una cella seleziona la riga quando non c'è la colonna di selezione. I campi della riga dei filtri restano tappe del Tab (usano le frecce); con la riga dei filtri la griglia ha due tappe, intestazioni e righe: Tab va dall'intestazione ai filtri e dai filtri alle righe, poi esce (Shift+Tab al contrario). Sull'intestazione, come ag-grid: Invio/Spazio ordina (Shift+Invio ordinamento multiplo), Alt+frecce cambia la larghezza, Shift+frecce sposta la colonna, Ctrl+Invio apre il filtro; Alt+frecce non arriva mai al browser (Alt+← è "Indietro" e farebbe lasciare la pagina). La tabella harole="grid"(treegridcon alberi e raggruppamenti, conaria-levelsulle righe) earia-multiselectablecon la selezione multipla. Con[keyboardNavigation]="false"è una tabella semplice: tappe del Tab solo le intestazioni interattive e i controlli.Pannelli dei filtri: il calendario dei filtri data e l'elenco dei filtri a scelta si aprono sopra la pagina, sotto il campo (sopra quando sotto non c'è spazio), senza essere tagliati dal contenitore scorrevole della griglia. Vale per
wm-sirio-dateewm-sirio-selectdentro qualsiasi contenitore che ritaglia il contenuto.
Browser supportati
Gli stessi di Angular 22: Chrome ed Edge 119+, Firefox 119+, Safari 17+ (macOS e iOS), ultime versioni di Chrome e Firefox per Android. Internet Explorer ed Edge Legacy non sono supportati.
