Julian Egner
Julian Egner

Reputation: 281

How to implement ranged inline calendar with angular material?

I want to use material calendar as a ranged inline calendar to display and insert date ranges. When using mat-date-range-picker, this is just working (but not inline). When using mat-calendar, it is working for inline, but not for ranged. But if I pass selectedRangeValue as DateRange instead of Date, the Range is displayed properly.

The only thing still missing is input...

This is the code I use now (abbreviated):

<mat-calendar (selectedChange)="selectedRangeChange($event)"
              [selected]="selectedRangeValue"
>
</mat-calendar>
selectedRangeValue: DateRange<Date> =  new DateRange<Date>(this.selectedValue.begin, this.selectedValue.end);

I have to do this because Saturn Date picker is only supported until Angular 9, and from Angular 10 onwards Material Datepicker supports date ranges. But this "inline date range calendar" I just cannot make work...

I found https://github.com/angular/components/issues/20697 where the same problem is described and a solution was found, but not written down, so that does not help.

I also tried to understand the source code of angular material datepicker, but unfortunately I still don't get it. I would appreciate any help or hints.

Upvotes: 4

Views: 7595

Answers (3)

Pedro Santiago
Pedro Santiago

Reputation: 316

Hy, after some real use case, my solution, is have startDate & endDate as part of component class. In my case have 1 month pre-selected and works very well.

HTML

<mat-calendar 
  [selected]="dateRange" 
  [maxDate]="maxDate" 
  (selectedChange)="selectedChange($event)">
</mat-calendar>

TS

import { Component, OnInit, ViewChild } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { MatCalendar, DateRange, MAT_DATE_RANGE_SELECTION_STRATEGY, DefaultMatCalendarRangeStrategy } from '@angular/material/datepicker';
import { hasValue } from 'common/utils'

@Component({
    selector: 'app-send-report',
    templateUrl: './send-report.component.html',
    styleUrls: ['./send-report.component.scss'],
    providers: [{
        provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
        useClass: DefaultMatCalendarRangeStrategy
    }]
})
export class SendReportComponent implements OnInit {

    startDate: Date;
    endDate: Date;
    maxDate: Date;

    @ViewChild(MatCalendar) calendar: MatCalendar<Date>;

    constructor(){

        this.endDate = new Date();

        this.startDate = new Date(this.endDate);
        this.startDate.setMonth(this.startDate.getMonth() - 1);

        this.maxDate = new Date();
        this.dateRange = new DateRange<Date>(this.startDate, this.endDate);
    }

    ngOnInit(): void { }

    dateRange: DateRange<Date>;

    selectedChange(m: any) {

        if (hasValue(this.startDate) && hasValue(this.endDate)) {
            //both has value, reset range picker interval
            this.dateRange = new DateRange<Date>(m, null);
        }
        else {

            let start = this.dateRange.start,
                end = m;

            if (end < start)
                this.dateRange = new DateRange<Date>(end, start);
            else
                this.dateRange = new DateRange<Date>(start, end);
        }

        this.startDate = this.dateRange.start;
        this.endDate = this.dateRange.end;

    }
}

Upvotes: 0

Lafaa
Lafaa

Reputation: 81

The suggested solution has so much extra code, and more importantly it wasn't working for me. Here's a stripped working version.

HTML

<mat-calendar [selected]="selectedRangeValue"
  (selectedChange)="selectedChange($event)">
</mat-calendar>

TS

import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DateRange } from '@angular/material/datepicker';

@Component({
    selector: 'inline-range-calendar',
    templateUrl: './inline-range-calendar.component.html',
})
export class InlineRangeCalendarComponent {

    @Input() selectedRangeValue: DateRange<Date> | undefined;
    @Output() selectedRangeValueChange = new EventEmitter<DateRange<Date>>();

    selectedChange(m: any) {
        if (!this.selectedRangeValue?.start || this.selectedRangeValue?.end) {
            this.selectedRangeValue = new DateRange<Date>(m, null);
        } else {
            const start = this.selectedRangeValue.start;
            const end = m;
            if (end < start) {
                this.selectedRangeValue = new DateRange<Date>(end, start);
            } else {
                this.selectedRangeValue = new DateRange<Date>(start, end);
            }
        }
        this.selectedRangeValueChange.emit(this.selectedRangeValue);
    }

}

Upvotes: 8

Julian Egner
Julian Egner

Reputation: 281

I found a way to solve this. While I am not sure if this is the best way, I want to provide the solution.

I had to create a new angular component for the inline ranged calendar:

HTML Schema

<mat-calendar
  #calendar
  [selected]="selectedRangeValue"
  (selectedChange)="selectedChange($event)"
>
</mat-calendar>

Typescript

import {Component, EventEmitter, Input, OnInit, Output, ViewChild} from '@angular/core';
import {
  DateRange, DefaultMatCalendarRangeStrategy,
  MAT_DATE_RANGE_SELECTION_STRATEGY, MatCalendar,
} from '@angular/material/datepicker';
import {isNullOrUndefined} from '../../util/is-null-or-undefined';
import moment from 'moment';

@Component({
  // tslint:disable-next-line:component-selector
  selector: 'inline-range-calendar',
  templateUrl: './inline-range-calendar.component.html',
  styleUrls: ['./inline-range-calendar.component.sass'],
  providers: [{
    provide: MAT_DATE_RANGE_SELECTION_STRATEGY,
    useClass: DefaultMatCalendarRangeStrategy
  }]
})
export class InlineRangeCalendarComponent implements OnInit {

  @ViewChild(MatCalendar) calendar: MatCalendar<Date>;

  @Input() selectedRangeValue: DateRange<Date>;
  @Output() selectedRangeValueChange = new EventEmitter<DateRange<Date>>();

  ngOnInit(): void {
 }

selectedChange($event) {
const m = moment($event);

if (!isNullOrUndefined(this.selectedRangeValue.end)) {
  const start = this.selectedRangeValue.start;
  // @ts-ignore the parser thinks that this is a date, but it is a moment.js object, so this will work
  start.set(m.toObject());
  this.selectedRangeValue = new DateRange<Date>(start, undefined);
  this.selectedRangeValueChange.emit(this.selectedRangeValue);
} else {
  const end = (!isNullOrUndefined(this.selectedRangeValue.end)) ? this.selectedRangeValue.end : moment(m);
  // @ts-ignore the parser thinks that this is a date, but it is a moment.js object, so this will work
  end.set(m.toObject());
  // @ts-ignore the parser thinks that this is a date, but it is a moment.js object, so this will work
  this.selectedRangeValue = new DateRange<Date>(this.selectedRangeValue.start, end);
  if (this.selectedRangeValue.end < this.selectedRangeValue.start)  {
    this.selectedRangeValue = new DateRange<Date>(this.selectedRangeValue.end, this.selectedRangeValue.start);
  }
  this.selectedRangeValueChange.emit(this.selectedRangeValue);
}
}

}

Hope this may help someone.

Upvotes: 1

Related Questions