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: runnpm run site:sync-rulesto 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 theIAppFormcontract.
Base classes¶
AppForm<T>(from@sumaris-net/ngx-components): base class for all forms. It wraps an AngularUntypedFormGroupand exposes the reactive lifecycle. Do not build theFormGroupinside the component — receive it from a validator service and pass it tosuper();MeasurementValuesForm<T, S>(from@app/data/measurement/measurement-values.form.class): extendsAppForm, adds dynamic PMFM-based measurement controls (acquisitionLevel,programLabel,pmfms$,strategyLabel). Use it when the entity carriesmeasurementValuesdriven by a program/strategy;AppFormitself extendsAppFormContainerfor 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 theinject()function on attributes); - Inject the entity's validator service and pass its
FormGrouptosuper():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
MyComponentStateinterface 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
ngOnInitviathis._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());
- Call
- Use
await this.ready()to wait until the form is initialized before reading/writing values; setValue(data)/getValue()synchronize the entity with theFormGroup— override them only when extra mapping is required.
Relationship with the validator service¶
- The component owns no form-building logic; the
.validator.tsservice is the single source of truth for controls and validators; - The validator's
getFormGroup(data?, opts?)builds theUntypedFormGroup; the component injects the service and passes the result tosuper(); - 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: thetranslate/translateContextpipes in templates; - Prefix keys with the functional domain (e.g.
TRIP.SAMPLE.EDIT.); add keys to bothfr.jsonanden.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).