Load component only after finishing HTTP requests

Asked

Viewed 4,308 times

4

My application uses angular-6-json-schema-form to create and manage forms dynamically. Now I would like the select Component, for example have your data coming from some REST source:

My attempts:

  • Forkjoin: even with forkJoin, the component loads first the data and the component does not work properly.

  • Viewchild (without *ngIf in the component, of course): as the get HTTP requests have not yet finished, it bursts hundreds of errors that even lock the application, if you use *ngIf, viewChild will always be undefined.

Jsonschema

    {
      "title": "A registration form",
      "type": "object",
      "required": [
        "users"
      ],
      "properties": {

        "users": {
          "type": "string",
          "title": "Usuarios"
        },
        "todo": {
          "type": "string",
          "title": "Tarefas"
        }
      }
    }

Uischema/Form:

    {
      "metadados": {
        "ui:widget": "select",
        "options": {
          "titleMap": [],
          "placeholder": "Selecione",
          "source": "https://jsonplaceholder.typicode.com/users&field=users",
          "columnName": "username",
          "columnValue": "id",
          "isAuthenticated": false
        }
      },
      "repos": {
        "ui:widget": "select",
        "options": {
          "titleMap": [],
          "placeholder": "Selecione",
          "source": "https://jsonplaceholder.typicode.com/users&field=todo",
          "columnName": "title",
          "columnValue": "id",
          "isAuthenticated": false
        }
      }
    }

my engine-form.component.ts:

    ngOnInit() {

      const sub = this.route.paramMap.pipe(
        switchMap(params => this.service.geByCTIformItemId(params.get('form_id'), params.get('form_item_id'))),
        map(res => {

          this.formData = this.loadResource(res);

          const uiSchemaObj = JSON.parse(res.ui_schema);

          if (this.hasSource(res.ui_schema)) {

            const depedencies = this.loadDependenciesFromUiSchema(res.ui_schema);

            depedencies.subscribe((dependencyResponse: any) => {

              dependencyResponse.map((item: any) => {

                const widItem = uiSchemaObj[item.field];
                const columnName = widItem.options.columnName;
                const columnValue = widItem.options.columnValue;

                widItem.options.titleMap = item['data'].map(itemRes => {
                  return {
                    value: itemRes[columnValue],
                    name: itemRes[columnName],
                  };
                });

                this.formSchema = {
                  schema: JSON.parse(res.json_schema),
                  form: uiSchemaObj,
                  formData: this.formData,
                };
              });
            });

          }
          this.formActive = true;
          return res;
        }),
      );
      sub.subscribe();
    }

    private loadResource(res: any) {
      const row = {};

      if (this.route.snapshot.routeConfig.path !== ':form_id/novo') {

        const allowed = Object.keys(res.data).filter(f => !RegExp('^__').test(f));

        allowed.forEach(i => {
          row[i] = res.data[i];
        });
      }
      return row;
    }

    private loadDependenciesFromUiSchema(uiSchema: any): Observable<any[]> {

      const uiSchemaObj = JSON.parse(uiSchema);

      const uiItems = Object.keys(uiSchemaObj).filter(item => !item.includes(':'));
      const arr: Observable<any>[] = [];

      uiItems.map(uiItem => {
        const widItem = uiSchemaObj[uiItem];

        if (widItem['ui:widget'] === 'select') {

          // tem requisicao dinamica:
          if (this.hasValidDynamic(widItem.options)) {

            const req = this.http.get(widItem.options.source);
            arr.push(req);

          }
        }
      });
      return forkJoin(arr);
    }

    private hasSource(uiSchema: string) {
      return uiSchema.includes('source');
    }

    private hasValidDynamic(options: any): boolean {
      return typeof (options.source) !== 'undefined'
        && typeof (options.columnName) !== 'undefined'
        && typeof (options.columnValue) !== 'undefined';
    }

engine-form.component.html

        <json-schema-form framework="bootstrap-4"
                          loadExternalAssets="true"
                          autoUpdateContent="true"
                          *ngIf="formActive"
                          [form]="formSchema"
                          [options]="jsonFormOptions"
                          (onChanges)="onChanges($event)"
                          [widgets]="yourNewWidgets">
        </json-schema-form>
  • Wouldn’t it be the case if you put the functions inside the page Onload? type $(Document),ready(Function(){ your routine});

  • 1

    I think it would be better even using ngIf, in which case the viewchild would also only be available after the data return. 'Cause you need the viewchild?

  • @Eduardovargas, in fact, not precise, were only desperate attempts to try to work.. I just need the component to be loaded only when all requests are complete. When it’s just a component for example, it works, but it’s not the common scenario of my application.

  • Make an ajax friend and in Success you put the content.

  • Guys, I’m using angle 7..

  • 1

    @Samueldiogo post after the solution that worked. I found interesting the first 2.

  • already see here Cacio! Thanks!

Show 2 more comments

4 answers

6


In my opinion, you have two options, if you want to load the server data before showing the components, you can use the Router Resolve, which ensures data loading before loading the component. The documentation is on: https://angular.io/api/router/Resolve. There is also an example in the angular documentation: https://angular.io/guide/router#resolve-pre-fetching-Component-data

Route Resolve

  • On the route, set the(s) resolve to be used:
const ROUTES: Routes = [
  {
    path: '', 
    component: DashboardComponent, 
    resolve: {AdminUser: AdminResolver, funcionario: FuncionarioResolver}
  }
]

Implement the resolve, implement the interface Resolve, of @angular/route:

...
import { Resolve } from '@angular/router';
...
export class FuncionarioResolver implements Resolve<Funcionario> {
  constructor(
    private loginService: LoginService,
    private portalRHAPI: PortalRHAPIService) { }

  resolve() : Observable<Funcionario> | Observable<never> {
    return this.loginService.userInfo().pipe(
      mergeMap(user => this.portalRHAPI.getFuncionario(user.codFuncionario))
    )      
  }
}
  • In the onInit component, in this case my Dashboard, i can obtain the data previously loaded by resolve:
constructor(private router: Router) {}

ngOnInit() {
    this.route.data
        .subscribe( (data: {AdminUser: boolean}) => {
        if ( data.AdminUser ) {          
            this.router.navigate([ '/admin/funcionarios' ])
        } else {
            this.isResolvingAdmin = false
        }
        })

    this.route.data
        .subscribe( (data: {funcionario: Funcionario}) => {        
        this.funcionario = data.funcionario[0]
        this.isLoadingResults = true
        this.checkPasswordExpired()        
        this.getFuncionarioDetails()
    })   
}

Combine Latest

This is another way, use this operator when you need to expect the result of more than one operation asincrona, look at the example:

private gettingApiData = new BehaviorSubject<boolean>(false)  
public isGettingApiData$ = this.gettingApiData.asObservable()

private getApiData() {
  this.gettingApiData.next(true)

  const lastId$ = this.alvosService.getLastId()
  const repSorteado$ = this.alvosService.getRandRepres()
  const estaticos$ = this.alvosService.getEstaticos()

  combineLatest(lastId$, repSorteado$, estaticos$, (lastId, repSorteado, estaticos) => ({lastId, repSorteado, estaticos}))
     .subscribe( result => {
        this.id = +result.lastId+1            
        this.repSorteado = result.repSorteado
        this.origem = result.estaticos.origem;
        this.grupos = result.estaticos.grupos;
        ...
        this.gettingApiData.next(false) 
    })
}
  • In HTML, use observable isGettingData$, in the ngIf ( in my example I show a progressbar until the screen is ready )

<div *ngIf="isGettingApiData$ | async; else showForm">
  <mat-progress-spinner mode="indeterminate"></mat-progress-spinner>
</div>

<ng-template #showForm>
   ... aqui vai seu formulario
</ng-template>

6

The Http get method is an asynchronous method, i.e., after starting its execution, the application starts a new thread for it, and continues the default execution of its other methods. It is very useful since http methods tend to take longer than other methods run within the application, because they require a connection time and so on. But it can really be a big problem when you need the data brought by the API for the display. one way around the problem is by asking the application to wait for the method to finish before proceeding. A Typescript solution may be the following:

In service class:

async exemploService(): Promise<Dado[]>{
    const listaDados: Dado[] = [];
    const dadosApi = await this.httpClient.get<any[]>(endpoint).toPromise().then(dataApi=> {
      dataApi.forEach(dado => {
                      listaDados.push(new Dado());
       });
    }
    );

    return listaDados;
}

In the component:

dados: Dado[];

constructor(private service: ExemploService);

async ngOnInit() {
    await this.service.exemploService().then(data => {
      this.dados = data;
     });

    //Métodos que utilizarão os dados da API na inicialização da view
  }

I recommend further research on asynchronous methods and async/await Keywords, as well as the Promise class and then method.

  • tense, to actually solve, I used your approach, but it only worked when I put a setTimeout of 1 second kkkkkkkkk

2

You can do something like this

loading=true

ngOnInit(){
   seuService.fazerRequest(response=>{
   //suaLogica
   loading=false;    
})

Html

<componente *ngIf="!loading">
  • Ola Duardo, thank you for answering, see that ```*ngIf="formActive"`````; makes a similar check, modified to stay as your suggestion, but did not work..

  • tries to put this.formActive = true inside the subscribe in the first line of the same

  • You can even assign false to formActive directly in your statement, so it starts as false. The only other point in your code that you should change formActive from false to true is at the last callback of the last request.

0

Using Promise.all() for cases where several components have different data sources, which was my case-problem.

First I had to go through all of us from the object form to find which ones need to call http GET.

Finally I wait for Promise.all() to finish so I can only display the dynamic form.

follows a demo and below the core of the solution:

private populateForm() {
    const formid = this.route.snapshot.paramMap.get('form_id');
    const formItemId = this.route.snapshot.paramMap.get('form_item_id');

    this.service.geByCTIformItemId(formid, formItemId).toPromise().then(response => {
    this.formActive = false;
    this.formData = this.loadResource(response);


    const onInits = [];

    this.form = JSON.parse(response.ui_schema);
    this.schema = JSON.parse(response.json_schema);

    if (this.form && this.hasSource(response.ui_schema)) {
        // traverse strategy
        const traverse = (item) => {

        if (item.items) {
            item.items.map(traverse);

        } else if (item.tabs) {
            item.tabs.map(traverse);
        }


        if (item.options) {
            if (item.options.source) {
            onInits.push(this.callRestService(item.key, item.options));
            }
        }
        };
        if (Array.isArray(this.form)) {
        this.form.map(traverse);

        Promise.all(onInits)
            .then(() => {
            this.form.map((item: { options: any; }) => {
                // deleto options pois o Angular6-json-schema-form se perde ao montar o titleMap
                delete item.options;
            });
            // monta form:
            this.ativaForm();
            })
            .catch(error => {
            // error handling and message display
            });
        } else {
        this.ativaForm();
        }
    } else {
        this.ativaForm();
    }
    });


}

private callRestService(key: string, options: any) {
    if (!this.hasValidDynamic(options)) return null;

    return this.http.get(options.source).toPromise().then((res: any) => {
    if (res.data) {

        const mapList = res.data.map((item: any) => {

        return {
            value: item[options.columnValue],
            name: item[options.columnName],
        };
        });

        if (key.includes('[]')) {

        const parentKey = key.split('[]')[0];
        const parent = this.form.find((item: { key: string; }) => item.key === parentKey);
        const child = parent.items.find((item: { key: string; }) => item.key === key);
        delete child.options;
        child.titleMap = mapList;

        } else {
        const itemIndex = this.form.findIndex((item: { key: string; }) => item.key === key);
        this.form[itemIndex]['titleMap'] = mapList;
        }

    }
    });
}

Browser other questions tagged

You are not signed in. Login or sign up in order to post.