我有一个父组件(CategoryComponent(,一个子组件(videoListComponent(和一个ApiService。
我的大部分工作正常,即每个组件都可以访问 json api 并通过可观察量获取其相关数据。
目前视频列表组件只获取所有视频,我想将其过滤为特定类别中的视频,我通过 @Input()
将 categoryId 传递给孩子来实现了这一点。
类别组件.html
<video-list *ngIf="category" [categoryId]="category.id"></video-list>
这有效,当父类别组件类别更改时,类别 Id 值通过 @Input()
传递,但我需要在 VideoListComponent 中检测到这一点,并通过 APIService 重新请求视频数组(使用新的 categoryId(。
在AngularJS中,我会对变量进行$watch
。处理此问题的最佳方法是什么?
实际上,有两种方法可以检测和处理 angular2+ 中子组件中的输入更改:
- 您可以使用 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
演示示例:看看这个 plunker
- 或者,您也可以使用输入属性资源库,如下所示:
private _categoryId: string;
@Input() set categoryId(value: string) {
this._categoryId = value;
this.doSomething(this._categoryId);
}
get categoryId(): string {
return this._categoryId;
}
文档链接:看这里。
演示示例:看看这个 plunker。
您应该使用哪种方法?
如果你的组件有多个输入,那么,如果你使用 ngOnChanges((,你将在 ngOnChanges(( 中一次获得所有输入的所有更改。使用此方法,您还可以比较已更改的输入的当前值和先前值,并采取相应的操作。
但是,如果要在只有一个特定的单个输入更改时执行某些操作(并且您不关心其他输入(,则使用输入属性 setter 可能更简单。但是,此方法不提供一种内置方法来比较已更改输入的先前值和当前值(您可以使用ngOnChanges生命周期方法轻松完成此操作(。
编辑2017-07-25:在某些情况下角度变化检测可能仍然不会触发
通常,只要父组件更改它传递给子组件的数据,setter 和 ngOnChanges 的更改检测就会触发,前提是数据是 JS 原始数据类型(字符串、数字、布尔值(。但是,在以下情况下,它不会触发,您必须采取额外的操作才能使其正常工作。
如果您使用嵌套对象或数组(而不是 JS 基元数据类型(将数据从父级传递到子级,则更改检测(使用 setter 或 ngchanges(可能不会触发,正如用户答案中提到的:muetzerich。有关解决方案,请看这里。
如果您在角度上下文之外(即外部(更改数据,则角度将不知道这些变化。您可能必须在组件中使用 ChangeDetectorRef 或 NgZone 来感知外部更改,从而触发更改检测。请参阅此处。
在组件中使用ngOnChanges()
生命周期方法。
ngOnChanges 是在数据绑定属性被调用后立即调用的 选中并在查看和内容之前选中子项,如果至少 其中一个发生了变化。
以下是文档。
函数签名中使用SimpleChanges
类型时,我在控制台以及编译器和 IDE 中遇到错误。若要防止出现错误,请改用签名中的 any
关键字。
ngOnChanges(changes: any) {
console.log(changes.myInput.currentValue);
}
编辑:
正如 Jon 在下面指出的那样,在使用括号表示法而不是点表示法时,您可以使用SimpleChanges
签名。
ngOnChanges(changes: SimpleChanges) {
console.log(changes['myInput'].currentValue);
}
@Input() set categoryId(categoryId: number) {
console.log(categoryId)
}
请尝试使用此方法。希望这有帮助
最安全的选择是使用共享服务而不是@Input
参数。此外,@Input
参数不会检测复杂嵌套对象类型的更改。
一个简单的示例服务如下:
服务网
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);
}
}
组件.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);
}
}
<小时 />您可以在其他组件中使用类似的实现,并且您的所有组件将共享相同的共享值。
Angular ngOnChanges
ngOnChanges()
是一个内置的 Angular 回调方法,如果至少有一个属性已更改,则在默认更改检测器检查数据绑定属性后立即调用该方法。在视图和内容之前,将检查子项。
// 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);
}
}
更多信息: 角度文档
我只想补充一点,还有另一个名为 DoCheck
的生命周期钩子,如果@Input
值不是基元值,它很有用。
我有一个数组作为Input
所以当内容更改时不会触发OnChanges
事件(因为 Angular 所做的检查是"简单"的并且不深入,所以数组仍然是一个数组,即使数组上的内容已更改(。
然后,我实现一些自定义检查代码来决定是否要使用更改的 Array 更新我的视图。
我会坚持@alan-c-s建议的方法,但有一些修改。首先 - 我反对使用ngOnChanges
.相反,我建议将所有需要更改的内容移动到一个对象下。并使用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>
在这里,ngOnChanges将始终在输入属性更改时触发:
ngOnChanges(changes: SimpleChanges): void {
console.log(changes.categoryId.currentValue)
}
您还可以 ,有一个可观察量,该可观察量会在父component(CategoryComponent)
中的更改时触发,并在子组件的订阅中执行您想要执行的操作。( videoListComponent
(
服务.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
});
}
此解决方案使用代理类并提供以下优点:
- 允许消费者利用 RXJS 的强大功能
- 比迄今为止提出的其他解决方案更紧凑
- 比使用
ngOnChanges()
更安全 - 您可以通过这种方式观察任何类字段。
用法示例:
@Input()
num: number;
@Input()
str: number;
fields = observeFields(this); // <- call our utility function
constructor() {
this.fields.str.subscribe(s => console.log(s));
}
实用功能:
import { BehaviorSubject, Observable, shareReplay } from 'rxjs';
const observeField = <T, K extends keyof T>(target: T, key: K) => {
const subject = new BehaviorSubject<T[K]>(target[key]);
Object.defineProperty(target, key, {
get: () => subject.getValue() as T[K],
set: (newValue: T[K]) => {
if (newValue !== subject.getValue()) {
subject.next(newValue);
}
}
});
return subject;
};
export const observeFields = <T extends object>(target: T) => {
const subjects = {} as { [key: string]: Observable<any> };
return new Proxy(target, {
get: (t, prop: string) => {
if (subjects[prop]) { return subjects[prop]; }
return subjects[prop] = observeField(t, prop as keyof T).pipe(
shareReplay({refCount: true, buffer:1}),
);
}
}) as Required<{ [key in keyof T]: Observable<NonNullable<T[key]>> }>;
};
演示
export class ChildComponent implements OnChanges {
@Input() categoryId: string;
ngOnChanges(changes: SimpleChanges) {
if (changes.categoryId) { // also add this check
console.log('Input data changed:', this.categoryId);
}
}
}
当输入发生更改时,更改检测将调用ngOnChanges。 更改:SimpleChanges 对象已完成所有更改。检查 categoryId 是否是发生的更改之一。如果是,请执行您需要执行的操作。
您也可以将事件发射器作为输入传递。不太确定这是否是最佳实践...
CategoryComponent.ts:
categoryIdEvent: EventEmitter<string> = new EventEmitter<>();
- OTHER CODE -
setCategoryId(id) {
this.category.id = id;
this.categoryIdEvent.emit(this.category.id);
}
类别组件.html:
<video-list *ngIf="category" [categoryId]="categoryIdEvent"></video-list>
在VideoListComponent.ts中:
@Input() categoryIdEvent: EventEmitter<string>
....
ngOnInit() {
this.categoryIdEvent.subscribe(newID => {
this.categoryId = newID;
}
}
没有一个答案是最好的解决方案。即使它们最终起作用,它们也过于复杂。
您需要做的就是在输入变量上使用 get-set 模式,也称为"自动属性",如下所示:
...
private _categoryId: number;
@Input()
set categoryId(value: number) {
this._categoryId = value;
// Do or call whatever you want when this input value changes here
}
get categoryId(): number {
return this._categoryId;
}
...
然后在代码中的其他任何地方,都可以将其引用为this.categoryId
因为当在 TypeScript 中引用它时会自动调用 getter。
延伸阅读:
- https://www.typescripttutorial.net/typescript-tutorial/typescript-getters-setters/
- https://dev.to/deerawan/3-different-ways-to-use-input-decorator-4b3c
- https://angular.io/guide/component-interaction#intercept-input-property-changes-with-a-setter
您可以在门面服务中使用BehaviorSubject
,然后在任何组件中订阅该主题,并在事件发生时触发数据调用更改.next()
。请确保在销毁生命周期挂钩内关闭这些订阅。
data-api.facade.ts
@Injectable({
providedIn: 'root'
})
export class DataApiFacade {
currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);
}
some.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();
}
基本上,两种建议的解决方案在大多数情况下都可以正常工作。我对ngOnChange((的主要负面体验是缺乏类型安全性。
在我的一个项目中,我做了一些重命名,之后一些魔弦保持不变,当然这个错误需要一些时间才能浮出水面。
设置器没有这个问题:您的 IDE 或编译器会通知您任何不匹配。
您可以使用 ngOnChanges(( 生命周期方法
@Input() inputValue: string;
ngOnChanges(changes: SimpleChanges) {
console.log(changes['inputValue'].currentValue);
}
正在处理使用@Input
在父组件和子组件之间共享数据的情况,则可以使用生命周期方法检测@Input
数据更改: ngOnChanges
ngOnChanges(changes: SimpleChanges) {
if (!changes.categoryId.firstChange) {
// only logged upon a change after rendering
console.log(changes.categoryId.currentValue);
}
}
我建议你应该关心为子组件实现的变更策略,你应该添加ChangeDetectionStrategy.OnPush,出于某种性能原因:
示例代码:
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VideoListComponent implements OnChanges {
@Input() categoryId: string;
@Input() public set categoryId(id: number) {
if (id) {
console.log(id)
}
}
如果你不想使用ngOnChange
实现 og onChange()
方法,你也可以通过valueChanges
事件等订阅特定项目的更改。
myForm = new FormGroup({
first: new FormControl(),
});
this.myForm.valueChanges.subscribe((formValue) => {
this.changeDetector.markForCheck();
});
markForCheck()
写是因为在此声明中使用:
changeDetection: ChangeDetectionStrategy.OnPush