question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItΒ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

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:closed
  • Created 4 years ago
  • Reactions:1
  • Comments:12 (7 by maintainers)

github_iconTop GitHub Comments

2reactions
norganoscommented, Aug 7, 2019

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:

  • instead of trying to hook into the existing reactive form methods, I now snuggle with it πŸ˜‰
  • I wrote a interface similar to CVA, that basically acts as the receiving part of my extended formControl metadata (I called it ControlMetaAccessor [=CMA in the following] and it offers methods like setConstraints which accepts a struct containing all kind of constraints. I also use that struct in the extended formControl to automatically create validators out of it)
  • and a injectorToken for CMA just like NG_VALUE_ACCESSOR is for CVA
  • Then I created 2 directives that use the same selectors formControl and formControlName. these directives get a hold of a CMA (out of the injector, just like CVAs) and they get the bound formControl (there I had to copy around 10 lines of the reactive forms code: formControlName directive has to get the control by path from the closest formGroup)
  • if the directives find a CMA and the formControl is a extended formControl then they set up the connection.
  • on top I wrote some default CMA directives (just like angular does with the builtin CVAs) to enable CMA functionality for html inputs and some library components

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?

1reaction
norganoscommented, Aug 6, 2019

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

Read more comments on GitHub >

github_iconTop Results From Across the Web

A proposal to improve Angular's ReactiveFormsModule
Ever been frustrated with limitation's in Angular's ReactiveFormsModule? Today we discuss a new proposal that aims to fix almost everything.
Read more >
Angular Reactive Forms Basics Guide - Fireship
Master the basics of Reactive Forms in Angular by building five different forms from scratch.
Read more >
Extension method on FormGroup - angular - Stack Overflow
I am using reactive forms and trying to createΒ ...
Read more >
Configurable Reactive Forms in Angular with dynamic ...
Another powerful use case is having a form driven by configuration. This allows us to develop a generic form component, with the child...
Read more >
Creating elegant reactive forms with RxWebValidators
The most excellent benefit of the Angular Reactive Form approach removes the core validation logic from the template. It allows us to focus...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found