Server-Side Integration

The recommended approach to connect dhtmlxGantt to a backend is to implement RESTful API on the server and use dhtmlxDataProcessor on the client. DataProcessor is a client-side library included into dhtmlxGantt.js. It monitors data changes and gets the server requests on the client side.

Gantt uses its own instance of DataProcessor which has some specificity in comparison to the main version of the library. The current article describes the way of using Gantt DataProcessor to provide integration with server-side platforms.

You can take a look at the video guide that shows how to create a Gantt chart on the page and load the data into it on the example of a Node.js platform.

Technique

Generally, to load data from the server side using REST API, you need to:

Client side

1) Call the load method, where as a parameter specify the URL that returns Gantt data in the JSON format.

2) Create a DataProcessor instance using one of the two ways:

  • Initialize DataProcessor and attach it to the dhtmlxGantt object:
gantt.init("gantt_here");
gantt.load("apiUrl");
 
// keep the order of the lines below
var dp = new gantt.dataProcessor("apiUrl");
dp.init(gantt);
dp.setTransactionMode("REST");
dp.deleteAfterConfirmation = true;
  • Call the createDataProcessor method and pass an object with configuration options as its parameter:
var dp = gantt.createDataProcessor({
    url: "apiUrl",
    mode: "REST",
    deleteAfterConfirmation: true
});

Check the detailed info in the next section.

Creating DataProcessor

While creating a DataProcessor via the API method createDataProcessor you have several possible options for passing parameters.

1. Use one of the predefined request modes, as in:

var dp = gantt.createDataProcessor({
    url: "/api",
    mode: "REST",
    deleteAfterConfirmation: true
});

where:

  • url - the URL to the server side
  • mode - the mode of sending data to the server: "JSON" | "REST-JSON" | "JSON" | "POST" | "GET"
  • deleteAfterConfirmation - defines whether the task must be deleted from the gantt only after a successful response from the server. Dependency links and subtasks will be deleted after the deletion of the parent task is confirmed.

2. Provide a custom router object:

var dp = gantt.createDataProcessor(router);
  • where router is either a function:
// entity - "task"|"link"|"resource"|"assignment"
// action - "create"|"update"|"delete"
// data - an object with task or link data
// id – the id of a processed object (task or link)
var dp = gantt.createDataProcessor(function(entity, action, data, id) { 
    switch(action) {
        case "create":
           return gantt.ajax.post(
                server + "/" + entity,
                data
           );
        break;
        case "update":
           return gantt.ajax.put(
                 server + "/" + entity + "/" + id,
                 data
            );
        break;
        case "delete":
           return gantt.ajax.del(
                 server + "/" + entity + "/" + id
           );
         break;
   }
});
  • or an object of the following structure:
var dp = gantt.createDataProcessor({ 
   task: {
      create: function(data) {},
      update: function(data, id) {},
      delete: function(id) {}
   },
   link: {
      create: function(data) {},
      update: function(data, id) {},
      delete: function(id) {}
   }
});

All the functions of the router object should return either a Promise or a data response object. This is needed for the dataProcessor to apply the database id and to hook onAfterUpdate event of the data processor.

router = function(entity, action, data, id) {
    return new gantt.Promise(function(resolve, reject) {
        // … some logic
        return resolve({tid: databaseId});
    });
}

Thus you can use DataProcessor for saving data in localStorage, or any other storage which is not linked to a certain URL, or in case if there are two different servers (URLs) responsible for creation and deletion of objects.

Related sample:  Custom data api - using local storage

Request and response details

The URL is formed by the following rule:

  • api/link/id
  • api/task/id
  • api/resource/id
  • api/assignment/id

where "api" is the URL you've specified in the dataProcessor configuration.

The list of possible requests and responses is:

ActionHTTP MethodURLResponse
load data GET /apiUrl JSON format
Tasks
add a new task POST /apiUrl/task {"action":"inserted","tid":"id"}
update a task PUT /apiUrl/task/id {"action":"updated"}
delete a task DELETE /apiUrl/task/id {"action":"deleted"}
Links
add a new link POST /apiUrl/link {"action":"inserted","tid":"id"}
update a link PUT /apiUrl/link/id {"action":"updated"}
delete a link DELETE /apiUrl/link/id {"action":"deleted"}
Resources
add a new resource POST /apiUrl/resource {"action":"inserted","tid":"id"}
update a resource PUT /apiUrl/resource/id {"action":"updated"}
delete a resource DELETE /apiUrl/resource/id {"action":"deleted"}
Resource Assignments
add a new assignment POST /apiUrl/assignment {"action":"inserted","tid":"id"}
update an assignment PUT /apiUrl/assignment/id {"action":"updated"}
delete an assignment DELETE /apiUrl/assignment/id {"action":"deleted"}

By default, Resources and Resource Assignments are not sent to the DataProcessor. If needed, you have to enable this behavior explicitly. Read more here.

Request parameters

Create/Update/Delete requests will contain all public properties of a client-side task or link object:

Task:

  • start_date:2017-04-08 00:00:00
  • duration:4
  • text:Task #2.2
  • parent:3
  • end_date:2017-04-12 00:00:00

Link:

  • source:1
  • target:2
  • type:0

Note:

  • The format of the start_date and end_date parameters is defined by the date_format config.
  • The client side sends all the public properties of a task or link object. Thus, a request may contain any number of additional parameters.
  • If you extend the data model by adding new columns/properties to it, no additional actions are needed to make gantt sending them to the backend.

By public properties here we mean the properties the names of which don't start with an underscore (_) or a dollar sign ($) characters, e.g. properties named task._owner or link.$state won't be sent to the backend.

REST-JSON mode

Besides the "POST","GET","REST" and "JSON" transaction modes, Gantt DataProcessor can also be used in the "REST-JSON" mode.

gantt.load("apiUrl");
 
var dp = gantt.createDataProcessor({
      url: "/apiUrl",
      mode: "REST-JSON"
});

It uses the same URLs for requests, but the request parameters for tasks and links and the form of sending them to the server differ.

In the REST mode data is sent to the server as a form:

Content-Type: application/x-www-form-urlencoded

while in the REST-JSON mode data is sent in the JSON format:

Headers

Content-type: application/json

So parameters are sent as a JSON object:

Request Payload

  • Task
{
    "start_date": "20-09-2018 00:00",
    "text": "New task",
    "duration":1,
    "end_date": "21-09-2018 00:00",
    "parent": 0,
    "usage":[{
        {"id":"1", "value":"30"},
        {"id":"2", "value":"20"}
    }]
}
  • Link
{
    "source": 1,
    "target": 2,
    "type": "0"
}

This format makes processing of complex records handier on any server-side platform.

Server side

On each action performed in the Gantt (adding, updating or deleting tasks or links), dataProcessor reacts by sending an AJAX request to the server.

Each request contains all the data needed to save changes in the database. As we initialized dataProcessor in the REST mode, it will use different HTTP verbs for each type of operation.

Since we use REST API, it's possible to implement the server side using different frameworks and programming languages. Here's a list of available server-side implementations that you can use for Gantt backend integration:

Storing the Order of Tasks

Gantt displays tasks in the same order they come from a data source. If you allow users to reorder tasks manually, you'll also need to store this order in the database and make sure that your data feed returns data sorted appropriately.

Client-side configuration:

// reordering tasks within the whole gantt
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;
 
gantt.init("gantt_here");
gantt.load("/api");
 
var dp = gantt.createDataProcessor({
      url: "/api",
      mode: "REST"
});

The saving order can be implemented in several ways, we'll show one of them.

  • You add a numeric column to your tasks table, let's call it 'sortorder'.
  • When serving the GET action, you sort tasks by this column in the ascending order.
  • When a new task is added, it should receive MAX(sortorder) + 1 sortorder.
  • When the order is changed on the client side, gantt will send PUT (POST if you don't use the REST mode) with all the properties of a task and also the values that describe the position of the task within the project tree.
HTTP MethodURLParametersResponse
PUT /apiUrl/task/taskId
  • target= adjacentTaskId
{"action":"updated"}

The target parameter will contain the id of the nearest task that goes right before or right after the current task.

Its value may come in one of two formats:

  • target=targetId - the current task should go right before the targetId task
  • target=next:targetId - the current task should go right after the targetId task

Applying of the order changes usually involves updating of multiple tasks, here is a pseudo code example of how it can be implemented:

const target = request["target"];
const currentTaskId = request["id"];
let nextTask;
let targetTaskId;
 
// get id of adjacent task and check whether updated task should go before or after it
if(target.startsWith("next:")){
    targetTaskId = target.substr("next:".length);
    nextTask = true;
}else{
    targetTaskId = target;
    nextTask = false;
}
 
const currentTask = tasks.getById(currentTaskId);
const targetTask = tasks.getById(targetTaskId);
 
if(!targetTaskId)
    return;
 
// updated task will receive the sortorder value of the adjacent task
let targetOrder = targetTask.sortorder;
 
// if it should go after the adjacent task, it should receive a bigger sortorder
if(nextTask)
    targetOrder++;
 
// increase sort orders of tasks that should go after the updated task
tasks.where(task => task.sortorder >= targetOrder).
   update(task => task.sortorder++);
 
// and update the task with its new sortorder
currentTask.sortorder = targetOrder;
 
tasks.save(currentTask);

You can have a look at the detailed examples on how to implement storing the tasks' order for particular server-side platforms: plain PHP, Laravel, Node.js, ASP.NET Web API and Rails.

Custom Request Headers and Parameters

Adding custom request headers

When you need Gantt to send additional headers to your backend, you can specify them using the dataProcessor.setTransactionMode method.

For example, let's suppose that you need to add an authorization token to your requests:

gantt.init("gantt_here");
gantt.load("/api");
 
var dp = gantt.createDataProcessor({
  url: "/api",
  mode:"REST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    "Authorization": "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
  }
});

Currently, load does not support header/payload parameters, so if you need them for GET request, you'll have to send xhr manually and load data into gantt using parse, for example:

gantt.ajax.get({
    url: "/api",
    headers: {
        "Authorization": "Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
    }
}).then(function (xhr) {
    gantt.parse(xhr.responseText)
});

Adding custom parameters to the request

There are a couple of ways to send additional parameters to requests.

As you know, gantt sends all properties of the data object back to the backend. Thus, you can add an additional property directly to the data object and it will be sent to the backend:

gantt.attachEvent("onTaskCreated", function(task){
    task.userId = currentUser;
    return true;
});

Alternatively, you can add custom parameters to all requests sent by data processor, using the payload property of the setTransactionMode parameter:

gantt.init("gantt_here");
gantt.load("/api");
 
var dp = gantt.createDataProcessor({
  url: "/api",
  mode:"REST",
  payload: {
    token: "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b"
  }
});

One more way to add custom parameters to a request is to use the onBeforeUpdate event of DataProcessor:

var dp = gantt.createDataProcessor({
  url: "/api",
  mode:"REST"
});
 
dp.attachEvent("onBeforeUpdate", function(id, state, data){
    data.projectId = "1";
    return true;
});

In order to change the backend URL dynamically, use the dataProcessor.url method:

dp.url("/url");

Triggering Data Saving from Script

If you have dataProcessor initialized, any change made by the user or programmatically will be automatically saved in the data source.

Generally, to update a specific task or dependency programmatically, use the updateTask and updateLink methods, respectively:

gantt.parse([
   {id:1, start_date:"2019-05-13 6:00", end_date:"2019-05-13 8:00", text:"Event 1"},
   {id:2, start_date:"2019-06-09 6:00", end_date:"2019-06-09 8:00", text:"Event 2"}
],"json");
 
gantt.getTask(1).text = "Task 111"; //changes task's data
gantt.updateTask(1); // renders the updated task

Other methods that invoke sending an update to the backend:

Custom Routing

In case RESTful AJAX API isn't what you need on the backend, or if you want to manually control what is sent to the server, you can make use of custom routing.

For example, if you use Angular, React, or any other framework where a component on a page doesn't send changes directly to the server, but passes them to a different component which is responsible for data saving.

To provide custom routing options for DataProcessor, you should use the createDataProcessor() method:

gantt.createDataProcessor(function(entity, action, data, id){
    const services = {
        "task": this.taskService,
        "link": this.linkService
    };
    const service = services[entity];
 
    switch (action) {
        case "update":
            return service.update(data);
        case "create":
            return service.insert(data);
        case "delete":
            return service.remove(id);
    }
});

Related sample:  Custom data api - using local storage

Using AJAX for setting custom routers

Gantt AJAX module can be useful for setting custom routes. Gantt expects a custom router to return a Promise object as a result of an operation, which allows catching the end of an action. The AJAX module supports promises and is suitable for usage inside of custom routers. Gantt will get Promise and process the content of Promise, when it is resolved.

In the example below a new task is created. If the server response includes the id of a newly created task, Gantt will be able to apply it.

gantt.createDataProcessor(function(entity, action, data, id){
...
 
  switch (action) {
    case "create":
      return gantt.ajax.post({
        headers: { 
          "Content-Type": "application/json" 
        },
        url: server + "/task",
        data: JSON.stringify(data)
      });
    break;
  }
});

Routing CRUD actions of resources and resource assignments

From v8.0, modified resource assignments can be sent to the dataProcessor as separate entries with persistent IDs, so making it easy to connect to backend API. Changes of resource objects can be also sent to the DataProcessor.

Note, this feature is disabled by default. By default, the dataProcessor only receives changes made to tasks and links. To enable the feature, use the following settings:

gantt.config.resources = {
    dataprocessor_assignments: true,
    dataprocessor_resources: true,
};

Once the resource mode of the dataProcessor is enabled, if the DataProcessor is configured to the REST mode - resources and resource assignments will be sent to the backend in separate requests.

If you use the dataProcessor in the Custom Routing mode, you'll be able to capture changes of resource assignments and resources in the handler:

gantt.createDataProcessor({
    task: {
        create: (data) => {
            return createRecord({type: "task", ...data}).then((res) => {
                return { tid: res.id, ...res };
            });
        },
        update: (data, id) => {
            return updateRecord({type: "task", ...data}).then(() => ({}));
        },
        delete: (id) => {
            return deleteRecord({type: "task:", id: id}).then(() => ({}));
        }
    },
    link: {
        create: (data) => {
            ...
        },
        update: (data, id) => {
            ...
        },
        delete: (id) => {
            ...
        }
    },
    assignment: {
        create: (data) => {
            ...
        },
        update: (data, id) => {
            ...
        },
        delete: (id) => {
            ...
        }
    },
    resource: {
        create: (data) => {
            ...
        },
        update: (data, id) => {
            ...
        },
        delete: (id) => {
            ...
        }
    }
});

Or, using function declaration:

gantt.createDataProcessor(function(entity, action, data, id){
    switch (entity) {
        case "task":
            break;
        case "link":
            break;
        case "resource":
            break;
        case "assignment":
            break;
    }
});

Error Handling

A server can inform Gantt that an action has failed by returning the "action":"error" response:

{"action":"error"}

Such a response can be captured on the client with the help of gantt.dataProcessor:

var dp = gantt.createDataProcessor({
  url: "/api",
  mode:"REST"
});
dp.attachEvent("onAfterUpdate", function(id, action, tid, response){
    if(action == "error"){
        // do something here
    }
});

The response object may contain any number of additional properties, they can be accessed via the response argument of the onAfterUpdate handler.

This event will be called only for managed errors that return JSON response as shown above. If you need to handle HTTP errors, please check onAjaxError API event.

If the server responded with an error on some of your action but the changes were saved on the client, the best way to synchronize their states is to clear the client's state, and reload the correct data from the server-side:

dp.attachEvent("onAfterUpdate", function(id, action, tid, response){
    if(action == "error"){
        gantt.clearAll();
        gantt.load("url1");
    }
});

If you want to synchronize client-server sides but don't want to make any server calls, you can use the silent() method which makes all code inside it not to trigger internal events or server calls:

gantt.silent(function(){
    gantt.deleteTask(item.id);
});
 
gantt.render();

Cascade Deletion

By default, deletion of a task invokes a chain deletion of its nested task and related links. Gantt will send a delete request for each removed task and link. Thus, you don't have to maintain data integrity on a backend, it can be handled by the Gantt reasonably well.

On the other hand, this strategy can generate a large number of AJAX calls to the backend API, since dhtmlxGantt has no batch-request support for AJAX and the number of tasks and links is not limited.

In that case, cascade deletion can be disabled using the cascade_delete config. Thus, when a project branch is deleted, the client will send a delete request only for the top item and will expect the backend to delete the related links and subtasks.

XSS, CSRF and SQL Injection Attacks

Pay attention that Gantt doesn't provide any means of preventing an application from various threats, such as SQL injections or XSS and CSRF attacks. It is important that responsibility for keeping an application safe is on the developers implementing the backend.

Check the Application Security article to learn the most vulnerable points of the component and the measures you can take to improve the safety of your application.

Back to top