Proposal to allow extension of reactive forms
See original GitHub issueπ feature request
Relevant Package
This feature request is for @angular/forms
Description
I want to minimize repetition of similar code related to forms and re-use the reactive form connection (formControl bound to input component via reactive forms directives) for other states than value/disabled. Developers already are able to implement custom ControlValueAccessors, so one approach would be to override FormControl class and provide necessary properties there. The problem is, that a custom ControlValueAccessor does not get the bound FormControl directly. Existing implementation only connects value and disabled state.
For Example:
- a custom SelectControlValueAccessor could automatically generate option tags inside select tag if bound formControl has a βelementsβ property.
- FormControls for numeric values with a informational property describing the measurement unit -> a custom NumberControlValueAccessor could automatically place a postfix tag or a custom component could use this unit.
- a custom component showing errors of formControl automatically.
- custom FormControl objects that not only have a βdisabledβ state but also a reason why itβs disabled => custom components/controlValueAccessors could display these reason automatically for better user experience Having additional data besides value and disabled on the FormControl objects has the advantage that it integrates nicely when such data comes from the same backend as the values themselves.
Describe the solution youβd like
The simplest approach is to provide a hook mechanism to get some custom code be called whenever setUpControl and cleanUpControl from forms/shared.ts are invoked. This approach is implemented in norganos/angular:reactive_forms_hooks
Example usage
The following code example shows how these hooks could be used to transport a unit postfix.
Custom Input Component
For simplicity, we create a component that wraps a number input and the unit
@Component({
selector: 'app-measurement',
template: `<input type="number" name="custom" [(ngModel)]="model" (ngModelChange)="changeFn($event)" [disabled]="isDisabled"><span>{{unit }}</span>`,
providers: [{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: MeasurementInput}]
})
class MeasurementInput implements ControlValueAccessor {
model = 0;
@Input('disabled') isDisabled: boolean = false;
changeFn: (value: any) => void;
unit = '';
writeValue(value: any) { this.model = value; }
registerOnChange(fn: (value: any) => void) { this.changeFn = fn; }
registerOnTouched() {}
setDisabledState(isDisabled: boolean) { this.isDisabled = isDisabled; }
setUnit(unit: string) { this.unit = unit};
}
Custom FormControl
Of course we need a place to store the unit. For simplicity, we just define a single βunitβ property here.
class MeasurementFormControl extends FormControl {
private _onUnitChange: Function[] = [];
private _unit = '';
get unit(): string { return this._unit; }
set unit(value: string) {
this._unit = value;
for (let f of this._onUnitChange) { f(value); }
}
constructor(formState: any, unit: string, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null) {
super(formState, validatorOrOpts, asyncValidator);
this.unit = unit;
}
registerOnUnitChange(fn: (unit: string) => void): void {
this._onUnitChange.push(fn);
}
clearUnitChangeFunctions(): void {
this._onUnitChange = [];
}
}
Hooks
This example hard codes MeasurementInput and MeasurementFormControl. a real world implementation of course would use interfaces and type guards.
@Injectable()
class MeasurementFormsHook implements FormsHook {
setUpControl(control: FormControl, dir: NgControl): void {
const accessor = dir.valueAccessor;
if (accessor != null && (<MeasurementInput>accessor).setUnit !== undefined&& (<MeasurementFormControl>control).registerOnUnitChange !== undefined) {
control.registerOnUnitChange(unit=> accessor.setUnit(unit));
accessor.setUnit(control.unit);
}
},
cleanUpControl(control: FormControl, dir: NgControl): void {
if ((<MeasurementFormControl>control).clearUnitChangeFunctions !== undefined) {
control.clearUnitChangeFunctions();
}
}
}
Register Hooks
@NgModule({
imports: [
ReactiveFormsModule
],
providers: [
{provide: NG_FORMS_HOOK, useClass: MeasurementFormsHook}
]
})
class AppModule {}
Usage
A component then can use the custom components and controls
@Component({
template: `
<form [formGroup]="form">
<app-measurement formControlName="width"></app-measurement>
<app-measurement formControlName="height"></app-measurement>
<app-measurement formControlName="weight"></app-measurement>
</form>`
})
class FormGroupNameComp implements OnInit {
form: FormGroup;
ngOnInit() {
this.form = new FormGroup({
width: new MeasurementFormControl(12, 'm'),
height: new MeasurementFormControl(50, 'cm'),
weight: new MeasurementFormControl(23, 'kg'),
})
}
}
Describe alternatives youβve considered
Issue #31963 could also address these possibilities, but it looks like itβs a bigger thing.
The proposal from issue #19686 (a general purpose meta-data container on AbstractControl) should integrate nicely with this approach here.
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:12 (7 by maintainers)
Top GitHub Comments
So, thanks to @thefliik and @Airblader, I came up with a different approach for my requirement, which I wanted to share in case anyone has similar requirements:
This works like a charm and it feels much cleaner than the proposed hook solution. So, very thanks for the discussion! Should I close this issue then?
@thefliik Iβve seen your proposal yesterday morning and was surprised by the coincidence. Yet I didnβt want to throw mine into the thrash π I think a ReactiveFormsModule2 would indeed solve most of my problems π
As you said, a refactoring of FormControlDirective with the shared code being overridable would make it way easier for this as well. Yet it probably would be necessary to expose almost all of the classes in forms module (e.g. the builtin CVAs)
Itβs totally correct, that my hooks proposal is more a consequence of code being too closed in the first place. I just thought, a minimal invasive change would probably be better.