Chedy2149
Chedy2149

Reputation: 3051

How to observe touched event on Angular 2 NgForm?

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?

Upvotes: 34

Views: 42612

Answers (8)

Pauli
Pauli

Reputation: 672

Anyone looking for this solution now there's way easier way that doesn't require any modifications to formControl itself. We can now subscribe to events on formControl. The events observable includes TouchedChangeEvent (as well as ValueChangeEvent, StatusChangeEvent, PristineChangeEvent, etc...)

    this.formControl.events.subscribe((event: ControlEvent<T>) => {
        if (e instanceof TouchedChangeEvent){
            if (e.touched){
                // Do stuff
            } else {
                // Do other stuff...
            } 
        }
      
    });

From the docs, events is:

A multicasting observable that emits an event every time the state of the control changes. It emits for value, status, pristine or touched changes

See a StackBlitz code example here. The example triggers TouchedChangeEvent event by calling markAsTouched(), and then calling markAsUntouched().

Upvotes: 3

lilith
lilith

Reputation: 21

Extended solution posted by @ʞᴉɯ

const form = new FormControl('');
(form as any)._markAsTouched = form.markAsTouched;
(form as any).touchedChanges = new Subject();
form.markAsTouched = opts => {
  (form as any)._markAsTouched(opts);
  (form as any).touchedChanges.next('touched');
}; 

...

(form as any).touchedChanges.asObservable().subscribe(() => {
  // execute something when form was marked as touched
});

Upvotes: 2

Viktor Chernodub
Viktor Chernodub

Reputation: 441

Here's the util function I came up with, it also listens for reset method and makes control untouched:

/**
 * Allows to listen the touched state change.
 * The util is needed until Angular allows to listen for such events.
 * Https://github.com/angular/angular/issues/10887.
 * @param control Control to listen for.
 */
export function listenControlTouched(
  control: AbstractControl,
): Observable<boolean> {
  return new Observable<boolean>(observer => {
    const originalMarkAsTouched = control.markAsTouched;
    const originalReset = control.reset;

    control.reset = (...args) => {
      observer.next(false);
      originalReset.call(control, ...args);
    };

    control.markAsTouched = (...args) => {
      observer.next(true);
      originalMarkAsTouched.call(control, ...args);
    };

    observer.next(control.touched);

    return () => {
      control.markAsTouched = originalMarkAsTouched;
      control.reset = originalReset;
    };
  });
}

Upvotes: 3

mbdavis
mbdavis

Reputation: 4010

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

Upvotes: 18

ʞᴉɯ
ʞᴉɯ

Reputation: 5594

I've solved this way:

this.control['_markAsTouched'] = this.control.markAsTouched;
this.control.markAsTouched = () => {
  this.control['_markAsTouched']();
  // your event handler
}

basically i'm overwriting the default markAsTouched method of FormControl.

Upvotes: 8

Ethan Standel
Ethan Standel

Reputation: 61

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.

Upvotes: 5

Eggy
Eggy

Reputation: 4174

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);
  }
}

Upvotes: 25

Valikhan Akhmedov
Valikhan Akhmedov

Reputation: 976

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.

Upvotes: 19

Related Questions