3

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

7
  • Did you try to call your functions in constructor? Please let me know. Commented Feb 5, 2021 at 6:20
  • I have not I actually dint know you could do that Commented Feb 5, 2021 at 6:22
  • Can you give me any sample of your code in stackblitz? Than it might me helpful for me to check . :) Commented Feb 5, 2021 at 6:23
  • Hmm I can share the Library Stackblitz, Its more or less the same u can consider the addevent() the same thing as my setCalender() stackblitz.com/edit/angular-trrnxc?file=demo/component.ts Commented Feb 5, 2021 at 6:27
  • What does the template look like? Also, you can combine both of the functions by putting the map into a pipe and do the stuff from setCalendar in subscribe. Also, you don't need async/await, getAventry + subscribe is already asynchronous. Commented Feb 5, 2021 at 6:29

2 Answers 2

3
+50

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.

Sign up to request clarification or add additional context in comments.

7 Comments

Hmm I get what your going for here with the parent and child component, And your very right generally this would be handled in a store, But that is a later integration sadly.. What im curious about is why are u passing entries in between? And where do u pass in the entries for setCalendar(entries) Entry and Events are 2 different tyypes in my case Entry: EntryModel And events : CalendarEvent, So I tried what u mentioned I get no data returns then.. But I might be a bit confused here These nested Component Parent/Child Input Output relations thing seems pretty interesting
If you call getEntries() in ngOnInit() then when template is generated, this.entries in smart component will be initialised, so then [entries]="entries | async" will fire and dumb component will receive values. For setCalendar I just used functional approach - map(this.setCalendar), because setCalendar is a pure function. The same would be with - map((entries) => this.setCalendar(entries))
Btw @Bitman You example is correct, Its just that the logic is a bit a swing here, And I think I have confused u, As IM noticing your Returning the entries When the whole question is about me trying to get this.events to be filled , Im trynig to understand what your doing here but its a bit confusing, As Im passing entires to the dumb component but what do I do with it there? As thing that needs to render is a calendar that reads from the events array, Should I just change the arrays around ? And then on the smart component do I have 2 asyncs? Entries and events?
Updated my question a little bit with what variables im using maybe that clears it up a bit more
Okay I think I have this running correctly, Just getting "core.js:4197 ERROR Error: InvalidPipeArgument: '' for pipe 'AsyncPipe'" Im asuming for the entries fetch
|
1

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.

6 Comments

I would extract the "fetch and process" logic to a service rather to a component to improve reusability and testability and use the Observable solutions as it is more the Angular style ;-)
@DanielPerales Such is for sure the plan, I am just attempting to understand whats happening and the process of it all, I am literally just making a app to understand angular havent read a book or watched a course just winging it, So once I have a decent enough grasp of everything I definitely plan to separate logic and actually implement this in a better way and im going to try implementing your solution I do think Bitmans answer is probably the correct approach I just fail to understand it.
@RJM I think im a bit confused about how some of the observables work, As, Id like to approach it that way as that is the correct way. Should my Entires Be of type observable? As entries: Entrymodel[] = []; Currently, So if Heres the 2 problems I encounter with these approaches. When using the Async Function with a type It expects Properties on getEntries() And when it comes to the Loading Display, It usually Just loads forever It reaches the Load bool But nothing changes, Is this ebcause its not an observable or because I return on the Fetch from get entries? the e => { return{ part
I would recommend you doing a RxJS tutorial or course before moving on with Angular, to me one of the more common pitfalls when learning Angular is not getting into the RxJS thing which may cause bad design decisions and performance issues in your app. I have suffered it hehe.
I think xD that would be for the best more then likely I can see my self already have designed this in a terrible state already, Ive read threw the docs but ill have to watcha. full course on this
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.