Features

Frontend (Angular 2+)

  • Loading calendar appointments from a database
  • Angular 4 calendar component (week view)
  • Navigator component for date switching
  • Drag and drop event moving
  • Drag and drop event resizing
  • Event deleting using a hover icon
  • Angular 4
  • Angular CLI 1.0
  • Includes a trial version of DayPilot Pro for JavaScript (see License below)

Backend (PHP)

  • Stores appointments in a local SQLite/MySQL database
  • JSON endpoints

Online Demo

License

Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript. Buy a license.

1. Create a New Angular 2 Project

This tutorial uses Angular CLI. It assumes Angular CLI package is installed globally.

Create a new project using Angular CLI:

ng new angular2-calendar-php-frontend

Install DayPilot Pro package from npm.daypilot.org:

npm install https://npm.daypilot.org/daypilot-pro-angular/trial/8.3.2826.tar.gz --save

2. Angular 2+ Calendar Component

angular-2-appointment-calendar-php-mysql-empty.png

We will minimize changes made to the source code generated by Angular CLI (app.module.ts, app.component.ts, app.component.html) and create a special component for the calendar (calendar/calendar.component.ts). This way you can upgrade the project to the latest Angular CLI version easily.

The calendar component will be created using Angular 2 Calendar from DayPilot Pro package. It can be inserted using <daypilot-calendar> tag.

calendar/calendar.component.ts

import {Component, ViewChild} from '@angular/core';
import {DayPilotCalendarComponent} from "daypilot-pro-angular";

@Component({
  selector: 'calendar-component',
  template: `
  <daypilot-calendar #calendar></daypilot-calendar>
  `,
  styles: [``]
})
export class CalendarComponent {
  @ViewChild("calendar") calendar : DayPilotCalendarComponent;


}

Our <calendar-component> will be wrapped in a standalone module, called CalendarModule.

We need to add DayPilotModule to the "imports" section so we can use <daypilot-calendar> element in our component.

calendar.module.ts

import {DataService} from "./data.service";
import {HttpModule} from "@angular/http";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {BrowserModule} from "@angular/platform-browser";
import {NgModule} from "@angular/core";
import {DayPilotModule} from "daypilot-pro-angular";
import {CalendarComponent} from "./calendar.component";
import {CreateComponent} from "./create.component";

@NgModule({
  imports:      [
    BrowserModule,
    FormsModule,
    HttpModule,
    DayPilotModule,
    ReactiveFormsModule
  ],
  declarations: [
    CalendarComponent,
    CreateComponent
  ],
  exports:      [ CalendarComponent ],
  providers:    [ DataService ]
})
export class CalendarModule { }

Now add the new component (<calendar-component>) to app.component.html

app.component.html

<h1>Calendar</h1>
<calendar-component></calendar-component>

It is also necessary to import our CalendarModule in the main AppModule class:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import {CalendarModule} from "./calendar/calendar.module";

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    CalendarModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

3. PHP Backend

Our backend will be implemented in PHP. It will expose a couple of endpoints which will let us load events and also store changes to the database.

Create a new PHP project

Create a new PHP project called angular2-calendar-php-backend. When completed, the backend PHP project will have the following structure:

+ angular2-calendar-php-backend
  + api
    _db.php _db.php backend_create.php backend_delete.php backend_events.php backend_move.php backend_update.php

Running the PHP Backend

We will run the backend using the built-in PHP server:

Linux

php -S 127.0.0.1:8090 -t /home/daypilot/tutorials/angular2-calendar-php-backend

Windows

php.exe -S 127.0.0.1:8090 -t C:\Users\daypilot\tutorials\angular2-calendar-php-backend

Backend Proxy

In order to avoid cross-origin requests from http://localhost:4200 (Angular 2 frontend) and http://localhost:8090 (PHP backend) we will proxy the "/api" directory of the Angular 2 fronted application to the backend server.

Create a new proxy.conf.json file with the following content:

proxy.conf.json

{
  "/api": {
    "target": "http://localhost:8090",
    "secure": false
  }
}

Modify the "start" script command in package.json to use the proxy configuration from proxy.conf.json

package.json

{
  "name": "angular2-calendar-php-frontend",
  // ...
  "scripts": {
    "start": "ng serve --proxy-config proxy.conf.json",
    // ...
  },
  // ...
}

When you start the Angular 2 built-in server using "npm run start" it will automatically configure the proxy:

npm run start

4. Calendar Configuration

angular-2-appointment-calendar-php-mysql-week-view.png

You can configure the calendar using [config] attribute. It points to an object with configuration properties. This object uses the standard properties of the DayPilot.Calendar object.

We will add "calendarConfig" field with two properties:

  • viewType: "Week"
  • startDate: DayPilot.Date.today()

This configuration switches the calendar to week view and displays the current week.

import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core';
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService, MoveEventParams} from "../backend/data.service";
import {CreateComponent} from "../dialogs/create.component";

@Component({
  selector: 'calendar-component',
  template: `
    <daypilot-calendar #calendar [config]="calendarConfig"></daypilot-calendar>
  `,
  styles: [``]
})
export class CalendarComponent {
  @ViewChild("calendar") calendar : DayPilotCalendarComponent;

  calendarConfig = {
    startDate: DayPilot.Date.today(),
    viewType: "Week"
  };

  constructor(private ds: DataService) {  }

}

5. Loading Calendar Appointments from Database

angular-2-appointment-calendar-php-mysql-loading-data.png

The calendar component will display appointment data from an array specified using [events] attribute.

calendar.component.ts

import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core';
import {DayPilot, DayPilotCalendarComponent } from "daypilot-pro-angular";
import {DataService, MoveEventParams} from "../backend/data.service";
import {CreateComponent} from "../dialogs/create.component";

@Component({
  selector: 'calendar-component',
  template: `
    <daypilot-calendar #calendar [events]="events" [config]="calendarConfig"></daypilot-calendar>
  `,
  styles: [``]
})
export class CalendarComponent implements OnInit, AfterViewInit {
  @ViewChild("calendar") calendar : DayPilotCalendarComponent;

  events: any[];

  calendarConfig = {
    startDate: DayPilot.Date.today(),
    viewType: "Week"
  };

  constructor(private ds: DataService) {  }

  ngOnInit(): void {}

  ngAfterViewInit(): void {
    this.ds.getEvents(this.calendar.control.visibleStart(), this.calendar.control.visibleEnd()).subscribe(result => this.events = result);
  }

}

We call the backend from AfterViewInit because we wouldn't be able to get correct visibleStart() and visibleEnd() values earlier. The calendar needs to be initialized and rendered first.

We have used BackendService class to load the data from the server. It's s simple service that loads appointments in JSON format from our PHP backend.

calendar/data.service.ts:

import { Http, Response } from '@angular/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import 'rxjs/Rx';

@Injectable()
export class DataService {

  constructor(private http : Http){
  }

  getEvents(start: DayPilot.Date, end: DayPilot.Date): Observable<EventData[]> {
    return this.http.post("/api/backend_events.php", {start: start, end: end}).map((response:Response) => response.json());
  }

}

Add DataService reference to calendar/calendar.module.ts ("providers" section of @NgModule):

import { DataService } from "./backend/data.service";
// ...

@NgModule({
  // ...
  providers: [
    DataService
  ],
  // ...
})
export class CalendarModule { }

The PHP backend_events.php script returns an array of appointment data in JSON format:

<?php
require_once '_db.php';

$json = file_get_contents('php://input');
$params = json_decode($json);

$stmt = $db->prepare("SELECT * FROM events WHERE NOT ((end <= :start) OR (start >= :end))");
$stmt->bindParam(':start', $params->start);
$stmt->bindParam(':end', $params->end);
$stmt->execute();
$result = $stmt->fetchAll();

class Event {}
$events = array();

date_default_timezone_set("UTC");
$now = new DateTime("now");
$today = $now->setTime(0, 0, 0);

foreach($result as $row) {
    $e = new Event();
    $e->id = $row['id'];
    $e->text = $row['text'];
    $e->start = $row['start'];
    $e->end = $row['end'];
    $events[] = $e;
}

header('Content-Type: application/json');
echo json_encode($events);

?>

Sample JSON response:

[
  {
    "text":"Monday Meeting", 
    "start":"2017-01-02T10:00:00",
    "end":"2017-01-02T13:00:00",
    "id":"lid2lwibsna37ltimymvlivsyq"
  }
]

For the appointment object format please see DayPilot.Event.data in the API docs.

6. Creating Appointments

angular-2-appointment-calendar-php-mysql-create.png

DayPilot Angular 2 Angular supports creating appointments using drag and drop. Time range selecting is enabled by default and it fires onTimeRangeSelected event handler whenever a user selects a time range.

Our onTimeRangeSelected event handler is specified using "calendarConfig" object and it opens a modal dialog for entering appointment details.

import {Component, ViewChild, OnInit, AfterViewInit} from '@angular/core';
import {DayPilot, DayPilotCalendarComponent} from "daypilot-pro-angular";
import {DataService, MoveEventParams} from "../backend/data.service";
import {CreateComponent} from "../dialogs/create.component";

@Component({
  selector: 'calendar-component',
  template: `
    <daypilot-navigator [config]="navigatorConfig" [(date)]="calendarConfig.startDate"></daypilot-navigator>
    <create-dialog #create (close)="createClosed($event)"></create-dialog>
  `,
  styles: [``]
})
export class CalendarComponent implements OnInit, AfterViewInit {
  @ViewChild("calendar") calendar : DayPilotCalendarComponent;
  @ViewChild("create") create: CreateComponent;

  events: any[];

  calendarConfig = {
    startDate: DayPilot.Date.today(),
    viewType: "Week",
    // ...
    onTimeRangeSelected: args => {
      this.create.show(args);
    }
  };
  
  // ...

  createClosed(args) {
    if (args && args.result) {
      this.events.push(args.result);
    }
    this.calendar.control.clearSelection();
  }

}

The modal dialog for entering new appointment details is implemented using DayPilotModalComponent.

calendar/create.component.ts

import {Component, ViewChild, Output, EventEmitter} from "@angular/core";
import {DayPilot, DayPilotModalComponent} from "daypilot-pro-angular";
import {Validators, FormBuilder, FormGroup, FormControl} from "@angular/forms";
import {CreateEventParams, DataService} from "./data.service";

@Component({
  selector: 'create-dialog',
  template: `
    <daypilot-modal #modal (close)="closed()">
    <div class="center">
      <h1>New Event</h1>
      <form [formGroup]="form">
        <div class="form-item">
          <input formControlName="name" type="text" placeholder="Event Name"> <span *ngIf="!form.controls.name.valid">Event name required</span>
        </div>
        <div class="form-item">
          <input formControlName="start" type="text" placeholder="Start"> <span *ngIf="!form.controls.start.valid">Invalid datetime</span>
        </div>
        <div class="form-item">
          <input formControlName="end" type="text" placeholder="End"> <span *ngIf="!form.controls.end.valid">Invalid datetime</span>
        </div>
        <div class="form-item">
          <button (click)="submit()" [disabled]="!form.valid">Create</button>
          <button (click)="cancel()">Cancel</button>
        </div>
    </form>
    </div>
    </daypilot-modal>
  `,
  styles: [`
  .center {
    max-width: 800px;
    margin-left: auto;
    margin-right: auto;
  }
  .form-item {
    margin: 4px 0px;
  }
  `]
})
export class CreateComponent {
  @ViewChild("modal") modal : DayPilotModalComponent;
  @Output() close = new EventEmitter();

  form: FormGroup;
  dateFormat = "MM/dd/yyyy h:mm tt";

  constructor(private fb: FormBuilder, private ds: DataService) {
    this.form = this.fb.group({
      name: ["", Validators.required],
      start: ["", this.dateTimeValidator(this.dateFormat)],
      end: ["", [Validators.required, this.dateTimeValidator(this.dateFormat)]]
    });
  }

  show(args: any) {
    args.name = "";
    this.form.setValue({
      start: args.start.toString(this.dateFormat),
      end: args.end.toString(this.dateFormat),
      name: ""
    });
    this.modal.show();
  }

  submit() {
    let data = this.form.getRawValue();

    let params: CreateEventParams = {
      start: DayPilot.Date.parse(data.start, this.dateFormat).toString(),
      end: DayPilot.Date.parse(data.end, this.dateFormat).toString(),
      text: data.name
    };

    this.ds.createEvent(params).subscribe(result => {
      params.id = result.id;
      this.modal.hide();
      this.close.emit(params);
    });
  }

  cancel() {
    this.modal.hide();
    this.close.emit();
  }

  closed() {
    this.close.emit();
  }

  dateTimeValidator(format: string) {
    return function(c:FormControl) {
      let valid = !!DayPilot.Date.parse(c.value, format);
      return valid ? null : {badDateTimeFormat: true};
    };
  }
}