React/Custom edit view
Loading live demo…from dhtmlx.github.io
React
Custom edit view
Split task editing into a dedicated React Router view with shared Gantt context.
- GanttEditorViewDemo.tsx
- Gantt.tsx
- TaskEditor.tsx
src/examples/custom-edit-view/GanttEditorViewDemo.tsx View on GitHub
import React, { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Task, Link } from "@dhtmlx/trial-react-gantt";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
export interface GanttEditorContext {
tasks: Task[];
links: Link[];
handleSaveTask: (updated: Task) => void;
handleDeleteTask: (id: string) => void;
createTask: (newTask: Task) => void;
}
export default function GanttssEditorViewDemo() {
const [tasks, setTasks] = useState<Task[]>([
{ id: 1, text: "Office itinerancy", type: "project", start_date: new Date(2025, 3, 2), duration: 17, progress: 0.4, parent: 0, open: true },
{ id: 2, text: "Office facing", type: "project", start_date: new Date(2025, 3, 2), duration: 8, progress: 0.6, parent: 1, open: true },
{ id: 3, text: "Furniture installation", type: "project", start_date: new Date(2025, 3, 11), duration: 8, progress: 0.6, parent: 1, open: true },
{ id: 4, text: "The employee relocation", type: "project", start_date: new Date(2025, 3, 13), duration: 5, progress: 0.5, parent: 1, priority: 3, open: true },
{ id: 5, text: "Interior office", type: "task", start_date: new Date(2025, 3, 3), duration: 7, progress: 0.6, parent: 2, priority: 1 },
{ id: 6, text: "Air conditioners check", type: "task", start_date: new Date(2025, 3, 3), duration: 7, progress: 0.6, parent: 2, priority: 2 },
{ id: 7, text: "Workplaces preparation", type: "task", start_date: new Date(2025, 3, 12), duration: 8, progress: 0.6, parent: 3 },
{ id: 8, text: "Preparing workplaces", type: "task", start_date: new Date(2025, 3, 14), duration: 5, progress: 0.5, parent: 4, priority: 1 },
{ id: 9, text: "Workplaces importation", type: "task", start_date: new Date(2025, 3, 21), duration: 4, progress: 0.5, parent: 4 },
{ id: 10, text: "Workplaces exportation", type: "task", start_date: new Date(2025, 3, 27), duration: 3, progress: 0.5, parent: 4, priority: 2 },
{ id: 11, text: "Product launch", type: "project", start_date: new Date(2025, 3, 2), duration: 13, progress: 0.6, parent: 0, open: true },
{ id: 12, text: "Perform Initial testing", type: "task", start_date: new Date(2025, 3, 3), duration: 5, progress: 1, parent: 11 },
{ id: 13, text: "Development", type: "project", start_date: new Date(2025, 3, 3), duration: 11, progress: 0.5, parent: 11, open: true },
{ id: 14, text: "Analysis", type: "task", start_date: new Date(2025, 3, 3), duration: 6, progress: 0.8, parent: 11 },
{ id: 15, text: "Design", type: "project", start_date: new Date(2025, 3, 3), duration: 5, progress: 0.2, parent: 11, open: true },
{ id: 16, text: "Documentation creation", type: "task", start_date: new Date(2025, 3, 3), duration: 7, progress: 0, parent: 11, priority: 1 },
{ id: 17, text: "Develop System", type: "task", start_date: new Date(2025, 3, 3), duration: 2, progress: 1, parent: 13, priority: 2 },
{ id: 25, text: "Beta Release", type: "milestone", start_date: new Date(2025, 3, 6), duration: 0, progress: 0, parent: 13 },
{ id: 18, text: "Integrate System", type: "task", start_date: new Date(2025, 3, 10), duration: 2, progress: 0.8, parent: 13, priority: 3 },
{ id: 19, text: "Test", type: "task", start_date: new Date(2025, 3, 13), duration: 4, progress: 0.2, parent: 13 },
{ id: 20, text: "Marketing", type: "task", start_date: new Date(2025, 3, 13), duration: 4, progress: 0, parent: 13, priority: 1 },
{ id: 21, text: "Design database", type: "task", start_date: new Date(2025, 3, 3), duration: 4, progress: 0.5, parent: 15 },
{ id: 22, text: "Software design", type: "task", start_date: new Date(2025, 3, 3), duration: 4, progress: 0.1, parent: 15, priority: 1 },
{ id: 23, text: "Interface setup", type: "task", start_date: new Date(2025, 3, 3), duration: 5, progress: 0, parent: 15, priority: 1 },
{ id: 24, text: "Release v1.0", type: "milestone", start_date: new Date(2025, 3, 18), duration: 0, progress: 0, parent: 11 }
]);
const [links, setLinks] = useState<Link[]>([
{ id: 2, source: 2, target: 3, type: "0" },
{ id: 3, source: 3, target: 4, type: "0" },
{ id: 7, source: 8, target: 9, type: "0" },
{ id: 8, source: 9, target: 10, type: "0" },
{ id: 16, source: 17, target: 25, type: "0" },
{ id: 17, source: 18, target: 19, type: "0" },
{ id: 18, source: 19, target: 20, type: "0" },
{ id: 22, source: 13, target: 24, type: "0" },
{ id: 23, source: 25, target: 18, type: "0" }
]);
function handleSaveTask(updated: Task) {
setTasks((prev) =>
prev.map((t) => (t.id === updated.id ? { ...updated, $new: undefined, end_date: undefined } : t))
);
}
function handleDeleteTask(id: string) {
setTasks(prev => prev.filter(t => t.id !== id));
}
function createTask(newTask: Task) {
setTasks((prev) => {
const exists = prev.find((t) => t.id === newTask.id);
if (!exists) {
return [...prev, newTask];
} else {
return prev.map((t) => (t.id === newTask.id ? newTask : t));
}
});
}
return (
<div className="demo-container">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<Outlet
context={{
tasks,
links,
handleSaveTask,
handleDeleteTask,
createTask,
} satisfies GanttEditorContext}
/>
</LocalizationProvider>
</div>
);
}
src/examples/custom-edit-view/Gantt.tsx View on GitHub
import React, {useEffect} from 'react';
import { useNavigate } from 'react-router-dom';
import ReactGantt, { ReactGanttRef, Task, Link } from "@dhtmlx/trial-react-gantt";
import "@dhtmlx/trial-react-gantt/dist/react-gantt.css";
import { useOutletContext } from 'react-router-dom';
import type { GanttEditorContext } from './GanttEditorViewDemo';
export default function BasicInitDemo() {
const ganttRef = React.useRef<ReactGanttRef>(null);
useEffect(() => {
document.title = "DHTMLX React Gantt | Custom Edit View";
}, []);
const navigate = useNavigate();
const { tasks, links, handleSaveTask, handleDeleteTask, createTask } = useOutletContext<GanttEditorContext>();
const data = {
save: (entity: string, action: string, raw: any, id: string | number) => {
if (entity === 'task') {
if (action === 'update') {
handleSaveTask(raw);
} else if (action === 'create') {
createTask(raw);
} else if (action === 'delete') {
handleDeleteTask(String(id));
}
}
}
};
function handleTaskCreated(ganttTask: Task) {
ganttTask.$new = true;
createTask(ganttTask);
navigate(`editor/${ganttTask.id}`);
return false;
}
function handleEditTask(taskId: string|number) {
navigate(`editor/${taskId}`);
return false;
}
return (
<div style={{ width: "100%", height: "100%" }}>
<ReactGantt
ref={ganttRef}
tasks={tasks}
links={links}
data={data}
onTaskCreated={handleTaskCreated}
onBeforeLightbox={handleEditTask}
/>
</div>
);
}
src/examples/custom-edit-view/TaskEditor.tsx View on GitHub
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, useOutletContext } from 'react-router-dom';
import { Task } from "@dhtmlx/trial-react-gantt";
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import dayjs, { Dayjs } from 'dayjs';
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
import Slider from "@mui/material/Slider";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import type { GanttEditorContext } from './GanttEditorViewDemo';
export default function TaskEditor() {
useEffect(() => {
document.title = "DHTMLX React Gantt | Custom Edit View | Task Editor";
}, []);
const { id } = useParams();
const navigate = useNavigate();
const { tasks, handleSaveTask, handleDeleteTask } = useOutletContext<GanttEditorContext>();
const numId = Number(id);
const existing = tasks.find(t => t.id === numId) || {
id: numId,
text: "",
progress: 0,
duration: 1,
start_date: new Date(),
};
const [formData, setFormData] = useState<Task>({
...existing
});
const [startDateValue, setStartDateValue] = useState<Dayjs>(
dayjs(formData.start_date)
);
function handleChange(field: keyof Task, value: any) {
setFormData((prev:Task) => ({ ...prev, [field]: value }));
}
function handleSave() {
handleSaveTask(formData);
navigate("..");
}
function handleDelete() {
handleDeleteTask(String(formData.id));
navigate("..");
}
function handleCancel() {
navigate("..");
}
return (
<Box sx={{ width: 380, p: 2, border: "1px solid #ccc" }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Editing Task {formData.id}
</Typography>
<TextField
label="Task Name"
fullWidth
sx={{ mb: 2 }}
value={formData.text}
onChange={e => handleChange("text", e.target.value)}
/>
<DatePicker
label="Start Date"
value={startDateValue}
disabled={(formData.type === "project")}
onChange={newVal => {
if (newVal) {
setStartDateValue(newVal);
handleChange("start_date", newVal.toDate());
}
}}
/>
<TextField
label="Duration"
type="number"
disabled={(formData.type === "project")}
fullWidth
sx={{ mt: 2, mb: 2 }}
value={formData.duration ?? 1}
onChange={e => handleChange("duration", parseInt(e.target.value, 10) || 1)}
/>
<Typography gutterBottom>Progress (%)</Typography>
<Slider
min={0}
max={100}
value={Math.round((formData.progress || 0) * 100)}
onChange={(_, val) => {
const fraction = (Array.isArray(val) ? val[0] : val) / 100;
handleChange("progress", fraction);
}}
valueLabelDisplay="auto"
sx={{ mb: 3 }}
/>
<Stack direction="row" spacing={2}>
<Button variant="contained" onClick={handleSave}>
Save
</Button>
<Button variant="outlined" onClick={handleCancel}>
Cancel
</Button>
<Button variant="contained" color="error" onClick={handleDelete}>
Delete
</Button>
</Stack>
</Box>
);
}