It is possible to subscribe a callback to an NgForm
's valueChanges
observable property in order to react to changes in the values of the controls of the form.
I need, in the same fashion, to react to the event of the user touching one of the form controls.
This class seem to define the valueChanges
Observable and the touched
property is defined as a boolean.
Is there a way to to react to the "control touched" event?
There is not direct way provided by ng2 to react on touched event. It uses (input) event to fire the valueChanges event and (blur) event to set touched/untouched property of AbstractControl.
So you need to manually subscribe on desired event in the template and handle it in your component class.
You can extend default FormControl
class, and add markAsTouched
method that will call native method, plus your side effect.
import { Injectable } from '@angular/core';
import { FormControl, AsyncValidatorFn, ValidatorFn } from '@angular/forms';
import { Subscription, Subject, Observable } from 'rxjs';
export class ExtendedFormControl extends FormControl {
statusChanges$: Subscription;
touchedChanges: Subject<boolean> = new Subject<boolean>();
constructor(
formState: Object,
validator: ValidatorFn | ValidatorFn[] = null,
asyncValidator: AsyncValidatorFn | AsyncValidatorFn[] = null
) {
super(formState, validator, asyncValidator);
this.statusChanges$ = Observable.merge(
this.valueChanges,
this.touchedChanges.distinctUntilChanged()
).subscribe(() => {
console.log('new value or field was touched');
});
}
markAsTouched({ onlySelf }: { onlySelf?: boolean } = {}): void {
super.markAsTouched({ onlySelf });
this.touchedChanges.next(true);
}
}
If your issue was anything like mine, I was trying to mark a field as touched in one component and then respond to that in another component. I had access to the AbstractControl
for that field. The way I got around it was
field.markAsTouched();
(field.valueChanges as EventEmitter<any>).emit(field.value);
And then I just subscribed to valueChanges in my other component. Noteworthy: field.valueChanges
is exported as an Observable, but at runtime it's an EventEmitter
, making this a less than beautiful solution. The other limitation of this would obviously be the fact that you're subscribing to a lot more than just the touched state.
Had this same issue - put together this helper method to extract an observable which you can subscribe to in a form to be notified when touched status changes:
// Helper types
/**
* Extract arguments of function
*/
export type ArgumentsType<F> = F extends (...args: infer A) => any ? A : never;
/**
* Creates an object like O. Optionally provide minimum set of properties P which the objects must share to conform
*/
type ObjectLike<O extends object, P extends keyof O = keyof O> = Pick<O, P>;
/**
* Extract a touched changed observable from an abstract control
* @param control AbstractControl like object with markAsTouched method
*/
export const extractTouchedChanges = (control: ObjectLike<AbstractControl, 'markAsTouched' | 'markAsUntouched'>): Observable<boolean> => {
const prevMarkAsTouched = control.markAsTouched;
const prevMarkAsUntouched = control.markAsUntouched;
const touchedChanges$ = new Subject<boolean>();
function nextMarkAsTouched(...args: ArgumentsType<AbstractControl['markAsTouched']>) {
touchedChanges$.next(true);
prevMarkAsTouched.bind(control)(...args);
}
function nextMarkAsUntouched(...args: ArgumentsType<AbstractControl['markAsUntouched']>) {
touchedChanges$.next(false);
prevMarkAsUntouched.bind(control)(...args);
}
control.markAsTouched = nextMarkAsTouched;
control.markAsUntouched = nextMarkAsUntouched;
return touchedChanges$;
}
// Usage (in component file)
...
this.touchedChanged$ = extractTouchedChanges(this.form);
...
I then like to do merge(this.touchedChanged$, this.form.valueChanges)
to get an observable of all changes required to update validation.