Finer control over Angular rendering

247 views Asked by At

I have what should likely be a very common scenario:

I display a busy-wait spinner until the NgRx state has signaled an operation is complete.

In the template:

<my-loading-spinner *ngIf="loading$ | async"></my-loading-spinner>

In the component backing code:

this.loading$ = store.pipe(select(operationStatus), map(loadingFn));

... where the loadingFn function maps the operationState NgRx state value to a boolean.

The result data for this view is loaded into the NgRx state right alongside the operationStatus, in the very same call in the reducer:

return set(
        STATUS, EntityStatus.loadingSuccess, // the operationState
        myEntityAdapter.addAll(action.payload.tokens, state) // the data
      ) as MyEntityState;

This result data is a lengthy object list, that goes through a sort in the component, then is rendered via an *ngFor with non-trivial content.

So it is not surprising that my problem ensues: the busy-wait spinner goes away noticeably before the rendered data table appears on screen.

Is there any type of "some-specific-data-has-finished-rendering" event that could be monitored so that I can make my busy-wait spinner stay around until the data table has completely rendered?

2019.03.01 Update

I attempted the approach suggested by @ConnorsFan : I changed my template to use a new non-observable *ngIf="viewLoading" initialized as public viewLoading = true; Then in the subscription body I simply set it false:

this.dataRows.changes.subscribe(() => {
    this.viewLoading = false;
});

I tried this approach but it did not make a difference; the spinner still was hidden prematurely. Perhaps I did not apply the recipe correctly? I should note that this threw a runtime error as well, the always-challenging ExpressionChangedAfterItHasBeenCheckedError (Previous value: 'ngIf: true'. Current value: 'ngIf: false')

2

There are 2 answers

2
ConnorsFan On

When you have element generated by an ngFor loop:

<div #dataRow *ngFor="let item of items"> ... </div>

you can get the QueryList of elements with ViewChildren and be notified that the elements have been rendered by subscribing to the QueryList.changes event:

import { Component, ViewChildren, QueryList, ElementRef, AfterViewInit } from '@angular/core';
...    
export class MyComponent {

  @ViewChildren("dataRow") dataRows: QueryList<ElementRef>;

  dataRowsRendered: boolean;

  ngAfterViewInit() {
    this.dataRows.changes.subscribe(() => {
      // Do something after the data rows have been rendered
      console.log(`${this.dataRows.length} data rows have been rendered`);
      this.dataRowsRendered = true;
    });
  }

}
0
Reactgular On

I think @ConnorsFan is the correct answer. I didn't think of using @ViewChildren().

You could modify his answer to update the loading$ observable.

@ViewChildren("dataRow") dataRows: QueryList<ElementRef>;

this.loading$ = combineLatest(
     store.pipe(select(operationStatus), map(loadingFn)),
     this.dataRows.changes.pipe(mapTo(this.dataRows.length === 0)
).pipe(map(([loading,rows])=>loading || rows));

I would also recommend adding the style display: none to the ngFor element while it's loading, and then removing it based up loading$. This will remove the display after QueryList emits that it's changed. So when the list is shown it should already exist in the DOM fully rendered.