DHTMLX Gantt Chart Usage with Angular Framework

| Leave a comment

Updated on March 6, 2019

We’ve already described how to use dhtmlxGantt with AngularJS 1.x. Since Angular doesn’t have backward compatibility with Angular 1, such an approach won’t work. That’s why our plan for today is to give you a good starting point for using dhtmlxGantt with Angular framework.

Now we’re going to implement an angular gantt chart component for dhtmlxGantt (JavaScript Gantt Chart), add it into the small application and bind it to the RESTful API on a backend. We’ll write this demo in TypeScript since it’s recommended way, although it also can be done in plain JavaScript or in ES6.

Please mind that in this tutorial we won’t cover server-side logic of data saving. Instead, we’ll emulate the backend using Angular in-memory-web-api tool.

You can download a complete Angular Gantt chart demo from GitHub

Preparing App

To set up development environment, we’ll use angular cli. If you don’t have angular cli you can get it with package manager (npm install -g @angular/cli) or check its documentation for more details. To create app skeleton, run following command:

    ng new gantt-angular

After operation finish, we can go to app directory and run application:

    cd gantt-angular
    ng serve

Now, if we open http://127.0.0.1:4200 we should see initial page. ng serve will watch the source file changing and rebuild app if necessary.

angular app

Now, open src/app and create folders for components, models and services to use them for necessary elements we’ll need further. So, we will put all the components to the components folder, all the models to the models folder and all the services and service helpers to the service folder.

Creating gantt chart component

At first, we should get DHTMLX Gantt chart code. Starting from Gantt v6.1, type definitions are bundled with the package, so we don’t need to install them separately. Run the following command in order to add Gantt:

npm install dhtmlx-gantt --save

To add dhtmlxGantt to Angular app we should create a new component. For this, create gantt.component.ts file with the following code:

import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';

import "dhtmlx-gantt";

@Component({
    encapsulation: ViewEncapsulation.None,
    selector: 'gantt',
    styleUrls: ['./gantt.component.css'],
    providers: [TaskService, LinkService],
    template: `<div #gantt_here class='gantt-chart'></div>`,
})

export class GanttComponent implements OnInit {
    @ViewChild("gantt_here") ganttContainer: ElementRef;

    ngOnInit(){
        gantt.init(this.ganttContainer.nativeElement);
    }
}

Here we’ve defined our new component. It can be used with “gantt” tag in html. When element is loaded, it initializes gantt inside container.

Note that we’ll declare gantt styles in the separate file named “gantt.component.css”. Default styles can look like the following:

@import "dhtmlx-gantt/codebase/dhtmlxgantt.css";
.gantt-chart{
    position: relative;
    width: 100%;
    height: 600px;
}

We should also add our component to app.module declarations. Now, “app.module.ts” should contain code like following:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { HttpClientModule } from '@angular/common/http';

import {AppComponent} from './app.component';
import {GanttComponent} from "./components/gantt.component";

@NgModule({
    declarations: [
        AppComponent,
        GanttComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule {}

Next, we can put out new component to the page. For this, open “app.component.html” and put following code in there:

<gantt></gantt>

Now if we open the page, we should see an empty gantt chart on it.

empty gantt chart

Data Loading

To add data loading to our angular gantt chart, we are going to add task and link services. But let’s create models at first.

Create task.ts with following code:

export class Task {
    id: number;
    start_date: string;
    text: string;
    progress: number;
    duration: number;
    parent: number;
}

Create link.ts with following code:

export class Link {
    id: number;
    source: number;
    target: number;
    type: string;
}

Now, let’s create our services. Service is a class that responsible for doing a specific task. Angular services can be injected using Dependency Injection mechanism and include data, function or some features necessary for your application. We’re going to create a data service that we’ll use to provide our gantt chart with tasks.

Create task.service.ts with following code:

import {Injectable} from "@angular/core";
import {Task} from "../models/task";

@Injectable()
export class TaskService {
    get(): Promise<Task[]>{
        return Promise.resolve([
            {id: 1, text: "Task #1", start_date: "2017-04-15 00:00", duration: 3, progress: 0.6},
            {id: 2, text: "Task #2", start_date: "2017-04-18 00:00", duration: 3, progress: 0.4}
        ]);
    }
}

We’ve added @Injectable() decorator to our service. It marks a class as available to an injector for instantiation. We’ll inject it to our component further.

Currently, it returns resolved promise with hardcoded data. But you can load data from the server side and return promise. We will discuss it a little closer below.

Now we need to add link service. Create link.service.ts with following code:

import {Injectable} from "@angular/core";
import {Link} from "../models/link";

@Injectable()
export class LinkService {
    get(): Promise<Link[]> {
        return Promise.resolve([
            {id: 1, source: 1, target: 2, type: "0"}
        ]);
    }
}

This service is pretty much the same as task service.

Now open our gantt.component.ts. We should add TaskService and LinkService to our component. At first, add necessary imports for our services:

import {TaskService} from "../services/task.service";
import {LinkService} from "../services/link.service";

Also we should add providers property to @Component decorator argument:

    providers: [TaskService, LinkService]

It tells angular to create fresh instance of our services when it creates new GanttComponent. So, our component can use those services to get tasks and links.

Now, we can inject our services to the component. For this purpose, add the following constructor to GanttComponent class:

constructor(private taskService: TaskService, private linkService: LinkService){}

Put following code to ngOnInit function.

gantt.config.xml_date = "%Y-%m-%d %H:%i";

gantt.init(this.ganttContainer.nativeElement);

Promise.all([this.taskService.get(), this.linkService.get()])
     .then(([data, links]) => {
         gantt.parse({data, links});
     });

Here we’ve added xml_date config definition. It sets loading tasks data format. Also here we call our services to get function and then are waiting for response to put data to gantt. gantt.parse accepts data object of following structure.

We should get code like following at our gantt.component.ts

import {Component, ElementRef, OnInit, ViewChild} from "@angular/core";

import "dhtmlx-gantt";
import {TaskService} from "../services/task.service";
import {LinkService} from "../services/link.service";

@Component({
    selector: "gantt",
    styles: [
        `
        :host{
            display: block;
            height: 600px;
            position: relative;
            width: 100%;
        }
    `],
    providers: [TaskService, LinkService],
    template: "<div #gantt_here style='width: 100%; height: 100%;'></div>",
})
export class GanttComponent implements OnInit {
    @ViewChild("gantt_here") ganttContainer: ElementRef;

    constructor(private taskService: TaskService, private linkService: LinkService){}

    ngOnInit(){
        gantt.config.xml_date = "%Y-%m-%d %H:%i";

        gantt.init(this.ganttContainer.nativeElement);

        Promise.all([this.taskService.get(), this.linkService.get()])
            .then(([data, links]) => {
                gantt.parse({data, links});
            });
    }
}

Now, if we open app, we should see our tasks with link between them.

gantt with tasks

Data Saving

In this tutorial we’re not going to create an actual database and write a backend. Instead, we’ll mock backend with a help of angular-in-memory-web-api library – it’ll intercept xhr requests and call data storage we’ll define on the client. If it’s not what you want, you can skip the first step and the app will call the real backend.

1. At first, we should install angular-in-memory-web-api. For this, run the following command:

npm install angular-in-memory-web-api --save

Open app.module.ts to add InMemoryWebApiModule and define our mock database initialization. We will create necessary class on next step. For now, our app.module.ts should look like following:

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { HttpClientModule } from '@angular/common/http';


import {HttpClientInMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService}  from './services/in-memory-data.service'

import {AppComponent} from './app.component';
import {GanttComponent} from "./components/gantt.component";

@NgModule({
  declarations: [
     AppComponent,
     GanttComponent
  ],
  imports: [
     HttpClientModule,
     BrowserModule,
     HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService)
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Create service in-memory-data.service.ts with the following code:

import {InMemoryDbService} from "angular-in-memory-web-api";

export class InMemoryDataService implements InMemoryDbService {
    createDb() {
        let    tasks = [
            {id: 1, text: "Task #1", start_date: "2017-04-15 00:00", duration: 3, progress: 0.6},
            {id: 2, text: "Task #2", start_date: "2017-04-18 00:00", duration: 3, progress: 0.4}
        ];
        let links = [
            {id: 1, source: 1, target: 2, type: "0"}
        ];
        return {tasks, links};
    }
}

2. Let’s create a helper for our requests. Do so, create service-helper.ts with the following code:

import {Response} from "@angular/http";

export function HandleError(error: any): Promise<any>{
    console.log(error);
    return Promise.reject(error);
}

HandleError will put error to console in case of some errors.

3. Update our services to handle adding, updating and deleting items. Open task.service.ts and put the following code to it:

import {Injectable} from "@angular/core";
import {Task} from "../models/task";
import {HttpClient} from '@angular/common/http';
import {HandleError} from './service-helper';

@Injectable()
export class TaskService {
    private taskUrl = "api/tasks";

    constructor(private http: HttpClient) {}

    get(): Promise<Task[]>{
        return this.http.get(this.taskUrl)
            .toPromise()
            .catch(HandleError);
    }

    insert(task: Task): Promise<Task> {
        return this.http.post(this.taskUrl, task)
            .toPromise()
            .catch(HandleError);
    }


    update(task: Task): Promise<void> {
        return this.http
            .put('${this.taskUrl}/${task.id}', task)
            .toPromise()
            .catch(HandleError);
    }

    remove(id: number): Promise<void> {
        return this.http.delete('${this.taskUrl}/${id}')
            .toPromise()
            .catch(HandleError);
    }
}

We’ve defined taskUrl as a private element of our service. It contains url to our REST API. In order to send HTTP requests, we’ve injected HTTP class to our service.

To insert a new item, we send POST request to our url with new task in body. To update item, we send PUT request to url/item_id. This request also contains updated task in body. To remove item, we send delete request to url/item_id. In this case, an item with such id will be removed.

Now, open link.service.ts and update our LinkService.

import {Injectable} from "@angular/core";
import {Link} from "../models/link";
import {HttpClient} from '@angular/common/http';
import {HandleError} from './service-helper';


@Injectable()
export class LinkService {
    private linkUrl = "api/links";

    constructor(private http: Http) {}

    get(): Promise<Link[]> {
        return this.http.get(this.linkUrl)
            .toPromise()
            .catch(HandleError);
    }

    insert(link: Link): Promise<Link> {
        return this.http.post(this.linkUrl, link)
            .toPromise()
            .catch(HandleError);
    }

    update(link: Link): Promise<void> {
        return this.http.put('${this.linkUrl}/${link.id}', link)
            .toPromise()
            .catch(HandleError);
    }

    remove(id: number): Promise<void> {
        return this.http.delete('${this.linkUrl}/${id}')
            .toPromise()
            .catch(HandleError);
    }
}

It’s pretty much the same as TaskService and provides same API for links.

Now, open gantt.component.ts and add tasks editing logic to it.

import { Component, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import {TaskService} from "../services/task.service";
import {LinkService} from "../services/link.service";
import {Task} from "../models/Task";
import {Link} from "../models/Link";

import "dhtmlx-gantt";


@Component({
    encapsulation: ViewEncapsulation.None,
    selector: 'gantt',
    styleUrls: ['./gantt.component.css'],
    providers: [TaskService, LinkService],
    template: `<div #gantt_here class='gantt-chart'></div>`,
})
export class GanttComponent implements OnInit {
    @ViewChild("gantt_here") ganttContainer: ElementRef;

    constructor(private taskService: TaskService, private linkService: LinkService){}

    ngOnInit(){
        gantt.config.xml_date = "%Y-%m-%d %H:%i";

        gantt.init(this.ganttContainer.nativeElement);

        const dp = gantt.createDataProcessor({
            task: {
                update: (data: Task) => this.taskService.update(data),
                create: (data: Task) => this.taskService.insert(data),
                delete: (id) => this.taskService.remove(id)
            },
            link: {
                update: (data: Link) => this.linkService.update(data),
                create: (data: Link) => this.linkService.insert(data),
                delete: (id) => this.linkService.remove(id)
            }
        });


        Promise.all([this.taskService.get(), this.linkService.get()])
            .then(([data, links]) => {
                gantt.parse({data, links});
            });
    }
}

Here we’ve defined a dataProcessor handler that will capture changes made in gantt by the user and will transfer them to data services.
The handler can be declared either as a function or a router object, we’ve used the latter approach here.
Gantt accepts Promise response from the handler, so the gantt will correctly process the action completeness.
If your service changes a task or a link id after creating a new record (which it usually does), make sure that your Promise returns an object with {id: databaseId} or {tid: databaseId} as a result, so Gantt could apply new database id to the record.

Keep in mind that in this demo we’ve used angular-in-memory-api library to emulate the data storage, but in real life you’ll most probably want to save changes to the real database. In order to do that you’ll either need to remove in-memory-web-api from the app, or configure it to pass requests thru to the real backend, and implement a data storage.

Once again, you are welcome to check a complete demo of our gantt chart and Angular on GitHub.

Which technologies/frameworks are you using?

We need your feedback to provide the right integrations at the right time. Please leave your requests here:

Thank you in advance and stay tuned for new tutorials!