Search code examples
javascriptangulartypescriptangular-servicesangular4-router

How to handle data comes late from service?


In my angular application, i am in the need to store the data to an array which will be empty at initial stage.

Example:

someFunction() {

 let array = [];

 console.log("step 1");

 this.service.getRest(url).subscribe(result => { 

   result.data.forEach(element => {

   console.log("step 2");

    array.push(element); // Pushing all the objects comes from res.data     

   });

   console.log("step 3");

 });

   console.log("step 4");

}

Here i have listed down the console.log() with step order.

In which the order while calling the function was,

Step 1 Step 4 Step 2 Step 3

Here after step 1, the step 4 calls and later the step 2.. So if i console.log(array) in place of step 4, it gives again empty array..

But in place of step 2 and 3 it gives value.. Coming out of the service the value is empty.

And hence always i am getting empty value in the array.

Kindly help me to store the data to the variable even though there is a time duration of service call and response coming back.

Tried by modifying code for a long time but couldn't get it worked..

Edit:

I have given below the real time application i am currently working with stackblitz link https://stackblitz.com/edit/angular-x4a5b6-ng8m4z

Here in this demo see the file https://stackblitz.com/edit/angular-x4a5b6-ng8m4z?file=src%2Fapp%2Fquestion.service.ts

Where i am using the service call.. If i put async getQuestions() {}, it is giving error of questions.forEach of undefined

In service.ts

    jsonData: any = [
    {
      "elementType": "textbox",
      "class": "col-12 col-md-4 col-sm-12",
      "key": "project_name",
      "label": "Project Name",
      "type": "text",
      "value": "",
      "required": false,
      "minlength": 3,
      "maxlength": 20,
      "order": 1
    },
    {
      "elementType": "textbox",
      "class": "col-12 col-md-4 col-sm-12",
      "key": "project_desc",
      "label": "Project Description",
      "type": "text",
      "value": "",
      "required": true,
      "order": 2
    },
    {
      "elementType": "dropdown",
      "key": 'project',
      "label": 'Project Rating',
      "options": [],
      "order": 3
    }
  ];

  getQuestions() {

    let questions: any = [];

    //In the above JSON having empty values in "options": [],

    this.jsonData.forEach(element => {
      if (element.elementType === 'textbox') {
        questions.push(new TextboxQuestion(element));
      } else if (element.elementType === 'dropdown') {

        //Need to push the data that comes from service result (res.data) to the options

        questions.push(new DropdownQuestion(element));

        console.log("step 1");

      //The service which  i call in real time..

        // return this.http.get(element.optionsUrl).subscribe(res => {

        //res.data has the following array, Using foreach pushing to elements.options.

      //   [
      //   { "key": 'average', "value": 'Average' },
      //   { "key": 'good', "value": 'Good' },
      //   { "key": 'great', "value": 'Great' }
      // ],

        // res.data.forEach(result => {
          console.log("step 2");
        //   element.options.push(result);
        // });
        // console.log(element.options) give values as the above [
      //   { "key": 'average'...
        console.log("step 3");
                // console.log(element.options) give values as the above [
      //   { "key": 'average'...
        // });
        console.log("step 4");
      //But here console.log(element.options) gives empty 
      }
    });

    return questions.sort((a, b) => a.order - b.order);
  }

Solution

  • The first step if convert your function getQuestion in an Observable.

    Why it is necesary? Because you need call to a this.http.get(element.optionsUrl). This is asyncronous (all http.get return observable). And you need wait to the called is finished to get the data. The good of observable is that inside "subscribe function" you have the data.

    Therefore, we must thinking that the "services return observables, the component subscribe to the services".

    Well, let the issue. The main problem is that we need several calls to http.get. As we know, all the calls to http are asyncronous, so how can be sure that we have all the data (remember that we only has the data into the subscribe function. As we don't want have several subscribe -the best is have no subscribe- in our service, we need use forkJoin. ForkJoin need an array of calls, and return an array of result.

    So the fist is create an array of observable, then we return this array of observable. Wait a moment! we don't want return an array with the options, we want a observables of question. For this, in spite of return the array of observable, we return an object that use this array of observable. I put a simple example at bottom of the response

    getQuestions():Observable<any[]> { //See that return an Observable
    
        let questions: any = [];
    
        //First we create an array of observables
        let observables:Observable<any[]>[]=[];
        this.jsonData.forEach(element => {
          if (element.elementType === 'dropdown') {
            observables.push(this.http.get(element.optionsUrl))
          }
        }
        //if only want return a forkjoin of observables we make
        //return forkJoin(observables)
        //But we want return an Observable of questions, so we use pipe(map)) to transform the response
    
        return forkJoin(observables).pipe(map(res=>
        {  //here we have and array like-yes is an array of array-
           //with so many element as "dowpdown" we have in question
           // res=[
           //      [{ "key": 'average', "value": 'Average' },...],
           //        [{ "key": 'car', "value": 'dog },...],
           // ],
           //as we have yet all the options, we can fullfit our questions
           let index=0;
           this.jsonData.forEach((element) => { //see that have two argument, the 
                                                      //element and the "index"
              if (element.elementType === 'textbox') {
                 questions.push(new TextboxQuestion(element));
              } else if (element.elementType === 'dropdown') {
                   //here we give value to element.options
                   element.option=res[index];
                   questions.push(new DropdownQuestion(element));
                   index++;
              }
           })
           return question
        }))
     }
    

    NOTE: of how convert a function that return a value in observable using "of": Simple example

    import { of} from 'rxjs';
    
    getData():any
    {
       let data={property:"valor"}
       return data;
    }
    getObservableData():Observable<any>
    {
       let data={property:"observable"}
       return of(data);
    }
    getHttpData():Observable<any>
    {
        return this.httpClient.get("myUrl");
    }
    //A component can be call this functions as
    let data=myService.getData();
    console.log(data)
    //See that the call to a getHttpData is equal than the call to getObservableData
    //It is the reason becaouse we can "simulate" a httpClient.get call using "of" 
    myService.getObservableData().subscribe(res=>{
         console.log(res);
    }
    myService.getHttpData().subscribe(res=>{
         console.log(res);
    }
    

    NOTE2: use of forkJoin and map

    getData()
    {
        let observables:Observables[];
    
        observables.push(of({property:"observable"});
        observables.push(of({property:"observable2"});
    
        return (forkJoin(observables).pipe(map(res=>{
            //in res we have [{property:"observable"},{property:"observable2"}]
            res.forEach((x,index)=>x.newProperty=i)
            //in res we have [{property:"observable",newProperty:0},
            //                {property:"observable2",newProperty:1}]
           }))
    }
    

    Update There are other way to do the things. I think is better has a function that return the fullfilled "questions".

    //You have
    jsonData:any=....
    //So you can have a function that return an observable
    jsonData:any=...
    getJsonData()
    {
       return of(this.jsonData)
    }
    //Well, what about to have a function thah return a fullFilled Data?
    getFullFilledData()
    {
       let observables:Observables[]=[];
       this.jsonData.forEach(element => {
          if (element.elementType === 'dropdown') {
             observables.push(this.http.get(element.optionsUrl))
          }
       })
       return forkJoin(observables).pipe(map(res=>
          let index = 0;
          this.jsonData.forEach((element) => {
          if (element.elementType === 'dropdown') {
             element.options = res[index];
             index++;
          }
       })
       return this.jsonData
       }))
    }
    

    In this way you needn't change the component. If you call to getFullfilledData you have (in subscribe) the data

    see a stackblitz