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.
Generally, to load data from the server side using REST API, you need to:
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:
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;
var dp = gantt.createDataProcessor({
url: "apiUrl",
mode: "REST",
deleteAfterConfirmation: true
});
Check the detailed info in the next section.
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:
2. Provide a custom router object:
var dp = gantt.createDataProcessor(router);
// 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;
}
});
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
The URL is formed by the following rule:
where "api" is the URL you've specified in the dataProcessor configuration.
The list of possible requests and responses is:
Action | HTTP Method | URL | Response |
---|---|---|---|
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.
Create/Update/Delete requests will contain all public properties of a client-side task or link object:
Task:
Link:
Note:
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.
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
{
"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"}
}]
}
{
"source": 1,
"target": 2,
"type": "0"
}
This format makes processing of complex records handier on any server-side platform.
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:
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.
MAX(sortorder) + 1
sortorder.HTTP Method | URL | Parameters | Response |
---|---|---|---|
PUT | /apiUrl/task/taskId |
|
{"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:
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.
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)
});
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");
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:
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
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;
}
});
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;
}
});
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();
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.
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