Aller au contenu

Writing a form

Copy of sumaris-app/.agents/rules/form.md (rule written for AI coding agents, when creating or modifying form components (.form.ts, AppForm)). Do not edit here: run npm run site:sync-rules to update it.

A form component is a reusable reactive form bound to an Angular FormGroup. It manages a single entity (or a fragment of one), but does not load/save it — that is the job of the parent editor.

  • Use the file suffix .form.<extension> (see common rules);
  • Use changeDetection: ChangeDetectionStrategy.OnPush;
  • A form component extends AppForm<T> (or a subclass) and implements the IAppForm contract.

Base classes

  • AppForm<T> (from @sumaris-net/ngx-components): base class for all forms. It wraps an Angular UntypedFormGroup and exposes the reactive lifecycle. Do not build the FormGroup inside the component — receive it from a validator service and pass it to super();
  • MeasurementValuesForm<T, S> (from @app/data/measurement/measurement-values.form.class): extends AppForm, adds dynamic PMFM-based measurement controls (acquisitionLevel, programLabel, pmfms$, strategyLabel). Use it when the entity carries measurementValues driven by a program/strategy;
  • AppForm itself extends AppFormContainer for forms that aggregate several child forms.

IAppForm contract

The interface exposed to the parent editor (used by addForms() for dirty/validation tracking):

  • State accessors: valid, invalid, dirty, pending, empty, touched, untouched, enabled, disabled, loading, loaded, destroyed, error;
  • State mutators: enable(opts?), disable(opts?), markAsPristine(opts?), markAsDirty(opts?), markAllAsTouched(opts?), markAsReady(opts?), markAsLoading(), markAsLoaded();
  • Value methods: setValue(data, opts?), patchValue(data, opts?), getValue(), reset(data, opts?);
  • Async waiters: ready(opts?): Promise<void>, waitIdle(opts?): Promise<void>.

Constructor

  • Limit constructor parameters to ease inheritance: inject an Injector (and/or use the inject() function on attributes);
  • Inject the entity's validator service and pass its FormGroup to super():
    constructor(
      injector: Injector,
      protected validatorService: MyEntityValidatorService,
      protected formBuilder: UntypedFormBuilder
    ) {
      super(injector, validatorService.getFormGroup());
    }
    
  • For a MeasurementValuesForm, also pass the measurements validator and program service:
    constructor(
      injector: Injector,
      protected measurementsValidatorService: MeasurementsValidatorService,
      protected formBuilder: UntypedFormBuilder,
      protected programRefService: ProgramRefService,
      protected validatorService: SampleValidatorService
    ) {
      super(injector, measurementsValidatorService, formBuilder, programRefService, validatorService.getFormGroup());
      this.acquisitionLevel = AcquisitionLevelCodes.SAMPLE;
    }
    

RxState

  • Declare providers: [RxState] in the @Component;
  • Declare a MyComponentState interface above the class for dynamic attributes;
  • Use @RxStateProperty() for @Input-able attributes and @RxStateSelect() xxx$: Observable<...> for observed ones (see component rules);
  • Connect external data sources in the constructor or ngOnInit via this._state.connect('key', source$).

Lifecycle

  • In ngOnInit():
    • Call super.ngOnInit() first;
    • Register autocomplete fields with this.registerAutocompleteField(...) (see autocomplete rules);
    • Register subscriptions with this.registerSubscription(...);
    • Enable/disable specific controls as needed (e.g. this.form.get('gear').disable());
  • Use await this.ready() to wait until the form is initialized before reading/writing values;
  • setValue(data) / getValue() synchronize the entity with the FormGroup — override them only when extra mapping is required.

Relationship with the validator service

  • The component owns no form-building logic; the .validator.ts service is the single source of truth for controls and validators;
  • The validator's getFormGroup(data?, opts?) builds the UntypedFormGroup; the component injects the service and passes the result to super();
  • To rebuild the form when options change at runtime, call the validator's updateFormGroup(this.form, opts).

HTML template — form-field components

Use the project's custom mat-*-field components instead of raw inputs. Each binds with formControlName="..." (or [formControl]="..."), a [placeholder], and optionally [required], [tabindex], prefix/suffix icons:

Component Usage
<mat-form-field> + matInput Standard Angular Material text/number input
<mat-autocomplete-field> Autocomplete bound to [config]="autocompleteFields.xxx" (service, items, or suggestFn)
<mat-date-field> Date picker (date only)
<mat-date-time-field> Combined date + time picker
<mat-date-short-field> Date picker with short format
<mat-time-field> Time-only input
<mat-duration-field> Duration / time span input
<mat-boolean-field> Boolean toggle / radio (true/false)
<mat-latlong-field> Latitude / longitude coordinates
<mat-chips-field> Multi-select tags / chips
<mat-sampling-ratio-field> Sampling ratio input

Example:

<mat-autocomplete-field
  formControlName="taxonName"
  [config]="autocompleteFields.taxonName"
  [placeholder]="'TRIP.SAMPLE.EDIT.TAXON_NAME' | translateContext: i18nSuffix"
  [required]="true"
  [tabindex]="tabindex">
  <ion-icon matPrefix name="fish"></ion-icon>
</mat-autocomplete-field>

i18n

  • Use ngx-translate: the translate / translateContext pipes in templates;
  • Prefix keys with the functional domain (e.g. TRIP.SAMPLE.EDIT.); add keys to both fr.json and en.json (see translations rules).

Reference examples

  • Simple form (extends AppForm): src/app/trip/trip/trip.form.ts;
  • Measurements form (extends MeasurementValuesForm): src/app/trip/sample/sample.form.ts, src/app/trip/physicalgear/physical-gear.form.ts, src/app/trip/landing/landing.form.ts;
  • Validator providing the FormGroup: src/app/trip/sample/sample.validator.ts (see validator rules).