简体   繁体   English

如何检测 Angular 中的 @Input() 值何时发生变化?

[英]How to detect when an @Input() value changes in Angular?

I have a parent component ( CategoryComponent ), a child component ( videoListComponent ) and an ApiService.我有一个父组件 ( CategoryComponent )、一个子组件 ( videoListComponent ) 和一个 ApiService。

I have most of this working fine ie each component can access the json api and get its relevant data via observables.我的大部分工作都很好,即每个组件都可以访问 json api 并通过 observables 获取其相关数据。

Currently video list component just gets all videos, I would like to filter this to just videos in a particular category, I achieved this by passing the categoryId to the child via @Input() .目前视频列表组件只获取所有视频,我想将其过滤为特定类别中的视频,我通过@Input()将 categoryId 传递给孩子来实现这一点。

CategoryComponent.html类别组件.html

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

This works and when the parent CategoryComponent category changes then the categoryId value gets passed through via @Input() but I then need to detect this in VideoListComponent and re-request the videos array via APIService (with the new categoryId).这有效,当父 CategoryComponent 类别发生变化时,categoryId 值通过@Input()传递,但我需要在 VideoListComponent 中检测到这一点,并通过 APIService 重新请求视频数组(使用新的 categoryId)。

In AngularJS I would have done a $watch on the variable.在 AngularJS 中,我会对变量执行$watch What is the best way to handle this?处理这个问题的最佳方法是什么?

Actually, there are two ways of detecting and acting upon when an input changes in the child component in angular2+ :实际上,当 angular2+ 的子组件中的输入发生变化时,有两种方法可以检测和采取行动:

  1. You can use the ngOnChanges() lifecycle method as also mentioned in older answers:您可以使用旧答案中也提到的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
        
    }
    

Documentation Links: ngOnChanges, SimpleChanges, SimpleChange文档链接: ngOnChanges、 SimpleChanges、 SimpleChange
Demo Example: Look at this plunker演示示例:看看这个 plunker

  1. Alternately, you can also use an input property setter as follows:或者,您还可以使用输入属性设置器,如下所示:
    private _categoryId: string;
    
    @Input() set categoryId(value: string) {
    
       this._categoryId = value;
       this.doSomething(this._categoryId);
    
    }
    
    get categoryId(): string {
    
        return this._categoryId;
    
    }

Documentation Link: Look here .文档链接:看这里

Demo Example: Look at this plunker .演示示例:看看这个 plunker

WHICH APPROACH SHOULD YOU USE?您应该使用哪种方法?

If your component has several inputs, then, if you use ngOnChanges(), you will get all changes for all the inputs at once within ngOnChanges().如果您的组件有多个输入,那么,如果您使用 ngOnChanges(),您将在 ngOnChanges() 中一次获得所有输入的所有更改。 Using this approach, you can also compare current and previous values of the input that has changed and take actions accordingly.使用这种方法,您还可以比较已更改的输入的当前值和以前的值,并采取相应的措施。

However, if you want to do something when only a particular single input changes (and you don't care about the other inputs), then it might be simpler to use an input property setter.但是,如果您只想在特定的单个输入更改时执行某些操作(并且您不关心其他输入),那么使用输入属性设置器可能会更简单。 However, this approach does not provide a built in way to compare previous and current values of the changed input (which you can do easily with the ngOnChanges lifecycle method).但是,这种方法没有提供一种内置方法来比较更改输入的先前值和当前值(您可以使用 ngOnChanges 生命周期方法轻松完成)。

EDIT 2017-07-25: ANGULAR CHANGE DETECTION MAY STILL NOT FIRE UNDER SOME CIRCUMSTANCES编辑 2017-07-25:在某些情况下,角度变化检测可能仍然不会触发

Normally, change detection for both setter and ngOnChanges will fire whenever the parent component changes the data it passes to the child, provided that the data is a JS primitive datatype(string, number, boolean) .通常,只要父组件更改它传递给子组件的数据,setter 和 ngOnChanges 的更改检测就会触发,前提是数据是 JS 原始数据类型(string, number, boolean) However, in the following scenarios, it will not fire and you have to take extra actions in order to make it work.但是,在以下情况下,它不会触发,您必须采取额外的措施才能使其工作。

  1. If you are using a nested object or array (instead of a JS primitive data type) to pass data from Parent to Child, change detection (using either setter or ngchanges) might not fire, as also mentioned in the answer by user: muetzerich.如果您使用嵌套对象或数组(而不是 JS 原始数据类型)将数据从父级传递给子级,则更改检测(使用 setter 或 ngchanges)可能不会触发,正如用户回答中提到的:muetzerich。 For solutions look here .对于解决方案看这里

  2. If you are mutating data outside of the angular context (ie, externally), then angular will not know of the changes.如果您在角度上下文之外(即外部)改变数据,那么角度将不知道这些变化。 You may have to use ChangeDetectorRef or NgZone in your component for making angular aware of external changes and thereby triggering change detection.您可能必须在组件中使用 ChangeDetectorRef 或 NgZone 以从角度感知外部变化,从而触发变化检测。 Refer to this .参考这个

Use the ngOnChanges() lifecycle method in your component.在组件中使用ngOnChanges()生命周期方法。

ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed. ngOnChanges 在数据绑定属性被检查之后和视图和内容子元素被检查之前被调用,如果它们中的至少一个已经改变。

Here are the Docs .这是文档

I was getting errors in the console as well as the compiler and IDE when using the SimpleChanges type in the function signature.在函数签名中使用SimpleChanges类型时,我在控制台以及编译器和 IDE 中遇到错误。 To prevent the errors, use the any keyword in the signature instead.为防止出现错误,请改用签名中的any关键字。

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

EDIT:编辑:

As Jon pointed out below, you can use the SimpleChanges signature when using bracket notation rather than dot notation.正如 Jon 在下面指出的,您可以在使用括号表示法而不是点表示法时使用SimpleChanges签名。

ngOnChanges(changes: SimpleChanges) {
    console.log(changes['myInput'].currentValue);
}
@Input() set categoryId(categoryId: number) {
      console.log(categoryId)
}

please try using this method.请尝试使用此方法。 Hope this helps希望这可以帮助

The safest bet is to go with a shared service instead of a @Input parameter.最安全的选择是使用共享服务而不是@Input参数。 Also, @Input parameter does not detect changes in complex nested object type.此外, @Input参数不会检测复杂嵌套对象类型的变化。

A simple example service is as follows:一个简单的示例服务如下:

Service.ts服务.ts

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组件.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);
  }
}

You can use a similar implementation in other components and all your compoments will share the same shared values.您可以在其他组件中使用类似的实现,并且您的所有组件都将共享相同的共享值。

Angular ngOnChanges角 ngOnChanges

The ngOnChanges() is an inbuilt Angular callback method that is invoked immediately after the default change detector has checked data-bound properties if at least one has changed. ngOnChanges()是一种内置的 Angular 回调方法,在默认更改检测器检查数据绑定属性(如果至少有一个已更改)后立即调用该方法。 Before the view and content, children are checked.在查看和内容之前,检查孩子。

// child.component.ts

import { Component, OnInit, Input, SimpleChanges, OnChanges } from '@angular/core';

@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges {

  @Input() inputParentData: any;

  constructor() { }

  ngOnInit(): void {
  }

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

}

for more: Angular Docs更多信息: Angular 文档

I just want to add that there is another Lifecycle hook called DoCheck that is useful if the @Input value is not a primitive value.我只想补充一点,如果@Input值不是原始值,还有另一个名为DoCheck的生命周期钩子很有用。

I have an Array as an Input so this does not fire the OnChanges event when the content changes (because the checking that Angular does is 'simple' and not deep so the Array is still an Array, even though the content on the Array has changed).我有一个数组作为Input ,因此当内容更改时它不会触发OnChanges事件(因为 Angular 所做的检查是“简单”且不深入,因此数组仍然是一个数组,即使数组上的内容已更改)。

I then implement some custom checking code to decide if I want to update my view with the changed Array.然后我实现了一些自定义检查代码来决定是否要使用更改后的数组来更新我的视图。

Here ngOnChanges will trigger always when your input property changes:当您的输入属性更改时,这里 ngOnChanges 将始终触发:

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

You can also , have an observable which triggers on changes in the parent component(CategoryComponent) and do what you want to do in the subscribtion in the child component.您还可以拥有一个触发父component(CategoryComponent)更改的可观察对象,并在子组件的订阅中执行您想要执行的操作。 ( videoListComponent ) videoListComponent

service.ts服务.ts

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

CategoryComponent.ts类别组件.ts

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

videoListComponent.ts视频列表组件.ts

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

I'd stick to approach, suggested by @alan-cs, but with some modifications.我会坚持采用@alan-cs 建议的方法,但要进行一些修改。 First - I'm against using ngOnChanges .首先 - 我反对使用ngOnChanges Instead, I propose to move all what needs to be changed under one object.相反,我建议将所有需要更改的内容移到一个对象下。 And use BehaviorSubject to track it changes:并使用BehaviorSubject跟踪它的变化:

  private location$: BehaviorSubject<AbxMapLayers.Location> = new BehaviorSubject<AbxMapLayers.Location>(null);

  @Input()
  set location(value: AbxMapLayers.Location) {
    this.location$.next(value);
  }
  get location(): AbxMapLayers.Location {
    return this.location$.value;
  }

<abx-map-layer
    *ngIf="isInteger(unitForm.get('addressId').value)"
    [location]="{
      gpsLatitude: unitForm.get('address.gpsLatitude').value,
      gpsLongitude: unitForm.get('address.gpsLongitude').value,
      country: unitForm.get('address.country').value,
      placeName: unitForm.get('address.placeName').value,
      postalZip: unitForm.get('address.postalZip').value,
      streetName: unitForm.get('address.streetName').value,
      houseNumber: unitForm.get('address.houseNumber').value
    }"
    [inactive]="unitAddressForm.disabled"
    >
</abx-map-layer>

You could also just pass an EventEmitter as Input.你也可以只传递一个 EventEmitter 作为输入。 Not quite sure if this is best practice tho...不太确定这是否是最佳实践...

CategoryComponent.ts:类别组件.ts:

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

- OTHER CODE -

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

CategoryComponent.html:类别组件.html:

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

And in VideoListComponent.ts:在 VideoListComponent.ts 中:

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

This solution uses a proxy class and offers the following advantages:此解决方案使用代理类并提供以下优势:

  • Allows the consumer to leverage the power of RXJS允许消费者利用RXJS的力量
  • More compact than other solutions proposed so far比目前提出的其他解决方案更紧凑
  • More typesafe than using ngOnChanges()比使用ngOnChanges ngOnChanges()更安全

Example usage:示例用法:

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

Utility function:实用功能:

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

You can use a BehaviorSubject within a facade service then subscribe to that subject in any component and when an event happens to trigger a change in data call .next() on it.您可以在外观服务中使用BehaviorSubject ,然后在任何组件中订阅该主题,当事件发生时触发数据调用.next()的更改。 Make sure to close out those subscriptions within the on destroy lifecycle hook.确保在 on destroy 生命周期钩子中关闭这些订阅。

data-api.facade.ts数据-api.facade.ts

@Injectable({
  providedIn: 'root'
})
export class DataApiFacade {

currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);

}

some.component.ts一些.component.ts

constructor(private dataApiFacade: DataApiFacade){}

ngOnInit(): void {
  this.dataApiFacade.currentTabIndex
    .pipe(takeUntil(this.destroy$))
       .subscribe(value => {
          if (value) {
             this.currentTabIndex = value;
          }
    });
}

setTabView(event: MatTabChangeEvent) {
  this.dataApiFacade.currentTabIndex.next(event.index);
}

ngOnDestroy() {
  this.destroy$.next(true);
  this.destroy$.complete();
}

You can use the ngOnChanges() lifecycle method您可以使用ngOnChanges() 生命周期方法

@Input() inputValue: string;

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

Basically both suggested solutions work fine in most cases.基本上,两种建议的解决方案在大多数情况下都可以正常工作。 My main negative experience with ngOnChange() is the lack of type safety.我对ngOnChange()的主要负面体验是缺乏类型安全性。

In one of my projects I did some renaming, following which some of the magic strings remained unchanged, and the bug of course took some time to surface.在我的一个项目中,我做了一些重命名,之后一些魔术字符串保持不变,当然这个错误需要一些时间才能浮出水面。

Setters do not have that issue: your IDE or compiler will let you know of any mismatch. Setter 没有这个问题:您的 IDE 或编译器会让您知道任何不匹配的情况。

If you don't want use ngOnChange implement og onChange() method, you can also subscribe to changes of a specific item by valueChanges event , ETC.如果您不想使用ngOnChange实现 og onChange()方法,您也可以通过valueChanges事件等订阅特定项目的更改。

myForm = new FormGroup({
  first: new FormControl(),
});

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

the markForCheck() writen because of using in this declare:由于在此声明中使用而编写的markForCheck()

changeDetection: ChangeDetectionStrategy.OnPush

If you're dealing with the case that your are using @Input to share data between parent and child component, you can detect @Input data changes by using the lifecycle method: ngOnChanges如果您正在处理使用@Input在父组件和子组件之间共享数据的情况,您可以使用生命周期方法检测@Input数据更改: ngOnChanges

 ngOnChanges(changes: SimpleChanges) {
    if (!changes.categoryId.firstChange) {
      // only logged upon a change after rendering
      console.log(changes.categoryId.currentValue);
    }
  }

And I'm advising that you should care of the change strategy implemented for the child component you should add ChangeDetectionStrategy.OnPush for some performance reason :而且我建议您应该注意为子组件实施的更改策略,出于某种性能原因,您应该添加 ChangeDetectionStrategy.OnPush :

Example-Code :示例代码:

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoListComponent implements OnChanges {
  @Input() categoryId: string;

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM