Writing a validator¶
Copy of
sumaris-app/.agents/rules/validator.md(rule written for AI coding agents, when creating or modifying form validator services). Do not edit here: runnpm run site:sync-rulesto update it.
- Use the file suffix
.validator.ts; - Use
@Injectable({ providedIn: 'root' })for application-wide validators, or@Injectable()if provided in a specific module or component; - Use
UntypedFormBuilder(notFormBuilder) for Angular 16+ compatibility; - Use
UntypedFormGroupandUntypedFormControl(notFormGroup/FormControl) for untyped form handling; - Inject
LocalSettingsService(from@sumaris-net/ngx-components, notSettingsService) for app settings;
Class hierarchy¶
AppValidatorService<T>(from@sumaris-net/ngx-components): base class for all validators, implementsValidatorService(from@e-is/ngx-material-table);- Provides
getRowValidator(),getFormGroup(),getFormGroupConfig(),getI18nFormErrors(),getI18nErrors();
- Provides
BaseValidatorService<E, ID, O>(from@app/shared/service/base.validator.service): extendsAppValidatorService, adds typed optionsOandgetFormGroupOptions(),updateFormGroup();DataEntityValidatorService<T, O>(from@app/data/services/validator/data-entity.validator): extendsBaseValidatorService, adds data entity fields (id,updateDate,recorderDepartment, quality properties) andfillDefaultOptions();DataRootEntityValidatorService<T, O>: extendsDataEntityValidatorService, adds root entity fields (program,creationDate,validationDate,recorderPerson,comments,synchronizationStatus) and observers support;DataRootVesselEntityValidatorService<T, O>: extendsDataRootEntityValidatorService, adds vessel-related fields;
Options interface¶
- Define a typed options interface extending the parent's options:
export interface MyEntityValidatorOptions extends DataRootEntityValidatorOptions { withSomeFeature?: boolean; withMeasurements?: boolean; // ... other boolean flags controlling optional form parts } - Options control which parts of the form are built (e.g.
withObservers,withSale,withMeasurements);
Constructor¶
- Inject
UntypedFormBuilder,TranslateService,LocalSettingsService, and any child validator services needed:constructor( formBuilder: UntypedFormBuilder, translate: TranslateService, settings: LocalSettingsService, protected childValidator: ChildValidatorService, protected measurementsValidatorService: MeasurementsValidatorService ) { super(formBuilder, translate, settings); }
Key methods to implement¶
getFormGroup(data?, opts?)¶
- Call
this.fillDefaultOptions(opts)first; - Call
super.getFormGroup(data, opts)to build the base form; - Add optional sub-forms conditionally based on options:
getFormGroup(data?: MyEntity, opts?: O): UntypedFormGroup { opts = this.fillDefaultOptions(opts); const form = super.getFormGroup(data, opts); if (opts.withChild) { form.addControl('child', this.childValidator.getFormGroup(data?.child, { ... })); } if (opts.withMeasurements) { form.addControl('measurements', this.measurementsValidatorService.getFormGroup(data?.measurements, { pmfms, ... })); } return form; } - IMPORTANT: Use
form.addControl()for conditional sub-forms ingetFormGroup(), NOT ingetFormGroupConfig(); getFormGroupConfig()only defines the base controls; conditional parts go ingetFormGroup();
getFormGroupConfig(data?, opts?)¶
- Return an object merging
super.getFormGroupConfig(data, opts)with entity-specific controls:getFormGroupConfig(data?: MyEntity, opts?: O): { [key: string]: any } { return { ...super.getFormGroupConfig(data, opts), __typename: [MyEntity.TYPENAME], myField: [data?.myField || null, Validators.required], myEntity: [data?.myEntity || null, Validators.compose([Validators.required, SharedValidators.entity])], }; } - Use
SharedValidators.entityfor referential/entity fields; - Use
Validators.compose([...])to combine multiple validators;
getFormGroupOptions(data?, opts?)¶
- Return cross-field validators (e.g. date range validation):
getFormGroupOptions(data?: MyEntity, opts?: O): AbstractControlOptions { return { validator: Validators.compose([ SharedFormGroupValidators.dateRange('startDate', 'endDate'), SharedFormGroupValidators.dateMinDuration('startDate', 'endDate', minHours, 'hour'), SharedFormGroupValidators.dateMaxDuration('startDate', 'endDate', maxHours, 'hour'), ]), }; }
updateFormGroup(form, opts?)¶
- Dynamically add/remove controls and update validators when options change at runtime:
updateFormGroup(form: UntypedFormGroup, opts?: O): UntypedFormGroup { opts = this.fillDefaultOptions(opts); const enabled = form.enabled; // Add or remove optional controls if (opts.withChild) { if (!form.controls.child) { form.addControl('child', this.childValidator.getFormGroup(null, { ... })); } if (enabled) form.controls.child.enable(); else form.controls.child.disable(); } else { if (form.controls.child) form.removeControl('child'); } // Update cross-field validators const formValidators = this.getFormGroupOptions(null, opts)?.validators; form.setValidators(formValidators); return form; } - Preserve enabled/disabled state when adding controls;
fillDefaultOptions(opts?)¶
- Override to set default values from program properties or settings:
protected fillDefaultOptions(opts?: O): O { opts = super.fillDefaultOptions(opts); opts.withChild = opts.withChild ?? opts.program?.getPropertyAsBoolean(ProgramProperties.MY_CHILD_ENABLE); opts.withMeasurements = opts.withMeasurements ?? !!opts.program; return opts; }
FormArray support¶
- Use
AppFormArrayfor dynamic lists (observers, metiers, fishing areas):getMyItemsArray(data?: MyItem[], opts?: { required?: boolean }) { const required = !opts || opts.required !== false; const formArray = new AppFormArray<MyItem, UntypedFormControl>( (item) => this.getMyItemControl(item, { required }), MyItemUtils.equals, MyItemUtils.isEmpty, { allowEmptyArray: false, validators: required ? SharedFormArrayValidators.requiredArrayMinLength(1) : null, } ); if (data || required) { formArray.patchValue(data || [null]); } return formArray; } getMyItemControl(value?: any, opts?: { required?: boolean }): UntypedFormControl { const required = !opts || opts.required !== false; return this.formBuilder.control(value || null, required ? [Validators.required, SharedValidators.entity] : SharedValidators.entity); }
Shared validators¶
SharedValidators.entity: validates that a value is a valid entity (has anid);SharedValidators.dateRangeEnd(startField): validates that a date is after the start field;SharedValidators.copyParentErrors(errorKeys): copies specified errors from the parent form group;SharedFormGroupValidators.dateRange(startField, endField): cross-field date range validator;SharedFormGroupValidators.dateMinDuration(start, end, value, unit): minimum duration between two dates;SharedFormGroupValidators.dateMaxDuration(start, end, value, unit): maximum duration between two dates;SharedFormGroupValidators.requiredIfFalse(targetField, conditionField): makes a field required if another is false;SharedFormArrayValidators.requiredArrayMinLength(min): validates minimum array length;
Reference examples¶
- Root entity validator:
src/app/trip/trip/trip.validator.ts(TripValidatorService); - Data entity base validator:
src/app/data/services/validator/data-entity.validator.ts; - Root data entity validator:
src/app/data/services/validator/root-data-entity.validator.ts; - Base validator (lib):
ngx-sumaris-components/src/app/core/services/validator/base.validator.class.ts;