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

4

17

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

Gurule answered 8/8, 2017 at 23:27 Comment(3)
Can you create plunker? Do you want to type 3 and get 777?Basophil
Basically, yes. What I want is that the user enters 1 and the form control value is set to 0.01. And if the form control value is 0.01, the textbox should display 1.Gurule
This is a good article on the subject - specifically for Angular Material. medium.com/angular-in-depth/… - Also includes a note about validation which should apply to all controls.Hypoderma
G
11

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.

Glossectomy answered 20/9, 2018 at 16:15 Comment(2)
It is a good solution, but it does not work for custom validators. Correct me if I am wrong but as I tried, the validator function does not receive the "transformed" value of the directive.Jitterbug
Could you post an example? Maybe the order is off, so the transformation happens after the validation?Glossectomy
T
1

You may be able to use onBlur to call a function ( i.e. modifyValue() ) and then leverage patchValue to modify the value:

<input type="text" onblur="modifyValue()" formControlName="firstName" />

modifyValue() {
    this.form.patchValue({
      firstName: this.form.firstName //modify firstName here
    })
}

If that works, you could create a generic function and pass the key / value to in order to patch it without having to create a bunch of specific functions

<input type="text" onblur="modifyValue('firstName')" formControlName="firstName" />

  modifyValue(key) {
      this.form.controls[key].patchValue(this.form.controls[key] // modify value here)
  }
Tatro answered 9/8, 2017 at 7:5 Comment(1)
If I use onBlur to manually set the form value, why do I need the formControlName="firstName" at all? And again, the problem is scalability. This solution works fine for small instances. But when you have a form with many fields that need to be transformed, you end up with a ton of boilerplate code.Gurule
H
0

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

Hyponasty answered 14/6, 2021 at 7:55 Comment(0)
P
0

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

Pitta answered 15/2, 2022 at 0:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.