Features

Angular 2 Frontend

  • Angular 2 CLI
  • 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 2 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. Buy a license.

New Angular 2 Project

We will use Angular CLI to create a new project.

ng new angular2-gantt-php-frontend

Adding DayPilot Gantt Module to the Project

Install DayPilot NPM module for DayPilot NPM repository:

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

DayPilot will be automatically added to node_modules and listed as a dependency in package.json

app.module.ts

Add the Angular 2 Gantt Chart component to the module declarations (DayPilot.Angular.Gantt).

TypeScript

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

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent,
    GanttComponent,
    DayPilot.Angular.Gantt
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Gantt Chart Component

We will create a new component that will wrap the Gantt chart component. It will hold 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).

app.component.html

In order to display our new Gantt component we need to add it app.component.html:

<h1>Gantt Chart</h1>
<gantt-component></gantt-component>

Running the Angular 2 Project

angular2-gantt-chart-component-php-init.png

Now we can test if everything has been loaded properly by running the Angular 2 application:

ng serve

We can see an empty Gantt chart without any tasks, displaying 30 days from today.

Gantt Chart Configuration

angular2-gantt-chart-component-with-task.png

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 #gantt1 placeholder so we can access the Gantt chart object in our code:

<daypilot-gantt #gantt1 [config]="config"></daypilot-gantt>

Our first configuration will include the following properties:

  • startDate - custom start date ("2017-01-01")
  • days - custom number of days to be displayed (31 days of January 2017)
  • 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} from "daypilot-pro-angular";

@Component({
  selector: 'gantt-component',
  template: `
  <daypilot-gantt #gantt1 [config]="config"></daypilot-gantt>
  `,
  styles: [``]
})
export class GanttComponent {

  @ViewChild('gantt1') gantt1: DayPilot.Angular.Gantt;

  config: any = {
    startDate: "2017-01-01",
    days: new DayPilot.Date("2016-07-01").daysInMonth(),
    cellWidthSpec: "Auto",
    tasks: [
      {start: "2017-01-05", end: "2017-01-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 Tasks from PHP Backend

angular2-gantt-chart-component-task-hierarchy.png

PHP Project

You can find the PHP backend project in the download package in a directory called "angular2-gantt-php-backend".

By default, it works with a local SQLite database (daypilot.sqlite) which is 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 copying _db_mysql.php over _db.php file.

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":"2017-01-05T00:00:00","end":"2017-01-07T00:00:00","complete":"30"},
      {"id":"3","text":"Task 2","start":"2017-01-07T00:00:00","end":"2017-01-09T00:00:00","complete":"75"},
      {"id":"4","text":"Milestone 1","start":"2017-01-09T00:00:00","end":"2017-01-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/angular2-gantt-php-backend

Windows

C:\PHP\php.exe -S 127.0.0.1:8090 -t C:\Users\daypilot\Tutorials\angular2-gantt-php-backend

Backend Proxy in Angular 2 Application

We will create a proxy for the PHP backend application in our Angular 2 project so we can access it using a relative "/api/*" URL.

Create a new proxy.conf.json file in the Angular 2 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 --proxy-conf parameter when starting the built-in Angular 2 CLI web server:

ng serve --proxy-config proxy.conf.json

Task Moving

angular2-gantt-chart-task-moving-drag-and-drop.png

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: any = {
  // ...
  onTaskMoved: args => {
    let params : MoveTaskParams = {
      id: args.task.id(),
      start: args.newStart.toString(),
      end: args.newEnd.toString()
    };
    this.ds.moveTask(params).subscribe(result => this.gantt1.control.message("Moved."));
  },
  // ...
};

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){
  }
  
  // ...

  moveTask(data: MoveTaskParams): Observable<any> {
    return this.http.post("/api/backend_move.php", data).map((response:Response) => response.json());
  }

  // ...


}

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

?>

Task Resizing

angular2-gantt-chart-task-resizing-drag-and-drop.png

Task resizing works the same way as task moving. We will reuse the DataService.moveTask() method for notifying the server.

gantt.component.ts

config: any = {
  // ...
  onTaskResized: args => {
    let params : MoveTaskParams = {
      id: args.task.id(),
      start: args.newStart.toString(),
      end: args.newEnd.toString()
    };
    this.ds.moveTask(params).subscribe(result => this.gantt1.control.message("Resized."));
  },
  // ...
};

It calls backend_move.php

Row Moving

angular2-gantt-chart-row-moving-drag-and-drop.png

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: any = {
  // ...
  onRowMoved: args => {
    let params: MoveRowParams = {
      source: args.source.id(),
      target: args.target.id(),
      position: args.position
    };
    this.ds.moveRow(params).subscribe(result => this.gantt1.control.message("Moved."));
  },
  // ...
};

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){
  }

  // ...

  moveRow(data: MoveRowParams): Observable<any> {
    return this.http.post("/api/backend_row_move.php", data).map((response:Response) => response.json());
  }

  // ...

}

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

?>

Task Creating

angular2-gantt-chart-task-creating.png

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 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: any = {
  // ...
  rowCreateHandling: "Enabled",
  onRowCreated: args => {
    let start = new DayPilot.Date(this.gantt1.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.gantt1.control.message("Created.");
    });
  }
};

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){
  }
  
  // ...

  createRow(data: CreateRowParams): Observable<any> {
    return this.http.post("/api/backend_create.php", data).map((response:Response) => response.json());
  }

}

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

?>

Task Deleting

angular2-gantt-chart-task-deleting.png

There is no built-in task delete icon but we can easily add it using row customization event handler (onBeforeRowHeaderRender).

gantt.component.ts

config: any = {
  // ...
  onBeforeRowHeaderRender: args => {
    args.row.areas = [
      {
        right: 0,
        top: 0,
        height: 20,
        width: 20,
        action: "JavaScript",
        js: row => {
          console.log(row);
          this.ds.deleteTask({id: row.id}).subscribe(result => {
            var task = this.gantt1.control.tasks.find(row.id);
            this.gantt1.control.tasks.remove(task);
            this.gantt1.control.message("Deleted.");
          })
        },
        v: "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/Rx';
import 'rxjs/Rx';

@Injectable()
export class DataService {

  constructor(private http : Http){
  }
  
  // ...

  deleteTask(data: DeleteTaskParams): Observable<any> {
    return this.http.post("/api/backend_task_delete.php", data).map((response:Response) => response.json());
  }

}

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

?>