如何在Angular中监视表单更改


151

在Angular中,我可能会有一个如下形式的表单:

<ng-form>
    <label>First Name</label>
    <input type="text" ng-model="model.first_name">

    <label>Last Name</label>
    <input type="text" ng-model="model.last_name">
</ng-form>

在相应的控制器中,我可以轻松地监视该表单内容的更改,如下所示:

function($scope) {

    $scope.model = {};

    $scope.$watch('model', () => {
        // Model has updated
    }, true);

}

这是JSFiddle上Angular示例

我在弄清楚如何在Angular中完成相同的事情时遇到了麻烦。显然,我们不再拥有$scope$ rootScope。当然,有一种方法可以完成同一件事?

这是Plunker上Angular示例


我认为您应该从表单中触发一个事件(例如旧的ng-change),而不是查看控制器中的一些数据。
Deblaton Jean-Philippe

顺便说一句,删除范围的原因是摆脱那些观察者。我不认为有一块手表隐藏在2
号角的

4
如果我对您的理解正确,那么建议我(ngModelChange)="onModelChange($event)"每个表单输入添加一个属性以完成此操作?
漫步者

1
表单实例本身是否不会发出某种更改事件?如果是这样,您如何访问它?
漫步者

如果我确定要做什么,我会回答而不是发表评论。我还没有使用Angle 2.0,但是从我的阅读中,该手表已经完全消失,不再具有基于事件的框架(而不是在每个摘要中进行的深入研究)
Deblaton Jean-Philippe

Answers:


189

UPD。答案和演示已更新,以与最新的Angular保持一致。


由于表示表单的FormGroup提供的valueChanges属性是Observerable实例,因此您可以订阅整个表单更改:

this.form.valueChanges.subscribe(data => console.log('Form changes', data));

在这种情况下,您将需要使用FormBuilder手动构造表单。像这样:

export class App {
  constructor(private formBuilder: FormBuilder) {
    this.form = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })

    this.form.valueChanges.subscribe(data => {
      console.log('Form changes', data)
      this.output = data
    })
  }
}

退房valueChanges的行动在这个演示http://plnkr.co/edit/xOz5xaQyMlRzSrgtt7Wn?p=preview


2
这非常接近目标。确认-您是否告诉我,如果仅在模板内定义表单,则无法订阅该表单的valueChanges事件发送器?换句话说-在组件的构造函数中,无法获得对仅在该组件的模板内而不是在FormBuilder的帮助下定义的表单的引用吗?
漫步者

这是正确的答案。如果您没有表单构建器,那么您正在做模板驱动的表单。可能仍然有一种方法可以注入表单,但是如果您想使用Observable form.valueChanges,则应明确使用formBuilder并放弃ng-model
Angular University

2
@tambler,您可以使用引用NgForm @ViewChild()。看到我更新的答案。
Mark Rajcok '16

1
您不需要取消订阅吗?
巴青加

1
@galvan我认为不会泄漏。表单是组件的一部分,它将被适当地处置于其所有字段和事件侦听器中的destroy中。
dfsq

107

如果您使用FormBuilder,请参阅@dfsq的答案。

如果您不使用FormBuilder,则有两种方法可以将更改通知您。

方法一

如对问题的评论中所述,在每个输入元素上使用事件绑定。添加到您的模板:

<input type="text" class="form-control" required [ngModel]="model.first_name"
         (ngModelChange)="doSomething($event)">

然后在您的组件中:

doSomething(newValue) {
  model.first_name = newValue;
  console.log(newValue)
}

表单页面有关于ngModel一些额外的信息,那就是与此有关:

ngModelChange不是<input>元素事件。它实际上是NgModel指令的事件属性。当Angular看到表单中的绑定目标时[(x)],它期望x指令具有x输入属性和一个xChange输出属性。

另一个奇怪的是模板表达式model.name = $event。我们习惯于看到$event来自DOM事件的对象。ngModelChange属性不会产生DOM事件。这是一个角度EventEmitter属性,在触发时返回输入框的值。

我们几乎总是喜欢[(ngModel)]。如果必须在事件处理中执行一些特殊的操作,例如去抖动或限制按键,则可以拆分绑定。

就您而言,我想您想做一些特别的事情。

方法二

定义一个本地模板变量并将其设置为ngForm
在输入元素上使用ngControl。
使用@ViewChild获取对表单的NgForm指令的引用,然后订阅NgForm的ControlGroup进行更改:

<form #myForm="ngForm" (ngSubmit)="onSubmit()">
  ....
  <input type="text" ngControl="firstName" class="form-control" 
   required [(ngModel)]="model.first_name">
  ...
  <input type="text" ngControl="lastName" class="form-control" 
   required [(ngModel)]="model.last_name">

class MyForm {
  @ViewChild('myForm') form;
  ...
  ngAfterViewInit() {
    console.log(this.form)
    this.form.control.valueChanges
      .subscribe(values => this.doSomething(values));
  }
  doSomething(values) {
    console.log(values);
  }
}

plunker

有关方法2的更多信息,请参见Savkin的视频

另请参阅@Thierry的答案,以获取有关valueChanges可观察对象可以执行的操作的更多信息(例如,在处理更改之前先进行反跳/等待)。


61

为了完成更多先前的出色答案,您需要意识到表单利用可观察变量来检测和处理价值变化。这确实很重要而且功能强大。Mark和dfsq都在回答中描述了这一方面。

可观察对象不仅允许使用该subscribe方法(类似于thenAngular 1中的promise方法)。如果需要为表单中的更新数据实现一些处理链,则可以走得更远。

我的意思是您可以在此级别使用debounceTime方法指定去抖动时间。这使您可以等待一段时间才能处理更改并正确处理多个输入:

this.form.valueChanges
    .debounceTime(500)
    .subscribe(data => console.log('form changes', data));

您也可以在值更新时直接插入要触发的处理(例如一些异步处理)。例如,如果要处理文本值以基于AJAX请求过滤列表,则可以利用以下switchMap方法:

this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

您甚至可以通过直接将返回的observable链接到组件的属性来进行进一步操作:

this.list = this.textValue.valueChanges
    .debounceTime(500)
    .switchMap(data => this.httpService.getListValues(data))
    .subscribe(data => console.log('new list values', data));

并使用async管道显示它:

<ul>
  <li *ngFor="#elt of (list | async)">{{elt.name}}</li>
</ul>

只是说,您需要考虑在Angular2中以不同方式处理表单的方式(一种更强大的方式;-))。

希望对您有帮助,蒂埃里


属性'valueChanges'在类型'string'上不存在
Toolkit

我在TS文件中卡了应该放置检查表单更改方法的位置?如果我们将其存储在ngAfterViewInit(){}中,那么如果我们需要为表单中的更新数据实现一些处理链,则似乎总是调用this.form.valueChanges。
泰阮

1

扩展Mark的建议...

方法3

在模型上实施“深度”更改检测。优点主要包括避免将用户界面方面合并到组件中。这也可以捕获对模型进行的程序更改。也就是说,按照Thierry的建议,将需要进行一些额外的工作来实现诸如反跳之类的功能,并且这还会捕获您自己的程序更改,因此请谨慎使用。

export class App implements DoCheck {
  person = { first: "Sally", last: "Jones" };
  oldPerson = { ...this.person }; // ES6 shallow clone. Use lodash or something for deep cloning

  ngDoCheck() {
    // Simple shallow property comparison - use fancy recursive deep comparison for more complex needs
    for (let prop in this.person) {
      if (this.oldPerson[prop] !==  this.person[prop]) {
        console.log(`person.${prop} changed: ${this.person[prop]}`);
        this.oldPerson[prop] = this.person[prop];
      }
    }
  }

在Plunker中尝试


1

对于有角度的5+版本。放置版本会有所帮助,因为角度会做出很多更改。

ngOnInit() {

 this.myForm = formBuilder.group({
      firstName: 'Thomas',
      lastName: 'Mann'
    })
this.formControlValueChanged() // Note if you are doing an edit/fetching data from an observer this must be called only after your form is properly initialized otherwise you will get error.
}

formControlValueChanged(): void {       
        this.myForm.valueChanges.subscribe(value => {
            console.log('value changed', value)
        })
}

0

我考虑过使用(ngModelChange)方法,然后考虑了FormBuilder方法,最后选择了方法3的一种变体。这样可以节省用额外的属性来装饰模板,并自动选择对模型的更改-减少了忘记某些事情的可能性使用方法1或2。

简化方法3 ...

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        this.doSomething();
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}

您可以添加一个超时时间,以仅在x毫秒后调用doSomething()来模拟去抖动。

oldPerson = JSON.parse(JSON.stringify(this.person));

ngDoCheck(): void {
    if (JSON.stringify(this.person) !== JSON.stringify(this.oldPerson)) {
        if (timeOut) clearTimeout(timeOut);
        let timeOut = setTimeout(this.doSomething(), 2000);
        this.oldPerson = JSON.parse(JSON.stringify(this.person));
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.