Angular - Wait until I receive data before loading template

Viewed 148469

I have a component that renders several components dynamically, with this template:

<div [saJquiAccordion]="{active: group.value['collapsed']}" *ngFor="let group of filterGroupsTemplate | keysCheckDisplay;">
    <div>
        <h4>{{group.key | i18n}}</h4>
        <form id="ibo-{{group.key}}" class="form-horizontal" autocomplete="off" style="overflow: initial">
            <fieldset *ngFor="let field of group.value | keys">
                <ng-container *ngComponentOutlet="fieldSets[field.value.template];
                                    ngModuleFactory: smartadminFormsModule;"></ng-container>
            </fieldset>
        </form>
    </div>
</div>

The thing is that the data needed to fill those components I'm getting it from an API call:

      this.getFiltersSubscription = this.getFilters().subscribe(
            (filters) => {
                this.filters = filters;
                log.info('API CALL. getting filters');

                // Sending data to fieldform components
                this.iboService.updateIBOsRankList(filters['iboRank'].data);
                this.iboService.updateIBOsNewsletterOptions(filters['iboNewsletter'].data);
                this.iboService.updateIBOsTotalOrders(filters['iboTotalOrders'].data);
            }
        );

So, once I have my data, I'm triggering a service Observable which my components are subscribed to, and they will then process the gathered data.

PROBLEM

If the API call is made before all components load, I'll be triggering these service methods passing data but nobody will be subscribed to those Observables.

An approach would be to:

Load data first, and only when I have the data loaded, I'll render the template and, therefore, render all these components dynamically and only then I'll be triggering these service methods (Observables).

I don't want to make an API call for each component, because it can be like 60 components, I'll rather loose abstraction fo code but I prefer to do something like this:

// Listens to field's init and creates the fieldset triggering a service call that will be listened by the field component
        this.iboService.initIBOsFilters$.subscribe(
            (fieldName) => {
                if (fieldName === 'IBOsRankSelectorFieldComponent') {
                    log.data('inside initIBOsFilters$ subscription, calling updateIBOsFilters()', fieldName);
                    this.iboService.updateIBOsRankList(this.filters['iboRank'].data); // HERE I'M PASSING DATA TO THE COMPONENT RENDERED DYNAMICALY. BUT IF this.filters IS UNDEFINED, IT BREAKS
                }
            }
        );

In order to do this, I need to ensure that this.filters is defined and thus, I come to conclusion:

How can I wait until API call ends and this.filters is defined before rendering my template html?

Sorry if my question is a bit long, if you need any more details just let me know.

Thanks!

6 Answers
<p class="p-large">{{homeData?.meta[0].site_desc}}</p>

Just Used a "?" after the variable that has been getting loaded with data from the server.

home.component.ts

import { Component, OnInit } from '@angular/core';
import { HomeService } from '../services/home.service';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
  public homeData: any;
  constructor(private homeService: HomeService) {}

  ngOnInit(): void {
    this.homeService.getHomeData().subscribe( data => {
      this.homeData = data[0];
    }, error => {
      console.log(error);
    });
  }
}

Alternative to async would be creating variable isLoading = true. While it´s true show spinner provided by angular instead of content using *ngIf. When your subscription triggers, assign isLoading = false. This way you will not receive any errors about missing data since content that is using it technically won´t exist yet.

Easiest solution would be to wrap it in ngIf, i don't think this is a good practice but it resolve the problem.

In this case error is: Title of undifined

<div *ngIf="form">
  {{form.Title}}
</div>

I know this is quite late to the party, but what works for me is adding a loading boolean to my state in my reducer.

reducer.ts:

export interface State {
 things: Stuff;
 loaded: boolean;
}
export const reducers: ActionReducerMap<State> = {
    things: reducer,
    loaded: loadedReducer,
};

then make sure to export the loaded function so it toggles to true when the state comes back

export function loadedReducer(state: boolean = false, action: ThingActions): boolean {
    switch (action.type) {
        case ThingActionTypes.GetThings:
            return true;
    }
    return state;
}

then in your ts subscribe to the loaded.

parent.component.ts:

this.loaded$ = this.store.pipe(select(fromReducer.loaded));

and use this like an async pipe in your template.

parent.component.html:

        <ng-container *ngIf="(loaded$ | async); else loading">
            <child-component></child-component>
        </ng-container>
        <ng-template #loading></ng-template>
Related