2

Please note: I know there is APP_INITIALIZER but this seems to work only on the top-level module (app.module.ts) of the application.

I have as parent component which loads some data:

In admin-area.component.ts:

ngOnInit(): void {

  forkJoin([
    this._userService.getMe(),
    this._placeService.getAll()
    ])
    .pipe(finalize(() => this.afterInit()))
    .subscribe(
      ([user, businesses]) => {
        this._appState.user = user;
        this._appState.businesses = businesses;
        this.afterInit();
      }
    );
}

The problem is that there are child components which rely on this._appState.user and this._appState.businesses.

I cannot load this with APP_INITIALIZER because the above data is only loaded, if the user is logged in. The "logged-in" area is a lazy-loaded module on it own.

So, the question is rather simple:

How can I make sure my data is loaded before child components try to display themselves?

This:

providers: [
  AdminAreaState,
  { provide: APP_INITIALIZER, useFactory: adminAreaInit, deps: [AdminAreaState], multi: true}
]

Does not work. adminAreaInit() is not getting called. This only works for my top-level module app.module.ts. So, APP_INITIALIZER does not seem to work on any other module, is that corrent?

What are my options here?


I think I need to provide some more details.

Note, that I am already trying to use *ngIf:

<div *ngIf="!business?.servicePlan">
  Please Upgrade
</div>

but the problem is that if I navigate to this particular page and refresh the page, business is always undefined. business is a result of finding the right business in the Array of businesses using the businessId.

At the time the child component gets loaded, I load additional data. In this case some reviews (comments).

Now, loadPage() loads one page of my requrested data for a business. _appState is a service which is supposed to be loaded first. If I do this:

  private loadPage() {
    console.log(this._appState);
    console.log(this._appState.businesses);
    this._appState.businesses.forEach(
      (b) => {
        console.log(b);
      });
    setTimeout(() => this._appState.businesses.forEach(
      (b) => {
        console.log(b);
      }
    ), 100);

    const businessId = this._appState.businessId;
    this.business = this._appState.businesses.find(b => b.id === businessId);
  }

This is what I get:

enter image description here

As you can see this._appState has businesses but this._appState.businesses does print an empty array.

_appState.businesses is just an Observable:

ApplicationState service:

@Injectable()
export class ApplicationState {

  public businessIdChange;

  public businessesChange;

  private _user = new BehaviorSubject<UserModel>(null);

  private _businessId = new BehaviorSubject<number>(null);

  private _businesses = new BehaviorSubject<Array<BusinessModel>>([]);

  constructor() {
    this.businessIdChange = this._businessId.asObservable();
    this.businessesChange = this._businesses.asObservable();
  }

  set user(value: UserModel) {
    this._user.next(value);
  }

  get user(): UserModel {
    return this._user.getValue();
  }

  set businessId(businessId: number) {

    if (businessId === this.businessId) {
      return;
    }

    this._businessId.next(businessId);
  }

  get businessId() {
    return this._businessId.getValue();
  }

  set businesses(value) {
    this._businesses.next(value);
  }

  get businesses(): Array<BusinessModel> {
    return this._businesses.getValue();
  }

}

I have no idea why I see this and I thought pre-loading the data would make sense anyway. But maybe I have a different issue here?

Stefan Falk
  • 23,898
  • 50
  • 191
  • 378
  • 1
    Is there something to prevent to use `*ngIf="_appstate_user"` in the template where the child components are loaded ? – edkeveked Aug 14 '19 at 08:27
  • Or https://stackoverflow.com/questions/51191231/module-equivalent-of-app-initializer-in-angular – SGalea Aug 14 '19 at 08:29
  • @SGalea Can this work with `forkJoin` ? – Stefan Falk Aug 14 '19 at 08:41
  • I would use the technique @edkeveked suggested or resolvers. – SGalea Aug 14 '19 at 08:43
  • @SGalea Actually, this is what I am already doing but it is a problem when I refresh the page on that particular route. I added more details to my question. – Stefan Falk Aug 14 '19 at 09:08
  • 1
    This application might need some refactoring however you can solve this issue quickly
    Please Upgrade
    Read about Smart/Dumb components and OnPush change detection it will make your code very elegant and easier to reason about.
    – SGalea Aug 14 '19 at 09:12
  • @SGalea Not a beautiful solution but a workaround. I agree. This requires some refactorization. The thing here is that one can manage multiple businesses and I messed up the shared state of the parent component a little bit right there. – Stefan Falk Aug 14 '19 at 09:15

2 Answers2

2

Use Angular Resolvers on component level. Check this for more information.

  • I just tried this. It only works in the sense of "the data gets loaded" but the child is still rendered before the data has completely arrived. – Stefan Falk Aug 14 '19 at 08:52
  • @displayname can you show me your resolver? Usually Resolvers return with resolve() an Observable which is resolved before the component is rendered so the data is available –  Aug 14 '19 at 09:05
  • Since my question is already quite huge I pasted it here: https://pastecode.xyz/view/227132a3 I am just returning the Observable from `forkJoin`. – Stefan Falk Aug 14 '19 at 09:13
  • 1
    @displayname do not return a Subscription. It has to be an Observable! Set the data in a pipe with tap or something. Like this: https://pastecode.xyz/view/ab506506 –  Aug 14 '19 at 09:14
  • Ah, I guess I need to use `exhaustMap` like here: https://stackoverflow.com/a/50575875/826983 – Stefan Falk Aug 14 '19 at 09:18
  • If you could expand your answer a little bit s.t. it contains the relevant code I'd be happy to accept it. – Stefan Falk Aug 14 '19 at 09:21
0

If the child component are displayed by routing, then using resolvers might be a good option. Here is an example of doing so in the doc

If the parent component is simply calling the child component by using the child component tag, then *ngIf will be a good option - something like this

<child-component *ngIf="condition"></child-component>

Last option, would be to use ngOnInit to initialize the data in the child component.

edkeveked
  • 17,989
  • 10
  • 55
  • 93
  • I updated my quesiton. `*ngIf` is already my weapon of choice but it breaks as I refresh the page. In this case, the data is not loaded and my `condition` is `true`. (the condition asks for *not null*) – Stefan Falk Aug 14 '19 at 09:09
  • Maybe you need to use this condition instead `"business && !business?.servicePlan"` – edkeveked Aug 14 '19 at 09:57
  • This was suggested in another comment. I tried it but this only works in 50% of the time. – Stefan Falk Aug 14 '19 at 09:59
  • I think you need to tune that condition to satisfy your need. – edkeveked Aug 14 '19 at 10:20