简体   繁体   English

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

[英]Angular - When receiving @Input, how to wait for other async data in the children component before executing the @Input logic

PROBLEM INTRODUCTION问题介绍

I have button-address child component which onInit loads a list of mapItems:我有按钮地址子组件,它onInit加载一个 mapItems 列表:

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

The value of this.dataSource.data is set inside the switchMap() operator. this.dataSource.data的值在switchMap()操作符内设置。 This value is important because later the user will select elements from the list.这个值很重要,因为稍后用户将从列表中选择元素。 So I will listen for the click events from the user, find the right element of this.dataSource.data and update the selected item.所以我会监听来自用户的点击事件,找到this.dataSource.data的正确元素并更新所选项目。

My issue is that when the app inits, I also receive an @Input from another stream whose aim is to programatically select the appropiate item in the list of 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);
        }
    }

THE BUGGY LINE越野车

However, because the request to the store takes some time, when the @Input (which by the way is also an observable) is received by the component, the code inside toggleSelectedPoiZoi() cannot find the appropiate item as this.dataSource.data is still empty:但是,由于对store的请求需要一些时间,当组件接收到@Input (顺便说一下也是一个 observable)时, toggleSelectedPoiZoi() 中的代码找不到合适的项目,因为this.dataSource.data是还是空的:

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

THE QUESTION问题

How can I make my @Input() wait for the component to load the data for this.dataSource.data before executing this.toggleSelectedPoiZoi() ?如何让我的@Input()在执行this.toggleSelectedPoiZoi()之前等待组件加载this.dataSource.data的数据? This issue only happens during the app init.此问题仅在应用程序初始化期间发生。

THINGS I HAVE TRIED我尝试过的事情

  1. Await for the observable to load the data等待observable 加载数据
    @Input()
    set proximityReportPoiZoi(poiZoi: ProximityReportPoiZoi) {
            const updatePoiZoiSeletion = async () => {
                await this.refreshDataList().toPromise();
                this.toggleSelectedPoiZoi(poiZoi.id);
            }
            updatePoiZoiSeletion();
    }

But it never gets to execute the following line with this.toggleSelectedPoiZoi() method.但是它永远不会使用this.toggleSelectedPoiZoi()方法执行以下行。 If I change .toPromise() for .subscribe() , the value of this.dataSource.data is still an empty array so no item can be selected.如果我将.toPromise()更改为.subscribe() ,则this.dataSource.data的值仍然是一个空数组,因此无法选择任何项目。

  1. ngOnChanges ngOnChanges

However no success as although I can listen for the changes on the value of the @Input , I cannot listen for changes in this.dataSource.data .但是没有成功,因为虽然我可以监听@Input值的变化,但我无法监听this.dataSource.data 的变化。

  1. Get the value of the @Input() by subscribing it at a later stage in the parent component, in ngAfterViewInit() cycle.通过在父组件的稍后阶段在ngAfterViewInit()循环中订阅它来获取@Input()的值。

Angular complains that because the @Input is passed through the template, the value of proximityReportPoiZoi$ property has changed after parent component initialization. Angular 抱怨,因为@Input是通过模板传递的,在父组件初始化后, proximityReportPoiZoi$属性的值发生了变化。 See the parent template:查看父模板:

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

Any help is highly appreciated beforehand :)事先非常感谢任何帮助:)

Accd.附件to Angular life-cycle hooks event sequence, ngOnChanges would be triggered before ngOnInit .对于 Angular 生命周期钩子事件序列, ngOnChanges将在ngOnInit之前ngOnInit But calling the entire subscription in the ngOnChanges might lead to performance issues since from docs :但是在ngOnChanges调用整个订阅可能会导致性能问题,因为来自docs

Note that this happens very frequently, so any operation you perform here impacts performance significantly.请注意,这种情况经常发生,因此您在此处执行的任何操作都会显着影响性能。

So what you could do is use the @Input variable inside the ngOnInit directly in the subscription.所以你可以做的是使用@Input内部变量ngOnInit直接在订阅。

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

I propose an approach based on pure RxJs logic which does not relay on the Angular lifecycle methods.我提出了一种基于纯 RxJs 逻辑的方法,它不依赖于 Angular 生命周期方法。

If I understand right, you have 2 streams at play如果我理解正确,你有 2 个流在玩

  • the stream whose source is this.getDataList() and whose notification is used to set this.dataSource.data - let's call this stream obs_1其源为this.getDataList()且其通知用于设置this.dataSource.data的流 - 我们将此流称为 obs_1
  • the stream that emits the value which is passed to the proximityReportPoiZoi Input and which is used to select the the initial value of the list of values - let's call this stream obs_2发出值的流,该值传递给proximityReportPoiZoi报告PoiZoi输入并用于选择值列表的初始值 - 我们将此流称为 obs_2

The problem here is to make sure that we do the following steps in strict temporal order这里的问题是确保我们按照严格的时间顺序执行以下步骤

  • receive the notification from obs_2接收来自obs_2的通知
  • subscribe to obs_1 and process its notification subscribe obs_1并处理其通知
  • process the notification of obs_2 after the notification of obs_1 has been processed.obs_1的通知处理完毕后,处理obs_2的通知。

In terms of pure rxJs logic, this is a good case for using concatMap .就纯 rxJs 逻辑而言,这是使用concatMap的好例子。

The logic could look like this逻辑可能是这样的

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

You can try to implement this approach within your button-address Component.您可以尝试在button-address组件中实现这种方法。 This could be accomplished implementing obs_2 as a Subject which emits within the proximityReportPoiZoi set method.这可以通过将obs_2实现为在proximityReportPoiZoi PoiZoi设置方法内发出的Subject来实现。

The approach I would use though is to try to isolate all this logic into a service which is injected into the button-address Component.我想,虽然使用的方法是尝试所有这一切的逻辑隔离到一个service被注入到button-address组件。 This service knows both obs_1 and obs_2 and therefore can implement this logic in the pipe and expose it as a public API Observable, something like thisservice知道obs_1obs_2 ,因此可以在pipe实现此逻辑并将其公开为公共 API Observable,类似这样

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 Component would need to subscribe tp listAndSelectedVal$ to perform its job. button-address组件需要订阅 tp listAndSelectedVal$才能执行其工作。

It is possible also to explore the use of combineLatest in case obs_1 and obs_2 can emit more than once before completing.也可以探索combineLatest的使用,以防obs_1obs_2在完成之前可以发出多次。

Such an approach would make testing much easier, since testing a service is simpler than testing a Component.这种方法会使测试更容易,因为测试服务比测试组件更简单。

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

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