Features
Angular 11 Frontend
Angular 11
Written in TypeScript
Loads Gantt Chart tasks from the server-side backend
Drag and drop support: task moving, resizing, row moving
Task creating (new row)
Task deleting (hover icon)
Uses Angular Gantt Chart Component from DayPilot Pro for JavaScript package (trial)
PHP Backend
Uses simple REST JSON API
Stores Scheduler data in a local SQLite database (or MySQL database)
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.
New Angular Project
We will use Angular CLI to create a new project.
ng new angular-gantt-php-frontend
Adding DayPilot Gantt Module to the Angular Project
Install DayPilot NPM module for DayPilot NPM repository:
npm install https://npm.daypilot.org/daypilot-pro-angular/trial/2020.4.4807.tar.gz --save
DayPilot will be automatically added to node_modules
and listed as a dependency in package.json
.
Angular Gantt Chart Component
We will create a new component that will wrap the DayPilot Angular Gantt Chart component. It will define the configuration and event handling logic.
The first version of the Gantt component will look like this:
gantt/gantt.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'gantt-component',
template: `
<daypilot-gantt></daypilot-gantt>
`,
styles: [``]
})
export class GanttComponent {
}
It includes <daypilot-gantt>
without any configuration (we'll add it later).
The Gantt component and related classes will be defined in a standalone GanttModule (gantt/gantt.module.ts
):
import {DataService} from "./data.service";
import {HttpModule} from "@angular/http";
import {FormsModule} from "@angular/forms";
import {BrowserModule} from "@angular/platform-browser";
import {NgModule} from "@angular/core";
import {DayPilotModule} from "daypilot-pro-angular";
import {GanttComponent} from "./gantt.component";
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
DayPilotModule
],
declarations: [
GanttComponent
],
exports: [ GanttComponent ],
providers: [ DataService ]
})
export class GanttModule { }
app.module.ts
Add the new Gantt module to the AppModule
imports.
TypeScript
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 {GanttModule} from "./gantt/gantt.module";
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
GanttModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.html
In order to display our new Gantt component we need to add it to app.component.html
:
<gantt-component></gantt-component>
Gantt Chart Component Configurator
Instead of creating the Angular project from scratch and configuring the Gantt chart component manually, you can also use Gantt Chart UI Builder configurator. This online application lets you configure the Gantt chart component and test the changes in a live preview. It can also generate a pre-configured Angular application for you.
Running the Angular Project
Now we can test if everything has been loaded properly by running the Angular application:
ng serve
We can see an empty Gantt chart without any tasks, displaying 30 days from today.
Gantt Chart Configuration
Now we can start customizing the Gantt chart. We need to add [config] attribute that will point to an object with the configuration.
<daypilot-gantt [config]="config"></daypilot-gantt>
We will also add #gantt placeholder so we can access the Gantt chart object in our code:
<daypilot-gantt #gantt [config]="config"></daypilot-gantt>
Our first configuration will include the following properties:
startDate - custom start date (
"2021-09-01"
)days - custom number of days to be displayed (30 days of September 2021)
cellWidthSpec - this will adjust the cell width so the total grid width matches the control width (and eliminate a horizontal scrollbar and any blank space)
tasks - an array with task data
gantt.component.ts
import {Component, ViewChild} from '@angular/core';
import {DayPilot, DayPilotGanttComponent} from "daypilot-pro-angular";
@Component({
selector: 'gantt-component',
template: `
<daypilot-gantt #gantt [config]="config"></daypilot-gantt>
`,
styles: [``]
})
export class GanttComponent {
@ViewChild('gantt')
gantt: DayPilotGanttComponent;
config: DayPilot.GanttConfig = {
startDate: "2021-09-01",
days: new DayPilot.Date("2021-09-01").daysInMonth(),
cellWidthSpec: "Auto",
tasks: [
{start: "2021-09-05", end: "2021-09-07", id: 1, text: "Task 1", complete: 50}
]
};
}
This configuration specifies the task data locally. In the next step we will load the tasks from the server.
Loading Gantt Tasks from PHP Backend
PHP Project
You can find the PHP backend project in the download package in a directory called angular-gantt-php-backend
.
By default, it works with a local SQLite database (daypilot.sqlite
) which will be automatically created in the current directory if it doesn't exist. A new database is initialized with a new schema and sample data.
You can find the database-related code in _db.php
file which is included (require_once
) in every JSON endpoint (backend_*.php
files).
You can also switch to using MySQL by modifying _db.php
to point to _db_mysql.php
instead of _db.php
.
Database Schema
Database Schema (SQLite)
CREATE TABLE task (
id INTEGER PRIMARY KEY,
name TEXT,
start DATETIME,
[end] DATETIME,
parent_id INTEGER,
milestone BOOLEAN DEFAULT (0) NOT NULL,
ordinal INTEGER,
ordinal_priority DATETIME,
complete INTEGER DEFAULT (0) NOT NULL
);
CREATE TABLE link (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
type VARCHAR (100) NOT NULL
);
Database Schema (MySQL)
CREATE TABLE task (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
name TEXT,
start DATETIME,
end DATETIME,
parent_id INTEGER,
milestone BOOLEAN DEFAULT '0' NOT NULL,
ordinal INTEGER,
ordinal_priority DATETIME,
complete INTEGER DEFAULT '0' NOT NULL
);
CREATE TABLE link (
id INTEGER PRIMARY KEY AUTO_INCREMENT,
from_id INTEGER NOT NULL,
to_id INTEGER NOT NULL,
type VARCHAR (100) NOT NULL
);
Loading Tasks from the Backend (JSON Format)
The server-side task loading endpoint returns all tasks stored in a database. This is a sample JSON response:
[
{"id":"1","text":"Group 1","start":null,"end":null,"complete":"0","children":
[
{"id":"2","text":"Task 1","start":"2021-09-05T00:00:00","end":"2021-09-07T00:00:00","complete":"30"},
{"id":"3","text":"Task 2","start":"2021-09-07T00:00:00","end":"2021-09-09T00:00:00","complete":"75"},
{"id":"4","text":"Milestone 1","start":"2021-09-09T00:00:00","end":"2021-09-09T00:00:00","complete":"0","type":"Milestone"}
]
}
]
It follows the format required by tasks.list property.
backend_tasks.php
<?php
require_once '_db.php';
class Task {}
$result = tasklist($db, db_get_tasks(null));
header('Content-Type: application/json');
echo json_encode($result);
function tasklist($db, $items) {
$result = array();
foreach($items as $item) {
$r = new Task();
// rows
$r->id = $item['id'];
$r->text = htmlspecialchars($item['name']);
$r->start = $item['start'];
$r->end = $item['end'];
$r->complete = $item['complete'];
if ($item['milestone']) {
$r->type = 'Milestone';
}
$parent = $r->id;
$children = db_get_tasks($parent);
if (!empty($children)) {
$r->children = tasklist($db, $children);
}
$result[] = $r;
}
return $result;
}
Running the PHP backend
We will run the backend using the PHP built-in webserver on port 8090:
Linux
php -S 127.0.0.1:8090 -t /home/daypilot/tutorials/angular-gantt-php-backend
Windows
C:\PHP\php.exe -S 127.0.0.1:8090 -t C:\Users\daypilot\Tutorials\angular-gantt-php-backend
Backend Proxy in Angular Application
We will create a proxy for the PHP backend application in our Angular project so we can access it using a relative /api/*
URL.
Create a new proxy.conf.json
file in the Angular project root:
{
"/api": {
"target": "http://localhost:8090",
"secure": false
}
}
This will redirect all requests starting with /api
to the local PHP server running at port 8090.
You need to activate the proxy by specifying the --proxy-conf
parameter when starting the built-in Angular CLI web server:
ng serve --proxy-config proxy.conf.json
Gantt Chart: Task Moving
Gantt task moving is enabled by default. We will add a custom event handler (onTaskMoved) using the config object. The event handler will save the changes in the database by calling backend_move.php
script on the server side.
gantt.component.ts
config: DayPilot.GanttConfig = {
// ...
onTaskMoved: args => {
let params : MoveTaskParams = {
id: args.task.id(),
start: args.newStart.toString(),
end: args.newEnd.toString()
};
this.ds.moveTask(params).subscribe(result => this.gantt.control.message("Moved."));
},
// ...
};
data.service.ts
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
// ...
moveTask(data: MoveTaskParams): Observable<any> {
return this.http.post("/api/backend_move.php", data) as Observable<any>;
}
// ...
}
export interface MoveTaskParams {
id: string | number;
start: string;
end: string;
}
// ...
backend_move.php
<?php
require_once '_db.php';
$json = file_get_contents('php://input');
$params = json_decode($json);
$start = new DateTime($params->start);
$start->setTime(12, 0, 0);
$start_string = $start->format("Y-m-d\\TH:i:s");
$end = new DateTime($params->end);
$end->setTime(12, 0, 0);
$end_string = $end->format("Y-m-d\\TH:i:s");
db_update_task($params->id, $params->start, $params->end);
class Result {}
$response = new Result();
$response->result = 'OK';
header('Content-Type: application/json');
echo json_encode($response);
Gantt Chart: Task Resizing
Task resizing works the same way as task moving. We will reuse the DataService.moveTask()
method for notifying the server.
gantt.component.ts
config: DayPilot.GanttConfig = {
// ...
onTaskResized: args => {
let params : MoveTaskParams = {
id: args.task.id(),
start: args.newStart.toString(),
end: args.newEnd.toString()
};
this.ds.moveTask(params).subscribe(result => this.gantt.control.message("Resized."));
},
// ...
};
It calls the existing backend_move.php
on the server side.
Row Moving
The Gantt chart task hierarchy can be rearranged using drag and drop. When a task position is changed the Gantt component calls onRowMoved event handler. We will use this event handler to update the task position in the database.
gantt.component.ts
config: DayPilot.GanttConfig = {
// ...
onRowMoved: args => {
let params: MoveRowParams = {
source: args.source.id(),
target: args.target.id(),
position: args.position
};
this.ds.moveRow(params).subscribe(result => this.gantt.control.message("Moved."));
},
// ...
};
data.service.ts
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
// ...
moveRow(data: MoveRowParams): Observable<any> {
return this.http.post("/api/backend_row_move.php", data) as Observable<any>;
}
// ...
}
export interface MoveRowParams {
source: string | number;
target: string | number;
position: "before" | "after" | "child";
}
// ...
backend_row_move.php
<?php
require_once '_db.php';
$max = 100000;
$json = file_get_contents('php://input');
$params = json_decode($json);
$source = db_get_task($params->source);
$target = db_get_task($params->target);
$source_parent_id = $source ? $source["parent_id"] : null;
$target_parent_id = $target ? $target["parent_id"] : null;
$target_ordinal = $target["ordinal"];
switch ($params->position) {
case "before":
db_update_task_parent($source["id"], $target_parent_id, $target_ordinal);
break;
case "after":
db_update_task_parent($source["id"], $target_parent_id, $target_ordinal + 1);
break;
case "child":
echo "child:source/".$source["id"]."/target/".$target["id"];
db_update_task_parent($source["id"], $target["id"], $max);
$target_parent_id = $target["id"];
break;
case "forbidden":
break;
}
db_compact_ordinals($source_parent_id);
if ($source_parent_id != $target_parent_id) {
db_compact_ordinals($target_parent_id);
}
class Result {}
$response = new Result();
$response->result = 'OK';
header('Content-Type: application/json');
echo json_encode($response);
Creating New Tasks in the Gantt Chart
Now we will let our users add new tasks to the Gantt chart. One of the options for adding a new task is to display a special row at the bottom which accepts a new task name. The special "new task row" has to be enabled using rowCreateHandling
property (rowCreateHandling = "Enabled"
).
As soon as a user confirms the new task name the Gantt component fires onRowCreated events handler. In the onRowCreated
method, we call the server and save the new task in the database. When the server-side callback finishes we add the new task as a new item at the end of config.tasks
array.
gantt.component.ts
config: DayPilot.GanttConfig = {
// ...
rowCreateHandling: "Enabled",
onRowCreated: args => {
let start = new DayPilot.Date(this.gantt.control.startDate);
let end = start.addDays(1);
let params: CreateRowParams = {
start: start.toString(),
end: end.toString(),
name: args.text
};
this.ds.createRow(params).subscribe(result => {
this.config.tasks.push({
start: params.start,
end: params.end,
id: result.id,
text: params.name
});
this.gantt.control.message("Created.");
});
}
};
data.service.ts
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
// ...
createRow(data: CreateRowParams): Observable<any> {
return this.http.post("/api/backend_create.php", data) as Observable<any>;
}
}
export interface CreateRowParams {
start: string;
end: string;
name: string;
}
// ...
backend_create.php
<?php
require_once '_db.php';
$json = file_get_contents('php://input');
$params = json_decode($json);
$now = (new DateTime("now"))->format('Y-m-d H:i:s');
$ordinal = db_get_max_ordinal(null) + 1;
$stmt = $db->prepare("INSERT INTO task (name, start, end, ordinal, ordinal_priority) VALUES (:name, :start, :end, :ordinal, :priority)");
$stmt->bindParam(':name', $params->name);
$stmt->bindParam(':start', $params->start);
$stmt->bindParam(':end', $params->end);
$stmt->bindParam(":ordinal", $ordinal);
$stmt->bindParam(":priority", $now);
$stmt->execute();
class Result {}
$response = new Result();
$response->result = 'OK';
$response->message = 'Created with id: '.$db->lastInsertId();
$response->id = $db->lastInsertId();
header('Content-Type: application/json');
echo json_encode($response);
Gantt Chart Task Deleting
There is no built-in task delete icon but we can easily add it using row customization event handler (onBeforeRowHeaderRender).
gantt.component.ts
config: DayPilot.GanttConfig = {
// ...
onBeforeRowHeaderRender: args => {
args.row.areas = [
{
right: 0,
top: 0,
height: 20,
width: 20,
onClick: args => {
const row = args.source;
console.log(row);
this.ds.deleteTask({id: row.id}).subscribe(result => {
var task = this.gantt.control.tasks.find(row.id);
this.gantt.control.tasks.remove(task);
this.gantt.control.message("Deleted.");
})
},
visibility: "Hover",
style: "opacity: 0.5; cursor: pointer; background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjExR/NCNwAAAI5JREFUKFNtkLERgCAMRbmzdK8s4gAUlhYOYEHJEJYOYOEwDmGBPxC4kOPfvePy84MGR0RJ2N1A8H3N6DATwSQ57m2ql8NBG+AEM7D+UW+wjdfUPgerYNgB5gOLRHqhcasg84C2QxPMtrUhSqQIhg7ypy9VM2EUZPI/4rQ7rGxqo9sadTegw+UdjeDLAKUfhbaQUVPIfJYAAAAASUVORK5CYII=) center center no-repeat;"
}
];
}
};
data.service.ts
import {Http, Response} from '@angular/http';
import {Injectable} from '@angular/core';
import {Observable} from 'rxjs';
import {HttpClient} from '@angular/common/http';
@Injectable()
export class DataService {
constructor(private http : HttpClient){
}
// ...
deleteTask(data: DeleteTaskParams): Observable<any> {
return this.http.post("/api/backend_task_delete.php", data) as Observable<any>;
}
}
export interface DeleteTaskParams {
id: string | number;
}
// ...
backend_task_delete.php
<?php
require_once '_db.php';
$json = file_get_contents('php://input');
$params = json_decode($json);
$stmt = $db->prepare("DELETE from task WHERE id = :id");
$stmt->bindParam(':id', $params->id);
$stmt->execute();
class Result {}
$response = new Result();
$response->result = 'OK';
header('Content-Type: application/json');
echo json_encode($response);
History
December 20, 2020: Upgraded to Angular 11, DayPilot Pro 2020.4; saving new links to the database
August 3, 2020: Upgraded to Angular 10, DayPilot Pro 2020.3
September 5, 2019: Upgraded to Angular 8, DayPilot Pro 2019.2.3871
June 13, 2018: Upgraded to Angular 6, DayPilot Pro for JavaScript 2018.2.3297
April 12, 2017: Upgraded to Angular 4, Angular CLI 1.0. Standalone GanttModule.
December 13, 2016: Initial release (Angular 2)