我有一个父组件(CategoryComponent)、一个子组件(videoListComponent)和一个ApiService。
我的大部分工作都很好,即每个组件都可以访问 json api 并通过 observables 获取其相关数据。
目前视频列表组件只获取所有视频,我想将其过滤为特定类别中的视频,我通过 @Input()
将 categoryId 传递给孩子来实现这一点。
类别组件.html
<video-list *ngIf="category" [categoryId]="category.id"></video-list>
这有效,当父 CategoryComponent 类别发生变化时,categoryId 值通过 @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
演示示例:查看 this plunker
或者,您还可以使用输入属性设置器,如下所示:
private _categoryId: string;
@Input() set categoryId(value: string) {
this._categoryId = value;
this.doSomething(this._categoryId);
}
get categoryId(): string {
return this._categoryId;
}
文档链接:查看 here。
演示示例:查看 this plunker。
您应该使用哪种方法?
如果您的组件有多个输入,那么,如果您使用 ngOnChanges(),您将在 ngOnChanges() 中一次获得所有输入的所有更改。使用这种方法,您还可以比较已更改的输入的当前值和以前的值,并采取相应的措施。
但是,如果您只想在特定的单个输入更改时执行某些操作(并且您不关心其他输入),那么使用输入属性设置器可能会更简单。但是,这种方法没有提供一种内置方法来比较更改输入的先前值和当前值(您可以使用 ngOnChanges 生命周期方法轻松完成)。
编辑 2017-07-25:在某些情况下,角度变化检测可能仍然不会触发
通常,只要父组件更改它传递给子组件的数据,setter 和 ngOnChanges 的更改检测就会触发,前提是数据是 JS 原始数据类型(字符串、数字、布尔值)。但是,在以下情况下,它不会触发,您必须采取额外的措施才能使其工作。
如果您使用嵌套对象或数组(而不是 JS 原始数据类型)将数据从父级传递给子级,则更改检测(使用 setter 或 ngchanges)可能不会触发,正如用户回答中提到的:muetzerich。解决方案看这里。如果您在角度上下文之外(即外部)改变数据,那么角度将不知道这些变化。您可能必须在组件中使用 ChangeDetectorRef 或 NgZone 来从角度感知外部变化,从而触发变化检测。参考这个。
在函数签名中使用 SimpleChanges
类型时,我在控制台以及编译器和 IDE 中遇到错误。为防止出现错误,请改用签名中的 any
关键字。
ngOnChanges(changes: any) {
console.log(changes.myInput.currentValue);
}
编辑:
正如 Jon 在下面指出的,您可以在使用括号表示法而不是点表示法时使用 SimpleChanges
签名。
ngOnChanges(changes: SimpleChanges) {
console.log(changes['myInput'].currentValue);
}
SimpleChanges
界面?
<my-user-details [user]="selectedUser"></my-user-details>
)。通过调用 changes.user.currentValue
从 SimpleChanges 类访问此属性。注意 user
在运行时绑定,而不是 SimpleChanges 对象的一部分
ngOnChanges(changes: {[propName: string]: SimpleChange}) { console.log('onChanges - myProp = ' + changes['myProp'].currentValue); }
@Input() set categoryId(categoryId: number) {
console.log(categoryId)
}
请尝试使用此方法。希望这可以帮助
最安全的选择是使用 @Input
参数的共享 service instead。此外,@Input
参数不会检测复杂嵌套对象类型的变化。
一个简单的示例服务如下:
服务.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);
}
}
组件.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);
}
}
您可以在其他组件中使用类似的实现,并且您的所有组件都将共享相同的共享值。
@Input
参数不会检测复杂嵌套对象类型的变化,但它会检测简单的本机对象。
角 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);
}
}
更多信息:Angular Docs
我只想补充一点,如果 @Input
值不是原始值,还有另一个名为 DoCheck
的生命周期钩子很有用。
我有一个数组作为 Input
,所以当内容更改时这不会触发 OnChanges
事件(因为 Angular 所做的检查是“简单”且不深入,因此数组仍然是一个数组,即使内容数组已更改)。
然后我实现了一些自定义检查代码来决定是否要使用更改后的数组来更新我的视图。
当您的输入属性更改时,这里 ngOnChanges 将始终触发:
ngOnChanges(changes: SimpleChanges): void {
console.log(changes.categoryId.currentValue)
}
您还可以拥有一个触发父 component(CategoryComponent)
更改的可观察对象,并在子组件的订阅中执行您想要执行的操作。 (videoListComponent
)
服务.ts
public categoryChange$ : ReplaySubject<any> = new ReplaySubject(1);
类别组件.ts
public onCategoryChange(): void {
service.categoryChange$.next();
}
视频列表组件.ts
public ngOnInit(): void {
service.categoryChange$.subscribe(() => {
// do your logic
});
}
我会坚持采用@alan-cs 建议的方法,但要进行一些修改。首先 - 我反对使用 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>
此解决方案使用 proxy 类并提供以下优势:
允许消费者利用 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;
}
subject
将始终发出。
你也可以只传递一个 EventEmitter 作为输入。不太确定这是否是最佳实践...
类别组件.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;
}
}
您可以在外观服务中使用 BehaviorSubject
,然后在任何组件中订阅该主题,并在发生事件时触发对它的数据调用 .next()
的更改。确保在 on destroy 生命周期钩子中关闭这些订阅。
数据-api.facade.ts
@Injectable({
providedIn: 'root'
})
export class DataApiFacade {
currentTabIndex: BehaviorSubject<number> = new BehaviorSubject(0);
}
一些.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();
}
您可以使用 ngOnChanges() 生命周期方法
@Input() inputValue: string;
ngOnChanges(changes: SimpleChanges) {
console.log(changes['inputValue'].currentValue);
}
基本上,两种建议的解决方案在大多数情况下都可以正常工作。我对 ngOnChange() 的主要负面体验是缺乏类型安全性。
在我的一个项目中,我做了一些重命名,之后一些魔术字符串保持不变,当然这个错误需要一些时间才能浮出水面。
Setter 没有这个问题:您的 IDE 或编译器会让您知道任何不匹配的情况。
如果您不想使用 ngOnChange
实现 og onChange()
方法,您也可以通过 valueChanges
事件,ETC 订阅特定项目的更改。
myForm = new FormGroup({
first: new FormControl(),
});
this.myForm.valueChanges.subscribe((formValue) => {
this.changeDetector.markForCheck();
});
由于在此声明中使用而编写的 markForCheck()
:
changeDetection: ChangeDetectionStrategy.OnPush
如果您正在处理使用 @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;
doSomething
方法并在实际设置新值之前将 2 个参数作为新值和旧值,另一种方法是在设置之前存储旧值,然后调用该方法。