如何在Angular 2 NgForm上观察触摸事件?

27
可以通过订阅一个回调函数到NgFormvalueChanges可观察属性来对表单控件值的变化做出反应。
同样地,我需要对用户“触摸表单控件”这个事件作出响应。 这个类似乎定义了valueChanges可观察对象,而touched属性则被定义为布尔类型。 是否有一种方法可以对“控件触摸”事件做出反应?

请提供您的代码片段 - Piou
我使用以下代码解决了此问题: form.control.markAllAsTouched() - Pankwood
7个回答

23
你可以扩展默认的FormControl类,并添加markAsTouched方法,该方法将调用本地方法并附加你的副作用。
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);
  }
}

1
我还需要订阅/处理表单控件上的触摸事件。我该如何告诉Angular使用扩展的FormControl代替原始的FormControl类? - Adam
@adamisnt 使用 Angular DI 系统:https://angular.io/docs/ts/latest/guide/dependency-injection.html#injector-providers - Eggy
@adamisnt 需要注意的一点是,在上面的示例中订阅可能会导致内存泄漏,因为我们从未取消订阅。最好只公开一个可观察对象并在组件内使用它。 - Eggy
@Eggy,您介意向我们展示如何使用Angular DI机制提供扩展FormControl吗? - Stevy
@Eggy 我们如何使用Angular DI机制提供ExtendedFormControl? - Aniket

18

ng2没有提供直接响应触摸事件的方法。它使用 (input) 事件触发 valueChanges 事件,使用 (blur) 事件设置 AbstractControltouched/untouched 属性。

因此,您需要在模板中手动订阅所需的事件,并在组件类中处理它。


13

我曾经遇到过相同的问题 - 我编写了这个帮助方法来提取一个可观察对象,您可以订阅它以便在表单被触摸状态更改时得到通知:

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

我接下来会使用 merge(this.touchedChanged$, this.form.valueChanges),以获取一个observable对象,该对象包含了所有用于更新验证所需的更改。

*编辑 - 在@marked-down的建议下,我已将对先前函数的调用移到发出新值之前,以防您在收到值后直接查询并最终失去同步


我喜欢这个想法,但是你这样做不会改变原型吗?这样做会在其他地方触发副作用吗? - Joep Kockelkorn
2
有点晚了 - 但是不,你只影响传递给助手的表单实例。 - mbdavis
1
一个小的补充:我交换了绑定调用到先前函数和在新的 markAsTouched/markAsUntouched 函数中发出值的主题的顺序,因为如果你正在监听 touchChanged$ 事件并决定查询 control.touched,你可能会陷入一个状态,其中发射的值是 truecontrol.touched 属性仍然是 false。这可以通过在调用现有的 markAs(Un)touched 函数后再发射来避免。 - marked-down
这实际上是一个很好的观点,@marked-down - 我会更新我的答案。 - mbdavis

7
我已经找到了解决方法:
this.control['_markAsTouched'] = this.control.markAsTouched;
this.control.markAsTouched = () => {
  this.control['_markAsTouched']();
  // your event handler
}

基本上,我正在重写FormControl的默认markAsTouched方法。

所以,为了解释上面的内容,我们创建一个临时的_markAsTouched属性来存储默认的markAsTouched函数。然后,我们更新markAsTouched函数来执行其默认行为,然后执行我们需要的任何自定义操作。聪明。为了完整起见,我希望我们在ngOnDestroy中恢复更改,使用this.control.markAsTouched = this.control.['_markAsTouched']; - undefined

4

如果你的问题和我的一样,我正在尝试在一个组件中标记一个字段为已触摸,然后在另一个组件中做出响应。我可以访问该字段的AbstractControl。我解决这个问题的方法是:

field.markAsTouched();
(field.valueChanges as EventEmitter<any>).emit(field.value);

然后我只需在另一个组件中订阅valueChanges。值得注意的是:field.valueChanges被导出为Observable,但在运行时它是一个EventEmitter,这使得解决方案不够完美。显然,这种方法的另一个限制就是你订阅了比仅触摸状态更多的内容。


1
谢谢你的技巧!有一个开放的功能请求,希望扩展表单API与更多的事件发射器,我引用了你的答案作为一个临时解决方案: https://github.com/angular/angular/issues/10887#issuecomment-481729918 - almeidap

2
这是我想出来的实用函数,它还监听了“reset”方法,并使控件保持不变:
/**
 * 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;
    };
  });
}


1

@ʞᴉɯ发布的扩展解决方案

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

你的回答可以通过提供更多支持性信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人能够确认你的回答是否正确。你可以在帮助中心找到关于如何撰写好回答的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接