Angular Setting an Array[] before the view renders

Viewed 403

Currently i'm trying to figure out a way to fill this array events: CalendarEvent[] = []; Before the view renders. As i'm trying to fill a calendar from some database entries I have, But I already have the data pulled In one function then I assign in another. The issue being that I cant seem to get the array to update before the render but can easily update it with a button click event afterwords.

So im diving into the different ways to approach this, Ive tried things like setting a loader and not rendering with a ngIf until the function is done, But when it comes to angular alot of this is still a miss for me, So im unsure what possible approaches there are in regards to this.

As I understand it currently the way my service returns the entries it just returns a promise and not the values so the view doesn't have the data, So I understand there are ways for me to manipulate the component state, But are there ways to achieve this without having to refresh the component after the render

I thought something like this would work but as I understand Async in Angular is different from that in c#/Xamarin ect

async ngOnInit(){
    await this.getEntries();
    await this.setCalendar();
  }

I fetch the entries:

  getEntries(){
    this.entryservice.getAventry().subscribe(data => {
      this.entries = data.map(e => {
        return {
          id: e.payload.doc.id,
          ...e.payload.doc.data() as {}
        } as Entrymodel;
      })
    });
    console.log("Get Done");
  }

I then push them to the calendar array:

 setCalendar(){
     this.entries.forEach(element => {
       
      let start = element.startDate.year + "-" + element.startDate.month + "-" + element.startDate.day;
      let end = element.endDate.year + "-" + element.endDate.month + "-" + element.endDate.day;

      var startDate = new Date(start); 
      var endDate = new Date(end); 
   
      var title = element.info + " " + element.surname;
       this.events = [
        ...this.events,
        {
          title: title ,
                                                                                                                                                       
          start: startOfDay(startDate),
          end: endOfDay( endDate),
          color: {
            primary: element.color,
           secondary: element.color
          },
          draggable: false,
          resizable: {
            beforeStart: false,
            afterEnd: false,
          },
          cssClass: element.surname,
        },
      ];
    
    });

The Calendar Library StackBlitz More or less same thing StackBlitz

Please note: The Entires Array is Where I get the data from the database, The issue is the events: CalenderEvent[] = []; Is not getting filled before the view render So the calendar is empty And the only way to currently fill it is with a click event or something of the sorts after the initial render, I have tried the below answers they dont work for me

A small note I seem to have forgotten to add:

The Variables im using:

  entries: Entrymodel[] = [];
 events: CalendarEvent[] = [];

The Entry Model:

export class Entrymodel {
    id: string;
    surname: string;
    color: string;
    startDate: Startdate;
    endDate: Enddate;
    info: string;
    status?: string;
    inbet?: Inbetween[];
    editing?: boolean;
}

export class Startdate{
    day: string;
    month: string;
    year: string;
}

export class Enddate{
    day: string;
    month: string;
    year: string;
}

export class Inbetween{
    day: string;
    month: string;
    year: string;
}

I am trying to get the Entries, As they contain dates that Need to be pushed into the events Array to display on the calendar

2 Answers

Considering this issue as a good example when smart and dumb components are useful.

As you wrote yourself - managing a state is important here and its super easy if you split your component into two - smart and dumb components.

Smart component will prepare all the data while dumb component will render it.

Example

smart.component.ts:

  entries: Observable<any>;
  loading: false;

  getEntries(){
    this.loading = true;
    this.entries = this.entryservice.getAventry().pipe(
      map(data => {
        return data.map(e => {
          return {
            id: e.payload.doc.id,
            ...e.payload.doc.data() as {}
          } as Entrymodel;
        })
      }),
      map(this.setCalendar),
      finalise(() => this.loading = false)
    );
  }

 setCalendar(entries){
     return entries.forEach(element => {
       
      let start = element.startDate.year + "-" + element.startDate.month + "-" + element.startDate.day;
      let end = element.endDate.year + "-" + element.endDate.month + "-" + element.endDate.day;

      var startDate = new Date(start); 
      var endDate = new Date(end); 
   
      var title = element.info + " " + element.surname;
       this.events = [
        ...this.events,
        {
          title: title ,
                                                                                                                                                       
          start: startOfDay(startDate),
          end: endOfDay( endDate),
          color: {
            primary: element.color,
           secondary: element.color
          },
          draggable: false,
          resizable: {
            beforeStart: false,
            afterEnd: false,
          },
          cssClass: element.surname,
        },
      ];
    
    });

smart.component.html:

<app-dumb-component
  [entries]="entries | async"
  [loading]="loading"
></app-dumb-component>

dumb-component.ts:

@Input('entries') entries: any[];
@Input('loading'): boolean;

dumb.component.html:

<div *ngIf="!loading; else loading"> ... </div>

<ng-template #loading>
  // spinner
</ng-template>

p.s. here for simplicity I store state inside a smart component (while usually we use stores for this) and approach is in general just an example, since the whole architecture of this should be determined how data flows back and forward.

EDIT

What I just noticed you are you are using a real time database, right? (probably Firebase - will add a tag to your post. Please change it if it is not Firebase). And that changes the concept. With normal database like SQL, when you make a HTTP call it will return you once - response or error. While with real time database, you get a stream, like a web socket. Meaning you must treat that as a stream because it can return you values at any time in the future.

So what you asked - you need to get events based on results on entries stream. And that stream can push values at any time. So what you can do - map entries to events:

smart.component.ts:

  entries: Observable<any>;
  loading: false;

  getEntries(){
    this.loading = true;
    this.events = this.entryservice.getAventry().pipe(
      map(data => {
        return data.map(e => {
          return {
            id: e.payload.doc.id,
            ...e.payload.doc.data() as {}
          } as Entrymodel;
        })
      }),
      map(this.setCalendar),
      finalise(() => this.loading = false)
    );
  }

 setCalendar(entries){
     return entries.map(element => {
       
      let start = element.startDate.year + "-" + element.startDate.month + "-" + element.startDate.day;
      let end = element.endDate.year + "-" + element.endDate.month + "-" + element.endDate.day;

      var startDate = new Date(start); 
      var endDate = new Date(end); 
   
      var title = element.info + " " + element.surname;
       return {
          title: title ,
                                                                                                                                                       
          start: startOfDay(startDate),
          end: endOfDay( endDate),
          color: {
            primary: element.color,
           secondary: element.color
          },
          draggable: false,
          resizable: {
            beforeStart: false,
            afterEnd: false,
          },
          cssClass: element.surname,
        },
      ];
    
    });

Changes in setCalendar function - entries.forEach changed to entries.map and this.events = [ ...this.events, to return

smart.component.html:

<app-dumb-component
  [events]="events | async"
  [loading]="loading"
></app-dumb-component>

Now at this point, loading property will be useful only for first connection because later it will emit immediately.

So I agree with @Bitman's answer that you should look to leverage smart/dumb components. But first let's address your question.

Background

To begin with, I suggest you make the most of typescript to help you figure out what's going wrong. For instance, I can see that getEntries() has no return type, but you are attempting to await it. (You may want to do something like getEntries(): void or getEntries(): Entrymodel[])

The way RxJS works is using Observables which allow you to track value changes asynchronously using .subscribe(). The values inside a subscription are only available in that scope (and cannot be returned) because it uses a callback approach. If you do not want to have to learn RxJS (which I can completely understand) and are more comfortable with async/await, I suggest you use .ToPromise() instead of subscribe because you can await promises. Here is an article that explains promises/callbacks/observables.

The problem

The problem that I can see in your code is that this will not work how you expect:

async ngOnInit(){
    await this.getEntries();
    await this.setCalendar();
}

getEntries() is going to fetch the data from your service, but it is not going to wait for the data to come back before calling setCalendar().

Solutions

There are a number of ways to get the data before calling set calendar:

Using async/await with promises

async getEntries(): Entrymodel[] {
  const data = await this.entryservice.getAventry().toPromise();
  ...  // Do your data manipulation stuff here
  return this.entries;
}

Using observables

ngOnInit() {
 this.getEntries();
 // this.getCalendar(); <-- Moved inside getEntries()
}

getEntries(): void {
  this.entryservice.getAventry().subscribe(data => {
     ...  // Do your data manipulation stuff here and 
     this.getCalendar();
  });
}

If you want to prevent the view from loading until you have the desired data you can do what @Bitman suggested (make sure you define the loading variable and set it as done when the data is ready):

<div *ngIf="!loading; else loading"> ... </div>

<ng-template #loading>
  // spinner
</ng-template>

Conclusion

This is by no means the best way to achieve what you are doing, making proper use of smart/dumb components and properly using observables would be, and you can refer to @Bitman's answer for what that more ideal approach might look like.

This hopefully gives you just enough to get your code working in your current configuration with minimal changes.

Related