10

I am using an Angular Material Date Picker application in my project which is a reservation form. What I want is for the user to select the date through the Date Picker. The Date Picker will have a filter that will show which dates are open and which aren't. In order for the Date Picker to know which dates are available it must make a call to one of my Services which returns an Observable. Here is my HTML code for the Date Picker:

<mat-form-field>
  <label>
    <input matInput [matDatepickerFilter]="dateFilter" required [matDatepicker]="picker" placeholder="Choose a date"
           formControlName="date">
  </label>
  <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
  <mat-datepicker #picker></mat-datepicker>
</mat-form-field>

and dateFilter():

dateFilter = (d: Date): boolean => {
    if(monthIsSame(d) {
        ..// have a return statement that process a global variable: 'month: Day[]'
    } else {
        // the user hit the arrow at the top that switches months
        //
        // have a process that sets the 'month' variable to a new array of Days that was gotten 
        // through a RESTful api in one of my services.
        // Somehow there must be a loading wheel here until the result from my server comes back and
        // month is set to the new current month, and then a return statement that process a month
    }
};

The problem is every time the user switches months, the calendar must load the month availability from the backside server, but the code from the server returns an Observable. Somehow the calendar must show a loading wheel until a value comes back from the subscribe method of the observable, and the calendar will get populated with the available dates.

Any help is greatly appreciated, and if I'm coming at this from the completely wrong way, please tell me. Thanks!


Edits:

Note: when I change the return type of my filter to Observable<boolean> it sets all of the dates to be available.

I found the following post. Would it be somehow possible have a callback function that gets called when the user switches months and run my http request and load the results to a local variable. The only problem to this is that somehow the calendar would have to show a loading wheel until the callback function finishes.

Gabe
  • 5,643
  • 3
  • 26
  • 54

3 Answers3

10

How to make mat-calendar to refresh date filter asynchronously:

Solution is to set dateFilter to a new value whenever data gets retrieved from server. Angular is checking this value in its change detection and refreshing the view.

// initial filter function always returns true
dateFilter = (date: Date): boolean => {return true;}

constructor( ... ) {

  this.myDataObs.subscribe( () => {
    // set filter function when new data is available
    this.dateFilter = (date: Date): boolean => { 
      return filterBasedOnDataFromServer(date);
    };
  });
}
pdugic
  • 101
  • 5
2

I'll have a shot at this with a high level plan, let's see if it helps.

You will need a service to pass data between your custom header (details in a minute) and your page component, where you have the dateFilter method. Let's call this service OpenDatesService. I think it should not be provided in a module, because you don't always need it, but it has to have at least as long lifecycle as your page, so ideally it is provided in your page. In that case, you should be able to inject in both the page component, and in you custom header, and those will be the same instance.

So create a custom header. You can completely grab the one from Angular Material, or the example from the docs. I'll explain using this example. In the prev/next callbacks of the header, you don't immediately call the calendar (_dateFormats in the example) but first start a network request, replace the arrows with a spinner (use a flag and some ngIfs), and only re-enable the buttons once the network request has returned. Yes, this is async (observables/promise/async-await), but at this point, your are not forced to do anything synchronously. So after the network request has finished, but still before calling the calendar's prev/method, pass the open dates' data to the OpenDatesService, ie. it should have a method sg. like updateOpenDates. Then you can call the calendar's prev/next method.

After you have called the calendar's prev/next method, your page component's dateFilter will be called soon. But by this time, the OpenDatesService will have the data with the open dates, and the service is injected into the page, so in the dateFilter you can just get it synchronously, and synchronously return the result based on it to the calendar.

So to sum up, there are 2 tricks:

  • move the asyn nature of the network request from the dateFilter method used by the calendar, to the click handler of the header
  • pass the data between the page's dateFilter method and the header.

EDIT:

Now that I think, you don't even need that service to pass data between the custom header and the page component. That is because the custom header is instantiated in the page component's template, where the header can just pass data up to the page with an @Output, and the page can store it in some local fields, which are also accessible by the dateFilter method.

Alex Biro
  • 1,069
  • 1
  • 14
  • 27
-3

I'm unsure on the loading wheel part (maybe put a flag on the component and set it inside of a pipe before and after your service call?)

But in terms of the problem of returning an Observable, I think your problem can be solved with the async pipe:

[matDatepickerFilter]="dateFilter | async"

That will automatically handle subscribing and unsubscribing. Just make sure you put a shareReplay(1) after your API call or else an API call will get triggered on every change detection cycle (which can be pretty often).

Jesse
  • 2,391
  • 14
  • 15
  • When I just add what you say my IDE gives me the error of 'Argument type (d: Date) => boolean is not assignable to parameter type null' and when I run I get the error 'Error: InvalidPipeArgument for pipe 'AsyncPipe'. – Gabe Jan 16 '20 at 02:40