如何检测@Input()值何时在Angular中更改?


468

我有一个父组件(CategoryComponent),一个子组件(videoListComponent)和一个ApiService。

我的大部分工作都很好,即每个组件都可以访问json api并通过可观察对象获取其相关数据。

目前,视频列表组件仅获取所有视频,我想将其过滤为特定类别中的视频,我是通过将categoryId传递给子对象来实现的@Input()

CategoryComponent.html

<video-list *ngIf="category" [categoryId]="category.id"></video-list>

这可以正常工作,并且当父CategoryComponent类别更改时,categoryId值将通过传递,@Input()但是我需要在VideoListComponent中进行检测,然后通过APIService(带有新的categoryId)重新请求视频数组。

在AngularJS中,我会$watch在变量上做一个。处理此问题的最佳方法是什么?




Answers:


717

实际上,当输入在angular2 +的子组件中发生更改时,有两种检测和处理方法:

  1. 您可以使用ngOnChanges()生命周期方法,如较早的答案中所述:
    @Input() categoryId: string;

    ngOnChanges(changes: SimpleChanges) {

        this.doSomething(changes.categoryId.currentValue);
        // You can also use categoryId.previousValue and 
        // categoryId.firstChange for comparing old and new values

    }

文档链接:ngOnChanges, SimpleChanges, SimpleChange
演示示例:查看此插件

  1. 或者,您也可以使用输入属性设置器,如下所示:
    private _categoryId: string;

    @Input() set categoryId(value: string) {

       this._categoryId = value;
       this.doSomething(this._categoryId);

    }

    get categoryId(): string {

        return this._categoryId;

    }

文档链接:在此处查看

演示示例:看一下这个punker

您应该使用哪种方法?

如果您的组件具有多个输入,那么,如果您使用ngOnChanges(),则将在ngOnChanges()中一次获取所有输入的所有更改。使用这种方法,您还可以比较已更改输入的当前值和先前值,并采取相应的措施。

但是,如果您只想更改一个特定的单个输入(而不在乎其他输入)就想做某事,那么使用输入属性设置器可能会更简单。但是,这种方法没有提供一种内置的方式来比较更改后的输入的先前值和当前值(您可以使用ngOnChanges生命周期方法轻松完成此操作)。

编辑2017-07-25:在某些情况下,角度变化检测可能仍无法触发

通常,只要父组件更改传递给子代的数据只要该数据是JS基本数据类型(字符串,数字,布尔值), setter和ngOnChanges的更改检测就会触发。但是,在以下情况下,它不会启动,您必须采取额外的措施才能使其正常工作。

  1. 如果您使用嵌套的对象或数组(而不是JS基本数据类型)将数据从父级传递到子级,则更改触发(使用setter或ngchanges)可能不会触发,正如用户回答:muetzerich中所述。有关解决方案,请点击此处

  2. 如果要在角度上下文之外(即,在外部)对数据进行突变,则角度不会知道这些变化。您可能必须在组件中使用ChangeDetectorRef或NgZone才能使角度了解外部更改,从而触发更改检测。参考这个


8
关于将setter与输入配合使用的非常重要的一点,因为它更全面,我将其更新为公认的答案。谢谢。
乔恩·卡特穆尔

2
@trichetriche,只要父级更改输入,就会调用setter(设置方法)。同样,ngOnChanges()也是如此。
艾伦CS

好吧,显然不是……我会设法弄清楚,这可能是我做错了。

1
我只是偶然发现了这个问题,并指出您说使用setter不允许比较值,但是事实并非如此,您可以doSomething在实际设置新值之前调用您的方法并接受2个参数新旧值在设置和调用该方法之前将存储旧值。
T.Aoukar '17

1
@ T.Aoukar我的意思是输入设置器本身不支持比较ngOnChanges()的新旧值。您始终可以使用黑客来存储旧值(如您所述)以将其与新值进行比较。
艾伦CS

105

ngOnChanges()在组件中使用生命周期方法。

在检查了数据绑定属性之后,以及在检查视图和内容子级是否至少其中之一已更改之前,立即调用ngOnChanges。

这是文档


6
当变量设置为新值(例如)时有效MyComponent.myArray = [],但是如果输入值被更改(例如)不触发MyComponent.myArray.push(newValue)ngOnChanges()则此功能有效。关于如何捕获此更改的任何想法?
Levi Fuller

4
ngOnChanges()嵌套对象发生更改时不会调用。也许您在这里
muetzerich

33

SimpleChanges在函数签名中使用类型时,在控制台以及编译器和IDE中出现错误。为防止错误,请any在签名中使用关键字。

ngOnChanges(changes: any) {
    console.log(changes.myInput.currentValue);
}

编辑:

正如Jon在下面指出的那样,可以SimpleChanges在使用方括号表示法而不是点表示法时使用签名。

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}

1
您是否SimpleChanges在文件顶部导入了界面?
乔恩·卡特穆尔

@乔恩-是的。问题不在于签名本身,而是访问在运行时分配给SimpleChanges对象的参数。例如,在我的组件中,我将User类绑定到输入(即<my-user-details [user]="selectedUser"></my-user-details>)。可通过调用从SimpleChanges类访问此属性changes.user.currentValue。注意user是在运行时绑定的,并不属于SimpleChanges对象
Darcy

1
你有尝试过吗? ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
乔恩·卡特穆尔

@Jon-切换到括号表示法即可。我更新了答案,将其包括在内。
达西

1
从“ @ angular / core”导入{SimpleChanges};如果它是在Angular文档中,而不是本机打字稿类型,那么它可能来自
Angular

13

最安全的办法就是与共享服务,而不是一个@Input参数。另外,@Inputparameter不会检测复杂的嵌套对象类型的更改。

一个简单的示例服务如下:

服务

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class SyncService {

    private thread_id = new Subject<number>();
    thread_id$ = this.thread_id.asObservable();

    set_thread_id(thread_id: number) {
        this.thread_id.next(thread_id);
    }

}

Component.ts

export class ConsumerComponent implements OnInit {

  constructor(
    public sync: SyncService
  ) {
     this.sync.thread_id$.subscribe(thread_id => {
          **Process Value Updates Here**
      }
    }

  selectChat(thread_id: number) {  <--- How to update values
    this.sync.set_thread_id(thread_id);
  }
}

您可以在其他组件中使用类似的实现,并且所有组件都将共享相同的共享值。


1
谢谢Sanket。这是一个很好的观点,并且在适当情况下是一种更好的方法。我将保留接受的答案,因为它回答了有关检测@Input更改的特定问题。
乔恩·卡特穆尔

1
@Input参数不能检测复杂的嵌套对象类型内部的更改,但是如果对象本身发生更改,它将触发,对吗?例如,我有输入参数“ parent”,因此,如果我整体更改了parent,则将触发更改检测,但是如果更改parent.name,它将不会?
yogibimbi

您应提供解决方案更安全的理由
Joshua Kemmerer '18

1
@JoshuaKemmerer @Input参数不会检测复杂的嵌套对象类型中的更改,但是会检测简单的本机对象中的更改。
Sanket Berde '18

6

我只想补充一点DoCheck,如果该@Input值不是原始值,则有另一个Lifecycle钩子会很有用。

我有一个Array,Input因此OnChanges当内容更改时不会触发该事件(因为Angular所做的检查是“简单的”且不深入,因此即使Array上的内容已更改,该Array仍然是Array)。

然后,我实现一些自定义检查代码,以决定是否要使用更改后的Array更新视图。



4

您还可以具有一个可观察项,该可观察项触发父组件(CategoryComponent)中的更改,并在子组件的订阅中执行您想做的事情。(videoListComponent)

service.ts 
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);

-----------------
CategoryComponent.ts
public onCategoryChange(): void {
  service.categoryChange$.next();
}

-----------------
videoListComponent.ts
public ngOnInit(): void {
  service.categoryChange$.subscribe(() => {
   // do your logic
});
}

2

此处ngOnChanges总是在输入属性更改时触发:

ngOnChanges(changes: SimpleChanges): void {
 console.log(changes.categoryId.currentValue)
}

1

此解决方案使用代理类,并具有以下优点:

  • 允许消费者利用RXJS的功能
  • 比迄今为止提出的其他解决方案更紧凑
  • 比使用更类型安全ngOnChanges()

用法示例:

@Input()
public num: number;
numChanges$ = observeProperty(this as MyComponent, 'num');

实用功能:

export function observeProperty<T, K extends keyof T>(target: T, key: K) {
  const subject = new BehaviorSubject<T[K]>(target[key]);
  Object.defineProperty(target, key, {
    get(): T[K] { return subject.getValue(); },
    set(newValue: T[K]): void {
      if (newValue !== subject.getValue()) {
        subject.next(newValue);
      }
    }
  });
  return subject;
}

0

如果您不想使用ngOnChange实现og onChange()方法,则还可以通过valueChanges事件 ETC 订阅特定项目的更改。

 myForm= new FormGroup({
first: new FormControl()   

});

this.myForm.valueChanges.subscribe(formValue => {
  this.changeDetector.markForCheck();
});

由于在此声明中使用而写的markForCheck():

  changeDetection: ChangeDetectionStrategy.OnPush

3
这与问题完全不同,尽管乐于助人的人们应该意识到您的代码是关于另一种类型的更改的。该问题询问的是来自组件外部的数据更改,然后让您的组件知道并响应数据已更改的事实。您的答案是关于检测组件本身内部的变化,例如对组件本地的表单上的输入。
rmcsharry19年

0

您也可以只传递一个EventEmitter作为输入。不确定这是否是最佳实践吗?

CategoryComponent.ts:

categoryIdEvent: EventEmitter<string> = new EventEmitter<>();

- OTHER CODE -

setCategoryId(id) {
 this.category.id = id;
 this.categoryIdEvent.emit(this.category.id);
}

CategoryComponent.html:

<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>

在VideoListComponent.ts中:

@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
 this.categoryIdEvent.subscribe(newID => {
  this.categoryId = newID;
 }
}

请提供有用的链接,代码段以支持您的解决方案。
mishsx

我加了一个例子。
塞德里克施魏策尔
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.