Angular - 接收@Input时,如何在执行@Input逻辑之前等待子组件中的其他异步数据



问题介绍

我有按钮地址子组件,它在Init 上加载地图项列表:

ngOnInit() {
this.refreshDataList();
}
protected refreshDataList(): void {
this.subscription = this.getDataList()
.pipe(
switchMap((result: AddressModel[]) => {
this.dataSource.data = [...result].map((d) => {
return {
address: d,
selected: this.selectedItems
? this.selectedItems.some(
(it) =>
it.address.pointName ===
d.pointName && it.selected
)
: false,
};
});
this.sortList(this.dataSource.data);
return this.preSelectUserPreferencesPOIs();
})
)
.subscribe();
}
protected getDataList(): Observable<AddressModel[]> {
return this.store.pipe(select(selectZoi));
}

this.dataSource.data的值在switchMap()运算符中设置。此值很重要,因为稍后用户将从列表中选择元素。因此,我将侦听来自用户的点击事件,找到this.dataSource.data的正确元素并更新所选项目。

我的问题是,当应用程序初始化时,我还会收到来自另一个流的@Input,其目的是以编程方式选择this.dataSource.data列表中的适当项目:

@Input()
set proximityReportPoiZoi(poiZoi: ProximityReportPoiZoi) {
this.toggleSelectedPoiZoi(poiZoi.id);
}
protected toggleSelectedPoiZoi(poiZoiId: string) {
const addressZoneSelectionModel = this.dataSource.data.find(
(item) => poiZoiId === item.address.id
);
const address = addressZoneSelectionModel.address;
if (addressZoneSelectionModel.selected) {
addressZoneSelectionModel.selected = false;
this.showHidePoiZoi(address, false);
} else {
addressZoneSelectionModel.selected = true;
this.showHidePoiZoi(address, true);
}
}

越野车线

但是,由于对存储的请求需要一些时间,当组件收到@Input(顺便说一下,这也是一个可观察的)时,toggleSelectedPoiZoi()中的代码找不到适当的项,因为this.dataSource.data仍然是空的:

const addressZoneSelectionModel = this.dataSource.data.find(
(item) => poiZoiId === item.address.id
);

问题

如何让我的@Input() 在执行this.toggleSelectedPoiZoi()之前等待组件加载this.dataSource.data的数据? 此问题仅在应用初始化期间发生。

我尝试过的事情

  1. 等待可观察量加载数据
@Input()
set proximityReportPoiZoi(poiZoi: ProximityReportPoiZoi) {
const updatePoiZoiSeletion = async () => {
await this.refreshDataList().toPromise();
this.toggleSelectedPoiZoi(poiZoi.id);
}
updatePoiZoiSeletion();
}

但它永远不会用这个.toggleSelectedPoiZoi()方法执行以下行。如果我将.toPromise()更改为 .subscribe(),this.dataSource.data的值仍然是一个空数组,因此无法选择任何项目。

  1. ngOnChanges

但是没有成功,因为尽管我可以侦听@Input值的变化,但我无法侦听this.dataSource.data中的更改。

  1. 通过在父组件的稍后阶段(ngAfterViewInit() 循环中订阅它来获取@Input()的值。

Angular 抱怨说,由于@Input是通过模板传递的,因此在父组件初始化后,邻近性ReportPoiZoi$属性的值发生了变化。请参阅父模板:

<app-button-address 
[proximityReportPoiZoi]="(proximityReportPoiZoi$ | async)"
></app-button-address> 

任何帮助都非常感谢事先:)

根据 Angular 生命周期钩子事件序列,ngOnChanges将在ngOnInit之前触发。但是在ngOnChanges中调用整个订阅可能会导致性能问题,因为来自文档:

请注意,这种情况经常发生,因此您在此处执行的任何操作都会显著影响性能。

因此,您可以做的是直接在订阅中使用ngOnInit内的@Input变量。

export someComponent implements OnInit {
_poiZoi: ProximityReportPoiZoi;
@Input()
set proximityReportPoiZoi(poiZoi: ProximityReportPoiZoi) {
this._poiZoi = poiZoi;
}
ngOnInit() {
this.refreshDataList();
}
protected refreshDataList(): void {
this.subscription = this.getDataList().pipe(
switchMap((result: AddressModel[]) => {
this.dataSource.data = [...result].map((d) => {
return {
address: d,
selected: this.selectedItems 
? this.selectedItems.some((it) =>
it.address.pointName === d.pointName && it.selected
) 
: false,
};
});
this.sortList(this.dataSource.data);
this.preSelectUserPreferencesPOIs();
}),
map(() => this._poiZoi.id)
)
.subscribe({
next: (poiZoiId: any) => this.toggleSelectedPoiZoi(poiZoi.id),
error: (error: any) => console.log(error)
});
}
}

我提出了一种基于纯RxJs逻辑的方法,该方法不中继Angular生命周期方法。

如果我理解正确,您有 2 个流在起作用

  • 源为this.getDataList()且其通知用于设置this.dataSource.data的流 - 我们称此流为obs_1
  • 发出传递给proximityReportPoiZoi输入的值并用于选择值列表的初始值的流 - 我们将此流称为obs_2

这里的问题是确保我们按照严格的时间顺序执行以下步骤

  • 接收来自obs_2的通知
  • subscribeobs_1并处理其通知
  • 处理完obs_1通知后处理obs_2通知。

就纯 rxJs 逻辑而言,这是使用concatMap的一个很好的例子。

逻辑可能如下所示

obs_2.pipe(
concatMap(res_2 => obs_1.pipe(
map(res_1 => ([res_1, res_2))
)
)
).subscribe(
([addressModelArray, poiZoi]) => {
setDataSourceData(addressModelArray) // i.e. the logic in switchMap
toggleSelectedPoiZoi(poiZoi.id);
}
)

您可以尝试在button-address组件中实现此方法。这可以实现obs_2作为在proximityReportPoiZoiset 方法中发出的Subject来实现。

不过,我会使用的方法是尝试将所有这些逻辑隔离到注入button-address组件的service中。此service知道obs_1obs_2,因此可以在pipe中实现此逻辑,并将其公开为公共 API 可观察,如下所示

public listAndSelectedVal$ = obs_2.pipe(
concatMap(res_2 => obs_1.pipe(
// apply some logic to filter the default value
filter(res_1 => res_1.length > 0),
map(res_1 => ([res_1, res_2))
)
)
)

button-address组件需要订阅 tplistAndSelectedVal$才能执行其作业。

还可以探索combineLatest的使用,以防obs_1obs_2在完成之前可以多次排放。

这种方法将使测试变得更加容易,因为测试服务比测试组件更简单。

最新更新