Skip to content

Component FormShell

simone.tiberti edited this page Apr 21, 2026 · 7 revisions

FormShell (core-form-shell)

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.


Utilizzo base

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 — costruzione

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.

Accesso ai valori

// 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()}`
);

Edit mode (valori iniziali)

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 / Output della shell

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()submitted emette void.


Validazione

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).


FormFieldUIDef — proprietà

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 del FormModel.


Hidden e Disabled — via schema

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 },
  ],
)

Grid responsive

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.


Field component built-in

TextInputFieldComponent

{ 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, ...)

TextareaFieldComponent

{ key: 'note', label: 'Note', component: TextareaFieldComponent, span: 2,
  inputs: { rows: 4 } }
Input extra Default
rows 3 Numero righe visibili

ComboboxFieldComponent

{ 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 }

DatepickerFieldComponent

{ key: 'nascita', label: 'Data di nascita', component: DatepickerFieldComponent }

Nessun input extra. Il valore nel modello è un oggetto Date.


LookupFieldComponent

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')

Campo custom

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 il FieldTree
  • formField()() — chiama il FieldTree → restituisce FieldState con .value, .errors(), .touched(), .invalid()

Vedi anche

  • DataTable — lista con paginazione e sort server-side
  • Confirm — conferma prima di azioni distruttive
  • Toast — notifiche dopo il salvataggio

Clone this wiki locally