SrAxi
SrAxi

Reputation: 20005

Angular - Wait until I receive data before loading template

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!

Upvotes: 61

Views: 191068

Answers (6)

nab
nab

Reputation: 597

Edit: this is not the correct nor the best way but:

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 undefined

Markup:

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

Upvotes: 0

Max Tuzenko
Max Tuzenko

Reputation: 1349

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.

Upvotes: 1

Papa_D
Papa_D

Reputation: 11

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>

Upvotes: 0

Jyotirmay
Jyotirmay

Reputation: 1825

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

Upvotes: 12

SrAxi
SrAxi

Reputation: 20005

After studying the different approaches that people gave me, I found the solution on the async pipe. But, it took me a while to understand how to implement it.

Solution:

// Declaring the Promise, yes! Promise!
filtersLoaded: Promise<boolean>;

// Later in the Component, where I gather the data, I set the resolve() of the Promise
this.getFiltersSubscription = this.getFilters().subscribe(
    (filters) => {
        this.filters = filters;
        log.info('API CALL. getting filters');

        this.filtersLoaded = Promise.resolve(true); // Setting the Promise as resolved after I have the needed data
    }
);

// In this listener triggered by the dynamic components when instanced,
// I pass the data, knowing that is defined because of the template change

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

In the template, I use the async pipe that needs an Observable or a Promise

<div *ngIf="filtersLoaded | async">
    <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>
</div>

NOTE:

  • async pipe need an Observable or a Promise from what I understood, that's why the only way to make it work was by creating a Promise
  • I didn't use the resolver approach because it's used when you arrive to the component through Angular's routing. This component is part of a larger component and it's not instanced through routing like any other normal component. (Tried that approach though, worked a bit with it, didn't do the job)

Upvotes: 74

Jan B.
Jan B.

Reputation: 6450

You could use a resolver to ensure those data are loaded (or your filters have been initialized) before the route is activated.

https://blog.thoughtram.io/angular/2016/10/10/resolving-route-data-in-angular-2.html

https://angular.io/api/router/Resolve

Upvotes: 19

Related Questions