How to observe touched event on Angular 2 NgForm?
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);
}
}
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.
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.bind(control);
const prevMarkAsUntouched = control.markAsUntouched.bind(control);
const touchedChanges$ = new Subject<boolean>();
function nextMarkAsTouched(...args: ArgumentsType<AbstractControl['markAsTouched']>) {
prevMarkAsTouched(...args);
touchedChanges$.next(true);
}
function nextMarkAsUntouched(...args: ArgumentsType<AbstractControl['markAsUntouched']>) {
prevMarkAsUntouched(...args);
touchedChanges$.next(false);
}
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.
*Edit - on @marked-down's suggestion I've moved the call to the previous function to before emitting the new value, in case you query directly after receiving the value and end up out of sync