How to intercept the value of FormControl before setting or getting it?

Viewed 5567

The question is pretty self-explanatory. I would like to intercept the incoming value for a FormControl's value property, and be able to intercept the outgoing value to the HTML control that it's hooked up to.

Let's say I have a FormControl named "firstName" and I hook it up to a textbox as such:

<input type="text" formControlName="firstName" />

By default, when the user inputs the value in the textbox and submits, the FormControl's value gets set to the value in the textbox. Is there any way I can intercept the value that gets set and modify it before setting it?

Similarly, is there any way to intercept the value that the FormControl sends to the HTML control? For example if I have a FormControl's value set to something but I want to modify the value that shows up in the textbox.

I know I can use ngModel to act as a mediator between the form and the control, but that gets cumbersome when using more than a few controls. I also know you can create your own control and implement the ControlValueAccessor, but this is also cumbersome as I would have to create a corresponding control for each control I want to use.

For more information about why I'm asking this question, see https://github.com/ionic-team/ionic/issues/7121

4 Answers

You can write a reusable directive which intercepts the value coming from and going into the view:

const MODIFIER_CONTROL_VALUE_ACCESSOR: Provider = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => ValueModifierDirective),
  multi: true,
};

@Directive({
  selector: '[valueModifier]',
  host: { '(keyup)': 'doSomething($event)' },
  providers: [MODIFIER_CONTROL_VALUE_ACCESSOR],
})
export class ValueModifierDirective implements ControlValueAccessor {

  @Input() valueModifier: [Function, Function];

  private writeToForm;

  constructor(public _el: ElementRef) { }

  doSomething(event: Event) {
    const viewToForm = this.valueModifier[0];
    this.writeToForm(viewToForm(event.target.value));
  }

  registerOnChange(fn: (value: any) => void) {
    this.writeToForm = fn;
  }

  registerOnTouched(fn: any) {
    // nothing to do
  }

  writeValue(value: any) {
    const formToView = this.valueModifier[1];
    this._el.nativeElement.value = formToView(value);
  }
}

To use it, just add the directive to the same element you apply formControlName on and pass the transform-functions:

@Component({
  selector: 'my-app',
  template: `
  <form [formGroup]="form">
  <input [valueModifier]="[viewToForm, formToView]" name="value" type="text" formControlName="input"  />
  <button (click)="random()">Set Random Value</button>
  </form>
  `,
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  form = new FormGroup({
    input: new FormControl(1)
  });
  viewToForm = (text: string) => "toForm" + text;
  formToView = (text: string) => "toView" + text;

  constructor() {
    this.form.valueChanges.subscribe(value => console.log(value));
  }

  random () {
    this.form.patchValue({
      input: Math.random()
    })
  }
}

Live example (Stackblitz):

https://stackblitz.com/edit/angular-afmkxl?file=src%2Fapp%2Fapp.component.ts

The above works for text inputs. I think you can write similar directives for other kinds of inputs.

You can create a directive, inject the formControl in the directive, and then use emitModelToViewChange and emitViewToModelChange options of setValue.

Like this:

@Directive({
  selector: '[valueModifier]',
})
export class ValueModifierDirective {
  constructor(private control: NgControl) {}

  private setValueOfModel() {
    this.control.control!.setValue('Custom Value', { emitModelToViewChange: false });
  }
}

This solution works for any kind of form input (input, select, checkbox, etc.).

It seems that Validators have the highest priority in handling events in angular form api(if you actually debug into angular source code you can see that); so based on your need you can use a custom validator to get user value actually before form control value changes.

@Directive({
    selector:'[InterceptorDirective]',
    providers: [
    {provide: NG_VALIDATORS, useExisting: forwardRef(() => InterceptorDirective), multi: true}
    ]
}) export class InterceptorDirective implements Validator {
    validate(control: AbstractControl): ValidationErrors | null {
        let value = control.value
        // intercepting
        control.setValue(value, {emitModelToViewChange: false}) // to prevent revalidation we avoid model to view changes
    }
}

or even you can use a directive and inject Ng Control inside. Because this is pure directive and you may use @hostlistener on it(instead of validate method that angular calls for validators), everything happens before angular form.js enters handling things.

@Directive({
    selector:'[InterceptorDirective]'
}) export class InterceptorDirective implements AfterContentInit {
  constructor(private control:NgControl) {
  }
  ngAfterContentInit(){
      this.control.valueChanges.subscribe(()=>console.log('valueChanges')) // will be logged after hostlistener handler
  }
  @HostListener('input')
  onInput(event:any){
      console.log('input') // will be logged first
  }

the second way you can make sure even validators will receive intercepted value

Related