twinssbc / Ionic2-Calendar

A calendar component based on Ionic framework
https://ionic-calendar-demo.stackblitz.io
MIT License
388 stars 196 forks source link
calendar ionic ionic2 ionic3 ionic4 ionic5 ionic6 ionic7 ionic8 swiper

Ionic-Calendar directive

Ionic calendar directive

version GitHub License

Table of Contents

  1. Demo
  2. Dependency
  3. Usage
  4. Options
  5. Callback
  6. View Customization Option
  7. EventSource
  8. Performance Tuning
  9. Common Questions

Demo

Version 2.0
https://stackblitz.com/edit/ionic-calendar-demo-2-2?file=src%2Fapp%2Fexample.component.html
Version 1.0
https://stackblitz.com/edit/ionic-calendar-demo-1-0?file=src%2Fapp%2Fexample.component.html
Version 0.x
https://stackblitz.com/edit/ionic-calendar-demo?file=pages%2Fhome%2Fhome.html

Dependency

Calendar Version Ionic Version Angular Version Swiper Version
2.5.x >=8.0.0 >=18.0.0 >=11.0.0
2.4.x >=7.0.0 >=17.0.0 >=11.0.0
2.3.x >=7.0.0 >=17.0.0 >=11.0.0
2.2.x >=7.0.0 >=17.0.0 >=10.1.0
2.1.x >=7.0.0 >=16.0.0 >=10.1.0
2.0.x >=7.0.0 >=16.0.0 [8.4.6, 9.0.0)
1.0.x >=6.1.9 >=15.1.2 [8.4.6, 9.0.0)
0.6.x >=5.1.0 >=9.1.0
0.5.x >=4.0.0-rc.1
0.4.x >=3.9.2
0.3.x >=3.1.1
0.2.9+ >=2.3.0
0.2.x 2.0.0-rc.5
0.1.x [2.0.0-rc.1, 2.0.0-rc.4]

version 0.2-0.4 has below dependency:
intl 1.2.5, due to issue https://github.com/angular/angular/issues/3333

Usage

1. Install Calendar Dependency

npm install ionic2-calendar --save

version 1.0.x onwards

version 1.0.x is also published as Ionic6-Calendar package name. So could also run
npm install ionic6-calendar --save
version 2.0.x is also published as Ionic7-Calendar package name. So could also run
npm install ionic7-calendar --save
version 2.5.x is also published as Ionic8-Calendar package name. So could also run
npm install ionic8-calendar --save

NOTE: Starting from Version 1.0.x, the underlying implementaion is based on Swiper instead of IonSlides, so also needs to install Swiper dependency.

2. Import the Calendar module

If using version 1.0.x, could use both ionic2-calendar or ionic6-calendar, ionic7-calendar.

@NgModule({ declarations: [ MyApp ], imports: [ NgCalendarModule, IonicModule.forRoot(MyApp) ], bootstrap: [IonicApp], entryComponents: [ MyApp ] }) export class AppModule {}


- version 0.1.x - 0.4.x
``` typescript
import { NgModule } from '@angular/core';
import { IonicApp, IonicModule } from 'ionic-angular';
import { MyApp } from './app/app.component';
import { NgCalendarModule  } from 'ionic2-calendar';

@NgModule({
    declarations: [
        MyApp
    ],
    imports: [
        NgCalendarModule,
        IonicModule.forRoot(MyApp)
    ],
    bootstrap: [IonicApp],
    entryComponents: [
        MyApp
    ]
})
export class AppModule {}

If you are using PageModule, you need to import the NgCalendarModule in your page module

import { NgCalendarModule  } from 'ionic2-calendar';

@NgModule({
  declarations: [
    MyPage
  ],
  imports: [
    IonicPageModule.forChild(MyPage),
    NgCalendarModule
  ],
  entryComponents: [
    MyPage
  ]
})
export class MyPageModule {}

3. Add the directive in the html page

    <calendar [eventSource]="eventSource"
        [calendarMode]="calendar.mode"
        [currentDate]="calendar.currentDate"
        (onCurrentDateChanged)="onCurrentDateChanged($event)"
        (onRangeChanged)="reloadSource(startTime, endTime)"
        (onEventSelected)="onEventSelected($event)"
        (onTitleChanged)="onViewTitleChanged($event)"
        (onTimeSelected)="onTimeSelected($event)"
        [step]="calendar.step">        
    </calendar>

Options

        <calendar ... [calendarMode]="calendar.mode"></calendar>

Version 1.0.x onwards

    import { CalendarMode } from 'ionic2-calendar';

    calendar = {
        mode: 'week' as CalendarMode
    };

Version 0.x

    import { CalendarMode } from 'ionic2-calendar/calendar';

    calendar = {
        mode: 'week' as CalendarMode
    };
    import { Step } from 'ionic2-calendar/calendar';

    calendar = {
        step: 30 as Step
    };

Version 1.x
Note: Since swiping is disabled, you could set currentDate or call slideToPrev/slideToNext method to move the calendar to previous/next view. You need to first set the lockSwipes to false, move the slide, then set it back.

    <calendar ... [lockSwipes]="lockSwipes"></calendar>
    moveSlide() {
        this.calendar.lockSwipes = false;
        setTimeout(function() {
            this.myCalendar.slideNext();
            this.calendar.lockSwipes = true;
        },100);
    }

Version 0.x
Note: Since swiping is disabled, you could set currentDate or call slideToPrev/slideToNext method to move the calendar to previous/next view. Do not set lockSwipeToPrev in the constructor phase. It will cause the view not updating when changing the currentDate. You could either set it in some callback function after initialization phase or use setTimeout to trigger some delay.

    <calendar ... [lockSwipes]="lockSwipes"></calendar>
    ngAfterViewInit() {
        var me = this;
        setTimeout(function() {
            me.lockSwipes = true;
        },100);
    }

Callback

CSS Customization

The customized styles should be added in global.scss. Just adding in each components css file may not work due to the View Encapsulation.

Template Customization

Note: For any css class appear in the customized template, you need to specify the styles by yourself. The styles defined in the calendar component won’t be applied because of the view encapsulation. You could refer to calendar.ts to get the definition of context types.

    <ng-template #weekviewNormalEventTemplate let-displayEvent="displayEvent">
        <div class="calendar-event-inner">{{displayEvent.event.title}}</div>
    </ng-template>

    <calendar ... [weekviewNormalEventTemplate]="weekviewNormalEventTemplate"></calendar>
    <ng-template #dayviewNormalEventTemplate let-displayEvent="displayEvent">
        <div class="calendar-event-inner">{{displayEvent.event.title}}</div>
    </ng-template>

    <calendar ... [dayviewNormalEventTemplate]="dayviewNormalEventTemplate"></calendar>
        <ng-template #weekviewAllDayEventSectionTemplate let-day="day" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': day.events}" *ngIf="day.events"
                 [ngStyle]="{height: 25*day.events.length+'px'}">
                <div *ngFor="let displayEvent of day.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: 25*displayEvent.position+'px', width: 100*(displayEvent.endIndex-displayEvent.startIndex)+'%', height: '25px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [weekviewAllDayEventSectionTemplate]="weekviewAllDayEventSectionTemplate"></calendar>
        <ng-template #weekviewNormalEventSectionTemplate let-tm="tm" let-hourParts="hourParts" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': tm.events}" *ngIf="tm.events">
                <div *ngFor="let displayEvent of tm.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: (37*displayEvent.startOffset/hourParts)+'px',left: 100/displayEvent.overlapNumber*displayEvent.position+'%', width: 100/displayEvent.overlapNumber+'%', height: 37*(displayEvent.endIndex -displayEvent.startIndex - (displayEvent.endOffset + displayEvent.startOffset)/hourParts)+'px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [weekviewNormalEventSectionTemplate]="weekviewNormalEventSectionTemplate"></calendar>
        <ng-template #dayviewAllDayEventSectionTemplate let-allDayEvents="allDayEvents" let-eventTemplate="eventTemplate">
            <div *ngFor="let displayEvent of allDayEvents; let eventIndex=index"
                 class="calendar-event" tappable
                 (click)="onEventSelected(displayEvent.event)"
                 [ngStyle]="{top: 25*eventIndex+'px',width: '100%',height:'25px'}">
                <ng-template [ngTemplateOutlet]="eventTemplate"
                             [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                </ng-template>
            </div>
        </ng-template>

        <calendar ... [dayviewAllDayEventSectionTemplate]="dayviewAllDayEventSectionTemplate"></calendar>
        <ng-template #dayviewNormalEventSectionTemplate let-tm="tm" let-hourParts="hourParts" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': tm.events}" *ngIf="tm.events">
                <div *ngFor="let displayEvent of tm.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: (37*displayEvent.startOffset/hourParts)+'px',left: 100/displayEvent.overlapNumber*displayEvent.position+'%', width: 100/displayEvent.overlapNumber+'%', height: 37*(displayEvent.endIndex -displayEvent.startIndex - (displayEvent.endOffset + displayEvent.startOffset)/hourParts)+'px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [dayviewNormalEventSectionTemplate]="dayviewNormalEventSectionTemplate"></calendar>
        <ng-template #weekviewInactiveAllDayEventSectionTemplate let-day="day" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': day.events}" *ngIf="day.events"
                 [ngStyle]="{height: 25*day.events.length+'px'}">
                <div *ngFor="let displayEvent of day.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: 25*displayEvent.position+'px', width: 100*(displayEvent.endIndex-displayEvent.startIndex)+'%', height: '25px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [weekviewInactiveAllDayEventSectionTemplate]="weekviewInactiveAllDayEventSectionTemplate"></calendar>
        <ng-template #weekviewInactiveNormalEventSectionTemplate let-tm="tm" let-hourParts="hourParts" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': tm.events}" *ngIf="tm.events">
                <div *ngFor="let displayEvent of tm.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: (37*displayEvent.startOffset/hourParts)+'px',left: 100/displayEvent.overlapNumber*displayEvent.position+'%', width: 100/displayEvent.overlapNumber+'%', height: 37*(displayEvent.endIndex -displayEvent.startIndex - (displayEvent.endOffset + displayEvent.startOffset)/hourParts)+'px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [weekviewInactiveNormalEventSectionTemplate]="weekviewInactiveNormalEventSectionTemplate"></calendar>
        <ng-template #dayviewInactiveAllDayEventSectionTemplate let-allDayEvents="allDayEvents" let-eventTemplate="eventTemplate">
            <div *ngFor="let displayEvent of allDayEvents; let eventIndex=index"
                 class="calendar-event" tappable
                 (click)="onEventSelected(displayEvent.event)"
                 [ngStyle]="{top: 25*eventIndex+'px',width: '100%',height:'25px'}">
                <ng-template [ngTemplateOutlet]="eventTemplate"
                             [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                </ng-template>
            </div>
        </ng-template>

        <calendar ... [dayviewInactiveAllDayEventSectionTemplate]="dayviewInactiveAllDayEventSectionTemplate"></calendar>
        <ng-template #dayviewInactiveNormalEventSectionTemplate let-tm="tm" let-hourParts="hourParts" let-eventTemplate="eventTemplate">
            <div [ngClass]="{'calendar-event-wrap': tm.events}" *ngIf="tm.events">
                <div *ngFor="let displayEvent of tm.events" class="calendar-event" tappable
                     (click)="onEventSelected(displayEvent.event)"
                     [ngStyle]="{top: (37*displayEvent.startOffset/hourParts)+'px',left: 100/displayEvent.overlapNumber*displayEvent.position+'%', width: 100/displayEvent.overlapNumber+'%', height: 37*(displayEvent.endIndex -displayEvent.startIndex - (displayEvent.endOffset + displayEvent.startOffset)/hourParts)+'px'}">
                    <ng-template [ngTemplateOutlet]="eventTemplate"
                                 [ngTemplateOutletContext]="{displayEvent:displayEvent}">
                    </ng-template>
                </div>
            </div>
        </ng-template>

        <calendar ... [dayviewInactiveNormalEventSectionTemplate]="dayviewInactiveNormalEventSectionTemplate"></calendar>

EventSource

EventSource is an array of event object which contains at least below fields:

    var startTime = new Date(Date.UTC(2014, 4, 8));

Note The calendar only watches for the eventSource reference for performance consideration. That means only you manually reassign the eventSource value, the calendar gets notified, and this is usually fit to the scenario when the range is changed, you load a new data set from the backend. In case you want to manually insert/remove/update the element in the eventSource array, you can call instance method ‘loadEvents’ event to notify the calendar manually.

Instance Methods

import { CalendarComponent } from "ionic2-calendar";

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {
    @ViewChild(CalendarComponent, null) myCalendar:CalendarComponent;
    eventSource;
    …
    loadEvents: function() {
        this.eventSource.push({
            title: 'test',
            startTime: startTime,
            endTime: endTime,
            allDay: false
        });
        this.myCalendar.loadEvents();
    }
}
import { CalendarComponent } from "ionic2-calendar";

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {
    @ViewChild(CalendarComponent, null) myCalendar:CalendarComponent;
    …
    slideNext: function() {
        this.myCalendar.slideNext();
    }
}
import { CalendarComponent } from "ionic2-calendar";

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {
    @ViewChild(CalendarComponent, null) myCalendar:CalendarComponent;
    …
    slidePrev: function() {
        this.myCalendar.slidePrev();
    }
}
import { CalendarComponent } from "ionic2-calendar";

@Component({
    selector: 'page-home',
    templateUrl: 'home.html'
})
export class HomePage {
    @ViewChild(CalendarComponent, null) myCalendar:CalendarComponent;
    …
    slidePrev: function() {
        this.myCalendar.update();
    }
}

Localization

You could use locale option to achieve the localization.
If locale option is not specified, the calendar will use the LOCALE_ID set at the module level.
By default, the LOCALE_ID is en-US. You can override it in the module as below. If you pass undefined, the LOCALE_ID will be detected using the browser language setting. But using explicit value is recommended, as browser has different level of localization support.
Note that the event detail section in the month view doesn't support locale option, only LOCALE_ID takes effect. This is because it uses DatePipe in html directly. You could easily leverage customized event detail template to switch to other locale.

import { NgModule, LOCALE_ID } from '@angular/core';

@NgModule({
    …
    providers: [
        { provide: LOCALE_ID, useValue: 'zh-CN' }
    ]
})

For version 0.4.x+ which depends on Ionic 3.9.2+ and Angular 5.0+, locale module needs to be registered explicitly in module file as below.

import { registerLocaleData } from '@angular/common';
import localeZh from '@angular/common/locales/zh';
registerLocaleData(localeZh);

If you want to change the locale dynamically, you should use locale option instead of LOCALE_ID.

Performance Tuning

In the CPU profile, the default Intl based localization code occupies a big portion of the execution time. If you don’t need localization on certain parts, you can use the custom dateFormatter to override the date transform method. For example, the date in month view usually doesn’t require localization, you could use below code to just display the date part. If the month view day header doesn’t need to include the date, you could also use a string array containing static labels to save the date calculation.

<calendar ... [dateFormatter]="calendar.dateFormatter"></calendar>
calendar = {
    dateFormatter: {
        formatMonthViewDay: function(date:Date) {
            return date.getDate().toString();
        }            
    }
};

Known issue (No longer exists in version 1.0.x)

This component updates the ion-slide dynamically so that only 3 looped slides are needed.
The ion-slide in Ionic2 uses Swiper. It seems in the Swiper implementation, the next slide after the end of looped slide is a separate cached slide, instead of the first slide.
I can't find out a way to force refresh that cached slide, so you will notice that when sliding from the third month to the forth month, the preview month is not the forth month, but the first month.
Once the sliding is over, the slide will be forced to render the forth month.

Common Questions