-
Notifications
You must be signed in to change notification settings - Fork 0
Component FormShell
Sistema dichiarativo per form basato su Angular Signal Forms (@angular/forms/signals). Si dichiara il modello, i validatori e il layout UI in un unico FormModel — la shell gestisce la griglia responsive, la validazione e il submit.
import {
FormShellComponent,
FormModel,
TextInputFieldComponent,
ComboboxFieldComponent,
DatepickerFieldComponent,
} from '@gpa-gruppo-progetti-avanzati-srl/ng-core-ui';
import { required } from '@angular/forms/signals';
@Component({
standalone: true,
imports: [FormShellComponent],
...
})
export class PersonaEditComponent {
protected readonly formModel = new FormModel(
{ nome: '', cognome: '', categoria: null as string | null },
p => {
required(p.nome, { message: 'Nome obbligatorio' });
required(p.cognome, { message: 'Cognome obbligatorio' });
},
[
{ key: 'nome', label: 'Nome', component: TextInputFieldComponent },
{ key: 'cognome', label: 'Cognome', component: TextInputFieldComponent },
{ key: 'categoria', label: 'Categoria', component: ComboboxFieldComponent,
inputs: { options: [{ value: 'A', label: 'Tipo A' }, { value: 'B', label: 'Tipo B' }] } },
],
);
onSubmit(): void {
const values = this.formModel.model(); // { nome: 'Mario', cognome: 'Rossi', categoria: 'A' }
console.log(values.nome);
}
}<core-form-shell
[model]="formModel"
[columns]="2"
(submitted)="onSubmit()"
(cancelled)="goBack()"
/>FormModel<T> è una classe che racchiude la signal form Angular e il layout UI.
new FormModel<T>(
initialValue: T, // oggetto con i valori iniziali
validators, // funzione schema (required, min, max, ...)
layout: FormFieldUIDef[], // definizione campi UI
)Il terzo argomento FormFieldUIDef[] specifica come visualizzare ogni campo della signal form — quale componente usare, il label, lo span nella griglia, eventuali input aggiuntivi.
// Intero oggetto (sul submit o in qualsiasi momento)
const values = this.formModel.model(); // T — tutti i valori correnti
// Un campo reattivo in computed()
readonly preview = computed(() =>
`Ciao ${this.formModel.ft.nome().value()}`
);Passa l'oggetto esistente come initialValue:
protected readonly formModel = new FormModel(
this.persona, // { nome: 'Mario', cognome: 'Rossi', ... }
p => { required(p.nome); required(p.cognome); },
[ ... ],
);| Input | Tipo | Default | Descrizione |
|---|---|---|---|
model |
FormModel |
— | Il form model (obbligatorio) |
columns |
number |
2 |
Colonne della griglia |
submitLabel |
string |
'Salva' |
Testo bottone submit |
cancelLabel |
string |
'Annulla' |
Testo bottone annulla |
showCancel |
boolean |
true |
Mostra il bottone annulla |
| Output | Tipo | Descrizione |
|---|---|---|
submitted |
void |
Emesso al submit se il form è valido |
cancelled |
void |
Emesso al click su Annulla |
I valori si leggono da
formModel.model()—submittedemettevoid.
I validatori si dichiarano nello schema (secondo argomento di FormModel):
import { required, min, max, minLength, email } from '@angular/forms/signals';
new FormModel(
{ nome: '', eta: 0, email: '', codice: '' },
p => {
required(p.nome, { message: 'Nome obbligatorio' });
min(p.eta, 18, { message: 'Età minima 18 anni' });
max(p.eta, 120, { message: 'Età massima 120 anni' });
email(p.email, { message: 'Email non valida' });
minLength(p.codice, 3, { message: 'Almeno 3 caratteri' });
},
[ ... ],
)Gli errori vengono mostrati nei campi solo dopo il primo tentativo di submit (comportamento touched).
| Proprietà | Tipo | Descrizione |
|---|---|---|
key |
string |
Chiave del campo — deve corrispondere a una proprietà del modello |
label |
string |
Etichetta visibile |
component |
Type<CoreFieldComponent> |
Componente da renderizzare |
span |
number? |
Colonne occupate nella griglia (default: 1, max: columns) |
inputs |
Record<string, any>? |
Input aggiuntivi passati al componente (es. options) |
FormFieldUIDefè solo layout UI. Validazione, visibilità e disabilitazione si dichiarano nello schema delFormModel.
Visibilità e disabilitazione si dichiarano nello schema del FormModel con hidden() e disabled() di @angular/forms/signals. La shell legge FieldState.hidden() e FieldState.disabled() direttamente — nessuna callback nel layout.
import { required, hidden, disabled } from '@angular/forms/signals';
new FormModel(
{ tipo: null as string | null, piva: '', cf: '', note: '' },
p => {
required(p.tipo, { message: 'Tipo obbligatorio' });
hidden(p.piva, () => p.tipo().value() !== 'azienda');
hidden(p.cf, () => p.tipo().value() !== 'persona');
disabled(p.note, () => !p.tipo().value());
},
[
{ key: 'tipo', label: 'Tipo', component: ComboboxFieldComponent,
inputs: { options: [{ value: 'azienda', label: 'Azienda' }, { value: 'persona', label: 'Persona' }] } },
{ key: 'piva', label: 'P.IVA', component: TextInputFieldComponent },
{ key: 'cf', label: 'C.F.', component: TextInputFieldComponent },
{ key: 'note', label: 'Note', component: TextareaFieldComponent, span: 2 },
],
)columns |
Uso tipico |
|---|---|
1 |
Mobile, form semplici |
2 |
Default, form standard |
3 |
Form articolati, schermi larghi |
4 |
Dashboard, form molto ricchi |
Ogni campo occupa span colonne (default 1). Se span > columns viene cappato automaticamente.
{ key: 'email', label: 'Email', component: TextInputFieldComponent,
inputs: { type: 'email' } }
{ key: 'eta', label: 'Età', component: TextInputFieldComponent,
inputs: { type: 'number' } }| Input extra | Default | |
|---|---|---|
type |
'text' |
Tipo HTML dell'input (text, email, number, password, ...) |
{ key: 'note', label: 'Note', component: TextareaFieldComponent, span: 2,
inputs: { rows: 4 } }| Input extra | Default | |
|---|---|---|
rows |
3 |
Numero righe visibili |
{ key: 'stato', label: 'Stato', component: ComboboxFieldComponent,
inputs: {
options: [
{ value: 'attivo', label: 'Attivo' },
{ value: 'sospeso', label: 'Sospeso' },
]
}
}| Input extra | Tipo | |
|---|---|---|
options |
ComboboxOption[] |
Array di { value, label }
|
{ key: 'nascita', label: 'Data di nascita', component: DatepickerFieldComponent }Nessun input extra. Il valore nel modello è un oggetto Date.
Apre una dialog custom implementata nell'app e riceve un { id, label }.
import { LookupFieldComponent, LookupResult } from '@gpa-gruppo-progetti-avanzati-srl/ng-core-ui';
{ key: 'persona', label: 'Persona', component: LookupFieldComponent,
inputs: {
dialogConfig: {
component: PersonaPickerDialog,
width: '800px',
data: { tipo: 'fisica' },
}
},
}La validazione si dichiara nello schema:
required(p.persona, { message: 'Selezionare una persona' });Il valore è LookupResult | null:
onSubmit(): void {
const persona = this.formModel.model().persona as LookupResult;
console.log(persona.id, persona.label);
}Contratto del dialog:
@Component({ ... })
export class PersonaPickerDialog {
private ref = inject(MatDialogRef<PersonaPickerDialog>);
readonly data = inject(MAT_DIALOG_DATA);
select(p: Persona): void {
this.ref.close({ id: p.id, label: `${p.nome} ${p.cognome}` });
}
cancel(): void {
this.ref.close(null);
}
}| Input extra | Tipo | |
|---|---|---|
dialogConfig.component |
Type<any> |
Dialog da aprire (obbligatorio) |
dialogConfig.data |
Record<string, any>? |
Dati passati via MAT_DIALOG_DATA
|
dialogConfig.width |
string? |
Larghezza dialog (default: '600px') |
dialogConfig.maxWidth |
string? |
Larghezza massima (default: '95vw') |
Qualsiasi componente che implementa il contratto CoreFieldComponent può essere usato:
@Component({
standalone: true,
template: `
<label>{{ label() }}</label>
<input
type="range" min="0" max="100"
[formField]="formField()"
/>
@if (formField()().touched() && formField()().errors().length) {
<p class="text-red-500 text-xs">{{ formField()().errors()[0]?.message }}</p>
}
`
})
export class SliderFieldComponent {
readonly formField = input.required<any>();
readonly label = input<string>('');
}Il pattern formField()():
-
formField()— chiama l'InputSignal → restituisce ilFieldTree -
formField()()— chiama ilFieldTree→ restituisceFieldStatecon.value,.errors(),.touched(),.invalid()
Sviluppato e mantenuto da GPA — Gruppo Progetti Avanzati s.r.l. · gpa@gpagroup.it · +39 065913642 · P.IVA 05049391005 · ISO 9001 · ISO 27001
Sede legale: Via Ferdinando Galiani, 68 — 00191 Roma | Sedi operative: Piazzale Asia 21, Roma · Via Badoero 67, Roma · Viale Chiavellati 9, Foligno (PG)
npm · GitHub · Segnala un problema · Licenza MIT