How to Build an Ionic 5 Calendar with Modal & Customisation Last update: 2020-06-30

How to Build an Ionic 5 Calendar with Modal & Customisation

Having a calendar component in your Ionic app could be an essential element for your whole app experience, and while there’s not a standard solution, there are a lot of free components we can use.

In this post we will use one of the best calendar components for Ionic called ionic2-calendar. Don’t worry about the name - it was updated for all versions and works fine with Ionic 5 as well!

We will integrate the component into our app and also create a simple modal so you could use the calendar sort of like a date picker as well!

Getting Started with the Ionic 5 Calendar

First of all we need a new Ionic app and install the calendar. We also generate another page that we will later use for our modal:

ionic start ionCalendar blank --type=angular
cd ./ionCalendar
npm install ionic2-calendar
ionic g page pages/calModal

To use the calendar, you need to import it into the according page module. If you also want to localisee it for a specific language, you can use the registerLocaleData from Angular and import the language package you need. In my example I simply used the German language, but there are many more available!

Go ahead and change your home/home-module.ts to this now:

import { NgModule, LOCALE_ID } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { FormsModule } from '@angular/forms';
import { HomePage } from './home.page';

import { HomePageRoutingModule } from './home-routing.module';

import { NgCalendarModule  } from 'ionic2-calendar';
import { CalModalPageModule } from '../pages/cal-modal/cal-modal.module';

import { registerLocaleData } from '@angular/common';
import localeDe from '@angular/common/locales/de';
registerLocaleData(localeDe);

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    HomePageRoutingModule,
    NgCalendarModule,
    CalModalPageModule
  ],
  declarations: [HomePage],
  providers: [
    { provide: LOCALE_ID, useValue: 'de-DE' }
  ]
})
export class HomePageModule {}

That’s the only thing you need to do upfront for your calendar component!

Creating a Basic Calendar

To setup the calendar you need an array of events (which you could e.g. load from your API) and some basic settings for the calendar like the mode and the currentDate, which basically marks today in your calendar.

The mode specifies the view of the calendar, and you can select between month, week and day view.

Let’s go through the basic functions we add to our class:

  • next/back: Move the calendar view to the next month/week/day based on the current mode using the viewchild we added
  • onViewTitleChanged: Will be called by the calendar and reflects the current title of your view. This could also be customised with a special formatter as well!
  • onEventSelected: Called when you click on an entry in the calendar. In our case we simply present an alert with the information of the event.
  • createRandomEvents: Fill the calendar with some dummy data for testing, copied from the demo code.
  • removeEvents: Removes all events

You can either completely set the eventSource array to a new value, or otherwise also push single events to the source. We will see how the second part later in combination with our modal.

For now though go ahead and change the home/home.page.ts to:

import { CalendarComponent } from 'ionic2-calendar/calendar';
import { Component, ViewChild, OnInit, Inject, LOCALE_ID } from '@angular/core';
import { AlertController, ModalController } from '@ionic/angular';
import { formatDate } from '@angular/common';
import { CalModalPage } from '../pages/cal-modal/cal-modal.page';

@Component({
  selector: 'app-home',
  templateUrl: 'home.page.html',
  styleUrls: ['home.page.scss'],
})
export class HomePage implements OnInit {
  eventSource = [];
  viewTitle: string;

  calendar = {
    mode: 'month',
    currentDate: new Date(),
  };

  selectedDate: Date;

  @ViewChild(CalendarComponent) myCal: CalendarComponent;

  constructor(
    private alertCtrl: AlertController,
    @Inject(LOCALE_ID) private locale: string,
    private modalCtrl: ModalController
  ) {}

  ngOnInit() {}

  // Change current month/week/day
  next() {
    this.myCal.slideNext();
  }

  back() {
    this.myCal.slidePrev();
  }

  // Selected date reange and hence title changed
  onViewTitleChanged(title) {
    this.viewTitle = title;
  }

  // Calendar event was clicked
  async onEventSelected(event) {
    // Use Angular date pipe for conversion
    let start = formatDate(event.startTime, 'medium', this.locale);
    let end = formatDate(event.endTime, 'medium', this.locale);

    const alert = await this.alertCtrl.create({
      header: event.title,
      subHeader: event.desc,
      message: 'From: ' + start + '<br><br>To: ' + end,
      buttons: ['OK'],
    });
    alert.present();
  }

  createRandomEvents() {
    var events = [];
    for (var i = 0; i < 50; i += 1) {
      var date = new Date();
      var eventType = Math.floor(Math.random() * 2);
      var startDay = Math.floor(Math.random() * 90) - 45;
      var endDay = Math.floor(Math.random() * 2) + startDay;
      var startTime;
      var endTime;
      if (eventType === 0) {
        startTime = new Date(
          Date.UTC(
            date.getUTCFullYear(),
            date.getUTCMonth(),
            date.getUTCDate() + startDay
          )
        );
        if (endDay === startDay) {
          endDay += 1;
        }
        endTime = new Date(
          Date.UTC(
            date.getUTCFullYear(),
            date.getUTCMonth(),
            date.getUTCDate() + endDay
          )
        );
        events.push({
          title: 'All Day - ' + i,
          startTime: startTime,
          endTime: endTime,
          allDay: true,
        });
      } else {
        var startMinute = Math.floor(Math.random() * 24 * 60);
        var endMinute = Math.floor(Math.random() * 180) + startMinute;
        startTime = new Date(
          date.getFullYear(),
          date.getMonth(),
          date.getDate() + startDay,
          0,
          date.getMinutes() + startMinute
        );
        endTime = new Date(
          date.getFullYear(),
          date.getMonth(),
          date.getDate() + endDay,
          0,
          date.getMinutes() + endMinute
        );
        events.push({
          title: 'Event - ' + i,
          startTime: startTime,
          endTime: endTime,
          allDay: false,
        });
      }
    }
    this.eventSource = events;
  }

  removeEvents() {
    this.eventSource = [];
  }

}

With that in place we are ready for a basic view of our calendar.

We add some buttons to later trigger our modal and also the dummy functions to fill and clear the array of events.

Besides that we can use a cool segment from Ionic to easily switch between the three different calendar modes, which works by simply changing the mode property!

Additionally we add the helper functions to move left and right in the calendar (which also simply works by sliding the view!) and in the center the title of the current view that we see, which will be updated once we change to another month (or week, or day..).

The calendar itself has a lot of properties that we can or should set. The ones we use are:

  • eventSource: The array of events which will be displayed
  • calendarMode: The view mode as described before
  • currentDate: The today date
  • onEventSelected: Called when we click on a day with event
  • onTitleChanged: Triggered when we change the view
  • start/step: Additional settings for the different views

Now we can go ahead with our home/home.page.html and change it to:

<ion-header>
  <ion-toolbar color="primary">
    <ion-title>
      Ionic Calendar
    </ion-title>
    <ion-buttons slot="end">
      <ion-button (click)="openCalModal()">
        <ion-icon name="add" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-buttons>
  </ion-toolbar>
</ion-header>

<ion-content>
  <ion-segment [(ngModel)]="calendar.mode">
    <ion-segment-button value="month">
      <ion-label>Month</ion-label>
    </ion-segment-button>
    <ion-segment-button value="week">
      <ion-label>Week</ion-label>
    </ion-segment-button>
    <ion-segment-button value="day">
      <ion-label>Day</ion-label>
    </ion-segment-button>
  </ion-segment>

  <ion-row>
    <ion-col size="6">
      <ion-button (click)="createRandomEvents()" expand="block" fill="outline">
        Add random events
      </ion-button>
    </ion-col>
    <ion-col size="6">
      <ion-button (click)="removeEvents()" expand="block" fill="outline">
        Remove all events
      </ion-button>
    </ion-col>
  </ion-row>

  <ion-row>
    <!-- Move back one screen of the slides -->
    <ion-col size="2">
      <ion-button fill="clear" (click)="back()">
        <ion-icon name="arrow-back" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>

    <ion-col size="8" class="ion-text-center">
      <h2>{{ viewTitle }}</h2>
    </ion-col>

    <!-- Move forward one screen of the slides -->
    <ion-col size="2">
      <ion-button fill="clear" (click)="next()">
        <ion-icon name="arrow-forward" slot="icon-only"></ion-icon>
      </ion-button>
    </ion-col>
  </ion-row>

  <calendar
    [eventSource]="eventSource"
    [calendarMode]="calendar.mode"
    [currentDate]="calendar.currentDate"
    (onEventSelected)="onEventSelected($event)"
    (onTitleChanged)="onViewTitleChanged($event)"
    startHour="6"
    endHour="20"
    step="30"
    startingDayWeek="1"
  >
  </calendar>

</ion-content>

The standard calendar should work now, and you can also see how it looks per default with some random events!

Adding Events with Calendar Modal

Although the component is meant for something else, I thought it might look cool inside a custom modal as some sort of date picker - and it does!

Therefore we will add a new function to our page that presents our modal and catches the onDidDismiss event, in which we might get back a new event that we then add to our eventSource.

Here we can see the different of pushing an event - we also need to call loadEvents() on the calendar component in order to update the data!

Since we assume that we add an all-day event in our example, I manually set the end time to one day later here. Feel free to use custom times or add another ion-datetime to the modal in your example to pick an exact time!

Now go ahead and change the home/home.page.ts to:

async openCalModal() {
  const modal = await this.modalCtrl.create({
    component: CalModalPage,
    cssClass: 'cal-modal',
    backdropDismiss: false
  });

  await modal.present();

  modal.onDidDismiss().then((result) => {
    if (result.data && result.data.event) {
      let event = result.data.event;
      if (event.allDay) {
        let start = event.startTime;
        event.startTime = new Date(
          Date.UTC(
            start.getUTCFullYear(),
            start.getUTCMonth(),
            start.getUTCDate()
          )
        );
        event.endTime = new Date(
          Date.UTC(
            start.getUTCFullYear(),
            start.getUTCMonth(),
            start.getUTCDate() + 1
          )
        );
      }
      this.eventSource.push(result.data.event);
      this.myCal.loadEvents();
    }
  });
}

We’ve also added a custom class to the modal to make it stand out more like an overlay. To use the styling, we need to declare it inside the global.scss like this:

.cal-modal {
    --height: 80%;
    --border-radius: 10px;
    padding: 25px;
}

Now we got a nice looking modal and only need some more input fields and the calendar component once again.

Like before, we need to add it first of all to the module file, so in our case change the cal-modal/cal-modal.module.ts to:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { IonicModule } from '@ionic/angular';

import { CalModalPageRoutingModule } from './cal-modal-routing.module';

import { CalModalPage } from './cal-modal.page';

import { NgCalendarModule  } from 'ionic2-calendar';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    IonicModule,
    CalModalPageRoutingModule,
    NgCalendarModule
  ],
  declarations: [CalModalPage]
})
export class CalModalPageModule {}

Since we now don’t want to display any events, we need only a handful of functions.

We need an empty event object that will be passed back to our initial page inside the dismiss() function of the modal, and we need to react to the view title changes and click events on a time slot.

To store it correctly, we also need to transform the data we get inside the onTimeSelected() to a real date object!

Now open the cal-modal/cal-modal-page.ts and change it to:

import { Component, AfterViewInit } from '@angular/core';
import { ModalController } from '@ionic/angular';

@Component({
  selector: 'app-cal-modal',
  templateUrl: './cal-modal.page.html',
  styleUrls: ['./cal-modal.page.scss'],
})
export class CalModalPage implements AfterViewInit {
  calendar = {
    mode: 'month',
    currentDate: new Date()
  };
  viewTitle: string;

  event = {
    title: '',
    desc: '',
    startTime: null,
    endTime: '',
    allDay: true
  };

  modalReady = false;

  constructor(private modalCtrl: ModalController) { }

  ngAfterViewInit() {
    setTimeout(() => {
      this.modalReady = true;
    }, 0);
  }

  save() {
    this.modalCtrl.dismiss({event: this.event})
  }

  onViewTitleChanged(title) {
    this.viewTitle = title;
  }

  onTimeSelected(ev) {
    this.event.startTime = new Date(ev.selectedTime);
  }

  close() {
    this.modalCtrl.dismiss();
  }
}

The last missing piece is the modal view with some items as input fields and the calendar component just like before, but this time without any events!

You might have noticed the strange modalReady variable already. I couldn’t make the calendar work correctly when deployed to a device, since I assume the view wasn’t rendered correctly and something internally broke.

By hiding it until the view has finished loading we can easily fix this, and in reality you actually won’t even notice it!

Warp up the implementation with the code for the cal-modal/cal-modal.page.html:

<ion-toolbar color="primary">
  <ion-buttons slot="start">
    <ion-button (click)="close()">
      <ion-icon name="close" slot="icon-only"></ion-icon>
    </ion-button>
  </ion-buttons>
  <ion-title>{{ viewTitle }}</ion-title>
  <ion-buttons slot="end">
    <ion-button (click)="save()">
      <ion-icon name="checkmark" slot="icon-only"></ion-icon>
    </ion-button>
  </ion-buttons>
</ion-toolbar>

<ion-content>
  <ion-item>
    <ion-label position="stacked">Title</ion-label>
    <ion-input tpye="text" [(ngModel)]="event.title"></ion-input>
  </ion-item>
  <ion-item>
    <ion-label position="stacked">Description</ion-label>
    <ion-input tpye="text" [(ngModel)]="event.desc"></ion-input>
  </ion-item>

  <calendar
    *ngIf="modalReady"
    [calendarMode]="calendar.mode"
    [currentDate]="calendar.currentDate"
    (onTitleChanged)="onViewTitleChanged($event)"
    (onTimeSelected)="onTimeSelected($event)"
    lockSwipes="true"
  >
  </calendar>
</ion-content>

Now you are able to open the modal, set a date and some information and our initial page will create the event and add it to the source for the calendar!

Customising the Calendar View

Because the last time we used the component a lot of you asked about customisation, let’s add a few more things today.

First, we might wanna get rid of the list below the month view in our modal, since the calendar has no events in that place anyway.

There’s no setting to hide it (at least I didn’t find it) but we can hide the list by using the correct CSS class and access the encapsulated calendar component like this in our cal-modal/cal-modal.page.scss:

:host ::ng-deep {
    .monthview-container {
        height: auto !important;
    }

    .event-detail-container {
        display: none;
    }
}

Using CSS is one way to customise it, and another great way to change the appearance of the calendar is using custom templates for specific parts of the calendar.

Let’s for example change the look and color of a cell inside the calendar so it has a more rounded, different color and also shows the number of events on that specific date.

We could do this by creating a template and passing it to the monthviewDisplayEventTemplate property of the calendar like this inside the home/home.page.html:

<calendar
  [eventSource]="eventSource"
  [calendarMode]="calendar.mode"
  [currentDate]="calendar.currentDate"
  (onEventSelected)="onEventSelected($event)"
  (onTitleChanged)="onViewTitleChanged($event)"
  startHour="6"
  endHour="20"
  step="30"
  startingDayWeek="1"
  [monthviewDisplayEventTemplate]="template"
>
</calendar>

<ng-template #template let-view="view" let-row="row" let-col="col">
  <div [class.with-event]="view.dates[row*7+col].events.length">
    {{view.dates[row*7+col].label}}
    <div class="indicator-container">
      <div class="event-indicator" *ngFor="let e of view.dates[row*7+col].events"></div>
    </div>
  </div>
</ng-template>

As you can see we also added our own CSS rules in there, which we could now define either for our own template or additionally some rules for the calendar itself (following the style like we did in the modal) right inside the home/home.page.scss:

.indicator-container {
  padding-left: 0.5rem;
  padding-bottom: 0.4rem;
}

.event-indicator {
  background: var(--ion-color-success);
  width: 5px;
  height: 5px;
  border-radius: 5px;
  display: table-cell;
}

:host ::ng-deep {
  .monthview-primary-with-event {
    background-color: white !important;
  }

  .monthview-selected {
    background-color: var(--ion-color-success) !important;
  }
}

.with-event {
  background-color: var(--ion-color-primary);
  border-radius: 15px;
}

Now the view looks a lot different, and this hopefully gives you an idea how you can customise basically every part of this great component!

Conclusion

There are not a lot of components that exist since years and get updates for all major Ionic versions, but this calendar is one of them. Kudos again to the creator!

If you are looking for an alternative, also check out my post on the Angular calendar.

You can also find a video version of this tutorial below.