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

angular gantt chart component tutorial 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

angular gantt chart component tutorial php mysql empty

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

angular gantt chart component tutorial php mysql task

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

angular2 gantt chart component task hierarchy

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

angular2 gantt chart task moving drag and drop

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

angular2 gantt chart task resizing drag and drop

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

angular2 gantt chart row moving drag and drop

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

angular2 gantt chart task creating

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

angular2 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)