
| Downloads | |
|---|---|
| Repository | |
| Languages | TypeScriptphp |
| Technologies | angularMySQL |
| Code License | Apache License 2.0 (Open-Source) |
Features
Frontend (Angular)
-
Loading calendar appointments from a database
-
Angular 22 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
-
Includes DayPilot Lite for JavaScript
Backend (PHP)
-
The appointments are stored in a local SQLite/MySQL database
-
JSON endpoints
License
Apache License 2.0
1. Add the Angular Appointment Calendar Component
We keep the calendar in a separate CalendarComponent, located at frontend/src/app/calendar/calendar.component.ts. The application shell imports this standalone component. This keeps the calendar logic separate from the Angular application bootstrap.
We build the calendar component using Angular Calendar from the DayPilot Lite open-source calendar/scheduler library. Its default view is a single day, as shown above; the configuration in step 2 selects a week. The calendar is available as the <daypilot-calendar> element. Import DayPilotModule in the standalone component so its template can use the Calendar and Navigator.
import {AfterViewInit, Component, ViewChild, signal} from "@angular/core";
import {DayPilot, DayPilotModule, DayPilotCalendarComponent} from "@daypilot/daypilot-lite-angular";
import {DataService, MoveEventParams} from "./data.service";
import {firstValueFrom} from "rxjs";
@Component({
selector: 'calendar-component',
standalone: true,
imports: [DayPilotModule],
providers: [DataService],
template: `
<div class="calendar-layout">
<daypilot-navigator [config]="navigatorConfig" [(date)]="date"></daypilot-navigator>
<daypilot-calendar #calendar [events]="events" [config]="calendarConfig()"
(viewChange)="viewChange()"></daypilot-calendar>
</div>
@if (error()) { <p role="alert">{{ error() }}</p> }
`,
styles: [`
.calendar-layout { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 20px; }
[role="alert"] { color: #a40000; }
`]
})
export class CalendarComponent implements AfterViewInit {
@ViewChild("calendar") calendar!: DayPilotCalendarComponent;
Add <calendar-component> to frontend/src/app/app.html:
<calendar-component></calendar-component>
The application component in frontend/src/app/app.ts imports CalendarComponent:
import { Component } from '@angular/core';
import { CalendarComponent } from './calendar/calendar.component';
@Component({
selector: 'app-root',
imports: [CalendarComponent],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
}
2. Configure the Week View
You can configure the calendar using the [config] attribute. It receives the current signal value using [config]="calendarConfig()", containing the standard properties of the DayPilot.Calendar object. The calendarConfig signal sets viewType: "Week" and startDate: DayPilot.Date.today(), which displays the current week.
calendarConfig = signal<DayPilot.CalendarConfig>({
startDate: DayPilot.Date.today(),
viewType: "Week",
// ...
});
The standalone Angular application uses signals for the calendar configuration and appointment array. Changing a signal value updates the calendar without manual change detection. The date picker section below shows how to update the start date.
3. Load Calendar Appointments from the Database
The calendar displays appointment data from the array bound using [events]. The frontend uses the same JSON API for MySQL and the default SQLite database, so changing the database provider does not change the calendar code.
events = signal<DayPilot.EventData[]>([]);
Load appointments in ngAfterViewInit() after the Calendar reference becomes available. The (viewChange) callback loads them again whenever the visible date range changes. At this point, visibleStart() and visibleEnd() return the correct bounds. We pass these to the PHP API and assign the response using events.set(). A request counter prevents a slow response for an earlier week from replacing appointments for the current week.
ngAfterViewInit(): void {
this.viewChange();
}
async viewChange(): Promise<void> {
if (!this.calendar) {
return;
}
const version = ++this.loadVersion;
const calendar = this.calendar.control;
try {
const result = await firstValueFrom(this.ds.getEvents(calendar.visibleStart(), calendar.visibleEnd()));
if (version === this.loadVersion) {
this.events.set(result);
}
} catch {
if (version === this.loadVersion) {
this.error.set("Appointments could not be loaded. Check that the PHP server is running.");
}
}
}
The DataService in frontend/src/app/calendar/data.service.ts uses Angular HttpClient to load appointments in JSON format. The component provides this service, and frontend/src/app/app.config.ts enables HTTP requests using provideHttpClient().
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {DayPilot} from "@daypilot/daypilot-lite-angular";
import {HttpClient} from "@angular/common/http";
import EventData = DayPilot.EventData;
import EventId = DayPilot.EventId;
@Injectable()
export class DataService {
constructor(private http: HttpClient) {
}
getEvents(start: DayPilot.Date, end: DayPilot.Date): Observable<EventData[]> {
return this.http.post("/api/backend_events.php", {start: start, end: end}) as Observable<EventData[]>;
}
// ...
}
The PHP backend/api/backend_events.php script returns appointments overlapping the visible range. An event ending exactly at the range start, or starting at its end, is excluded:
<?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();
$events = array();
foreach ($result as $row) {
$e = new stdClass();
$e->id = $row['id'];
$e->text = $row['text'];
$e->start = $row['start'];
$e->end = $row['end'];
$e->barColor = $row['color'];
$events[] = $e;
}
header('Content-Type: application/json');
echo json_encode($events);
Each returned object includes its database id, text, start, end, and barColor. For the appointment object format, see DayPilot.Event.data in the API docs.
4. Create, Move, and Resize Appointments
The Angular Calendar supports creating appointments using drag and drop. Time range selection is enabled by default and fires the onTimeRangeSelected event handler whenever the user selects a time range.
Our handler is specified in calendarConfig. It opens a modal dialog with name, start, end, and color fields. The selected range supplies the initial dates, and the color defaults to blue. Canceling clears the selection without creating a database record.
calendarConfig = signal<DayPilot.CalendarConfig>({
// ...
onTimeRangeSelected: async args => {
const colors = [
{name: "Blue", id: "#3c78d8"},
{name: "Green", id: "#6aa84f"},
{name: "Yellow", id: "#f1c232"},
{name: "Red", id: "#cc0000"}
];
const form = [
{name: "Name", id: "text"},
{name: "Start", id: "start", type: "datetime"},
{name: "End", id: "end", type: "datetime"},
{name: "Color", id: "barColor", type: "select", options: colors}
];
const data = {text: "", start: args.start, end: args.end, barColor: "#3c78d8"};
const modal = await DayPilot.Modal.form(form, data);
this.calendar.control.clearSelection();
if (modal.canceled) {
return;
}
this.error.set("");
try {
const result = await firstValueFrom(this.ds.createEvent(modal.result));
this.events.update(events => [...events, {...modal.result, id: result.id!}]);
} catch {
this.error.set("The appointment could not be saved. Please try again.");
}
}
});
The form uses DayPilot.Modal.form(), a programmatic modal dialog. After the user confirms, createEvent() posts the values to backend_create.php. The backend inserts the record and returns its database ID. Use that returned ID when adding the appointment to the signal array: later move, resize, and delete requests must identify the same database record. A failed request displays an error instead of adding an unsaved appointment.
Moving or resizing an appointment sends its ID and new start/end values to backend_move.php. Both interactions use the same endpoint, because each changes the stored time range:
frontend/src/app/calendar/calendar.component.ts:
calendarConfig = signal<DayPilot.CalendarConfig>({
// ...
onEventMoved: args => {
const params: MoveEventParams = {
id: args.e.id(), newStart: args.newStart, newEnd: args.newEnd
};
this.save(() => firstValueFrom(this.ds.moveEvent(params)));
},
onEventResized: args => {
const params: MoveEventParams = {
id: args.e.id(), newStart: args.newStart, newEnd: args.newEnd
};
this.save(() => firstValueFrom(this.ds.moveEvent(params)));
},
// ...
});
The save() helper reloads stored appointments after these optimistic UI changes. If persistence fails, it displays an error and reloads the database state rather than leaving the failed change silently visible.
private async save(operation: () => Promise<unknown>): Promise<void> {
this.error.set("");
try {
await operation();
} catch {
this.error.set("The change could not be saved. Reloading stored appointments.");
}
await this.viewChange();
}
5. Delete Appointments
The Angular Calendar has built-in support for event deleting. Enable it using eventDeleteHandling: "Update". Hovering over an appointment reveals an “x” icon in its upper-right corner. Clicking it removes the appointment from the UI and fires onEventDeleted, which saves the deletion in the database.
calendarConfig = signal<DayPilot.CalendarConfig>({
// ...
eventDeleteHandling: "Update",
onEventDeleted: args => {
this.save(() => firstValueFrom(this.ds.deleteEvent(args.e.id())));
},
// ...
});
The handler calls backend/api/backend_delete.php, which deletes the appointment record using its ID:
<?php
require_once '_db.php';
$json = file_get_contents('php://input');
$params = json_decode($json);
$stmt = $db->prepare("DELETE FROM events WHERE id = :id");
$stmt->bindParam(':id', $params->id);
$stmt->execute();
$response = new stdClass();
$response->result = 'OK';
$response->message = 'Update successful';
header('Content-Type: application/json');
echo json_encode($response);
6. Change the Calendar Date Using a Date Picker
In the last step, we add a date picker using the Navigator component. It lets users select a week to display in the appointment calendar. The navigatorConfig sets selectMode: "Week" and shows three months.
<daypilot-navigator [config]="navigatorConfig" [(date)]="date"></daypilot-navigator>
The [(date)] binding calls the setter when the user selects a day. The setter creates a new configuration value using calendarConfig.update():
get date(): DayPilot.Date {
return this.calendarConfig().startDate as DayPilot.Date;
}
set date(value: DayPilot.Date) {
this.calendarConfig.update(config => ({...config, startDate: value}));
}
The Calendar detects the signal update and changes the visible week. Its (viewChange) output then calls the loading method shown earlier to fetch appointments for the new range:
<daypilot-calendar #calendar [events]="events" [config]="calendarConfig()"
(viewChange)="viewChange()"></daypilot-calendar>
For more details on using the date picker, see Angular Calendar: Date Switching.
7. Run and Configure the Project
The downloadable project includes an Angular frontend in frontend/ and a PHP API in backend/. It uses Angular CLI and the open-source @daypilot/daypilot-lite-angular package. The local CLI is included in the frontend dependencies; a global installation is not required.
Install a Node.js version supported by Angular 22, npm, and PHP 8 with PDO SQLite enabled. The commands below work in Windows PowerShell or Command Prompt, macOS, and Linux. Make sure php, node, and npm are on PATH. On Windows, enable pdo_sqlite in the active php.ini; use php --ini to locate it. From the sample root, install the frontend dependencies:
cd frontend
npm ci
The root README contains setup instructions for both applications. Shared licensing and third-party notices are included with the sample. All Angular package files and configuration belong to frontend/; there is no root npm package.
PHP Backend
Our backend is implemented in PHP. It exposes JSON endpoints that load appointments and store changes in the database. The existing API files live in backend/api/: backend_events.php, backend_create.php, backend_move.php, backend_delete.php, and backend_update.php. The last endpoint updates the name and color; the calendar UI in this tutorial uses the creation form and drag-and-drop operations.
Running the PHP backend
Start the built-in PHP server with the following commands (from the sample root). The root README includes these commands for reference:
Run cd backend, then php -S 127.0.0.1:8090 -t public.
The -t public option serves only backend/public/. Its API entry points forward requests to the implementation in backend/api/. The database and configuration stay outside the document root. SQLite is the default database; the first request creates backend/daypilot.sqlite and the events table. The database starts empty and appointments survive browser reloads and PHP server restarts. Its path is based on the backend directory, not the shell working directory.
backend/api/_db_sqlite.php:
<?php
// Store the database outside the public API directory, independent of cwd.
$db = new PDO('sqlite:' . dirname(__DIR__) . '/daypilot.sqlite');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->exec("CREATE TABLE IF NOT EXISTS events (
id INTEGER PRIMARY KEY,
text TEXT,
start DATETIME,
end DATETIME,
color VARCHAR(200)
)");
Optional MySQL Configuration
To use MySQL, create the calendar database and enable PDO MySQL (pdo_mysql in php.ini on Windows). The database user needs permission to create and modify the events table. Edit backend/config.php, change driver to mysql, and enter your connection settings in the mysql array:
<?php
// Local database settings. Keep this file outside the public document root.
return [
'driver' => 'sqlite',
'mysql' => [
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'calendar',
'username' => 'calendar',
'password' => '',
],
];
Keep real credentials local. Restart PHP using the same command above on Windows, macOS, or Linux; no shell environment variables are required. The backend selects the provider in backend/api/_db.php. Both schemas use the same appointment fields. Switching providers does not migrate existing SQLite appointments. SQLite is the tested local setup; optional MySQL requires your own running server.
Backend proxy
In order to avoid cross-origin requests between the Angular frontend and PHP backend, the Angular development server proxies /api/** requests to PHP. The configuration is stored in frontend/proxy.conf.json:
{
"/api/**": {
"target": "http://127.0.0.1:8090",
"secure": false
}
}
The Angular serve configuration loads this proxy file. In a second terminal, from the sample root, install dependencies and start Angular:
Run cd frontend, then npm ci and npm start.
Open http://localhost:4200. Keep both servers running: Angular serves the application while PHP handles database requests. To build the frontend, run from the sample root:
cd frontend
npm run build
The output is written to frontend/dist/angular-appointment-calendar-component-typescript/browser/. For deployment, configure your web server to serve these files and route /api/ to PHP; the development proxy is not included in the build. Keep the SQLite file private and add authentication before storing private appointments.
History
-
October 2, 2026: Updated to Angular 22 with standalone components and signals, and DayPilot Lite for Angular 6.0.0. Simplified PHP startup and added file-based database configuration for SQLite and MySQL.
-
February 15, 2022: Upgraded to Angular 13, switched to the open-source DayPilot Lite for Angular (version 2022.1.362), colors added
-
August 22, 2021: Upgraded to Angular 12, DayPilot Pro for Angular 2021.3.5046, using DayPilot.Modal.form() for modal dialogs
-
December 10, 2020: Upgraded to Angular 11, DayPilot Pro for Angular 2020.4.4788
-
July 19, 2020: Upgraded to Angular 10, DayPilot Pro for Angular 2020.3.4547
-
September 13, 2019: Upgraded to Angular 8, DayPilot Pro for Angular 2019.2.3871, appointment deleting section added.
-
June 5, 2018: Upgraded to Angular 6, DayPilot Pro for Angular 2018.2.3297
-
October 25, 2017: Updated to DayPilot Pro for Angular 8.4, event creating fixed
-
April 11, 2017: Upgraded to Angular 4, Angular CLI 1.0, AOT support, standalone CalendarModule
-
January 5, 2017: Initial release
DayPilot




