Vue/Inline editors
Loading live demo…from dhtmlx.github.io
Vue
Inline editors
Use custom inline editors for text, duration, and date fields directly in the Gantt grid.
- Demo.vue
- DateEditor.vue
- DurationEditor.vue
- TextEditor.vue
- shared/applyBatchChanges.ts
- shared/date.ts
- shared/projectData.ts
src/examples/inline-editors/Demo.vue View on GitHub
<script setup lang="ts">
import { ref } from "vue";
import {
VueGantt,
type BatchChanges,
type GanttConfigOptions,
type Link,
type Task
} from "@dhtmlx/trial-vue-gantt";
import "@dhtmlx/trial-vue-gantt/dist/vue-gantt.css";
import { applyBatchChanges } from "../shared/applyBatchChanges";
import { formatDateYMD } from "../shared/date";
import { createProjectData } from "../shared/projectData";
import DateEditor from "./editors/DateEditor.vue";
import DurationEditor from "./editors/DurationEditor.vue";
import TextEditor from "./editors/TextEditor.vue";
const dataSet = createProjectData();
const tasks = ref<Task[]>(dataSet.tasks);
const links = ref<Link[]>(dataSet.links);
const config: Partial<GanttConfigOptions> = {
row_height: 40,
scale_height: 68,
scales: [
{ unit: "year", step: 1, format: "%Y" },
{ unit: "month", step: 1, format: "%F, %Y" },
{ unit: "day", step: 1, format: "%d %M" }
],
columns: [
{
name: "text",
tree: true,
width: 220,
resize: true,
editor: { type: "TextEditor", map_to: "text" }
},
{
name: "start_date",
align: "center",
width: 170,
resize: true,
template: (task: Task) => formatDateYMD(task.start_date),
editor: { type: "DateEditor", map_to: "start_date", min: 0 }
},
{
name: "duration",
align: "center",
width: 90,
resize: true,
editor: { type: "DurationEditor", map_to: "duration", min: 0 }
},
{ name: "add", width: 44 }
]
};
const inlineEditors = {
TextEditor,
DurationEditor,
DateEditor
};
const data = {
batchSave: (changes: BatchChanges) => {
const next = applyBatchChanges(tasks.value, links.value, changes);
tasks.value = next.tasks;
links.value = next.links;
}
};
</script>
<template>
<section class="demo-panel" data-cy="inline-editors-demo">
<VueGantt
class="demo-gantt"
:tasks="tasks"
:links="links"
:config="config"
:inlineEditors="inlineEditors"
:data="data"
/>
</section>
</template>
src/examples/inline-editors/editors/DateEditor.vue View on GitHub
<script setup lang="ts">
import { nextTick, onMounted, onUpdated, ref, watch } from "vue";
import { formatDateYMD, parseInputDate } from "../../shared/date";
interface DateEditorProps {
initialValue: any;
save: () => void;
cancel: () => void;
}
const props = defineProps<DateEditorProps>();
const inputRef = ref<any>(null);
const value = ref("");
const normalize = (nextValue: any) => {
if (nextValue === null || nextValue === undefined || nextValue === "") {
return "";
}
return formatDateYMD(nextValue);
};
watch(
() => props.initialValue,
nextValue => {
value.value = normalize(nextValue);
},
{ immediate: true }
);
const getInputElement = () => {
const candidate = inputRef.value?.$el ?? inputRef.value;
if (!candidate || typeof candidate.querySelector !== "function") {
return null;
}
return candidate.querySelector("input") as HTMLInputElement | null;
};
const syncInputHook = () => {
const input = getInputElement();
input?.setAttribute("data-cy", "inline-date-input");
return input;
};
const focus = () => {
inputRef.value?.focus?.();
syncInputHook()?.focus();
};
const setValue = (nextValue: any) => {
value.value = normalize(nextValue);
};
const getValue = () => parseInputDate(value.value);
const isChanged = (rawValue: any) => normalize(rawValue) !== value.value;
const isValid = () => Boolean(value.value);
const save = () => props.save();
onMounted(async () => {
await nextTick();
syncInputHook();
focus();
});
onUpdated(() => {
syncInputHook();
});
defineExpose({
focus,
setValue,
getValue,
isChanged,
isValid,
save
});
</script>
<template>
<v-sheet class="editor-shell" border elevation="2" rounded="lg" data-cy="inline-date-editor">
<v-text-field
ref="inputRef"
v-model="value"
class="editor-field editor-field-date"
density="compact"
hide-details
type="date"
variant="outlined"
/>
</v-sheet>
</template>
<style scoped>
.editor-shell {
display: flex;
align-items: center;
gap: 8px;
border-radius: var(--dhx-gantt-border-radius);
/* padding: 6px; */
background: #fff;
border: 1px solid #b7c8d8;
}
.editor-field {
flex: 0 0 auto;
}
.editor-field-date {
width: 100%;
min-width: 100%;
}
</style>
src/examples/inline-editors/editors/DurationEditor.vue View on GitHub
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from "vue";
interface DurationEditorProps {
initialValue: any;
save: () => void;
cancel: () => void;
}
const props = defineProps<DurationEditorProps>();
const inputRef = ref<any>(null);
const value = ref(1);
const normalize = (nextValue: any) => {
const numberValue = Number(nextValue);
if (Number.isNaN(numberValue) || numberValue < 0) {
return 0;
}
return numberValue;
};
watch(
() => props.initialValue,
nextValue => {
value.value = normalize(nextValue);
},
{ immediate: true }
);
const getInputElement = () => {
const candidate = inputRef.value?.$el ?? inputRef.value;
if (!candidate || typeof candidate.querySelector !== "function") {
return null;
}
return candidate.querySelector("input") as HTMLInputElement | null;
};
const focus = () => {
inputRef.value?.focus?.();
getInputElement()?.focus();
};
const setValue = (nextValue: any) => {
value.value = normalize(nextValue);
};
const onModelUpdate = (nextValue: any) => {
value.value = normalize(nextValue);
};
const getValue = () => value.value;
const isChanged = (rawValue: any) => normalize(rawValue) !== value.value;
const isValid = () => value.value >= 0;
const save = () => props.save();
onMounted(async () => {
await nextTick();
focus();
});
defineExpose({
focus,
setValue,
getValue,
isChanged,
isValid,
save
});
</script>
<template>
<v-sheet class="editor-shell" border elevation="2" rounded="lg">
<v-text-field
ref="inputRef"
:model-value="String(value)"
class="editor-field editor-field-duration"
density="compact"
hide-details
min="0"
step="1"
type="number"
variant="outlined"
@update:model-value="onModelUpdate"
/>
</v-sheet>
</template>
<style scoped>
.editor-shell {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
}
.editor-field {
flex: 0 0 auto;
}
.editor-field-duration {
width: 100%;
min-width: 100%;
}
</style>
src/examples/inline-editors/editors/TextEditor.vue View on GitHub
<script setup lang="ts">
import { nextTick, onMounted, ref, watch } from "vue";
interface TextEditorProps {
initialValue: any;
save: () => void;
cancel: () => void;
}
const props = defineProps<TextEditorProps>();
const inputRef = ref<any>(null);
const value = ref("");
const normalize = (nextValue: any) => String(nextValue ?? "");
watch(
() => props.initialValue,
nextValue => {
value.value = normalize(nextValue);
},
{ immediate: true }
);
const getInputElement = () => {
const candidate = inputRef.value?.$el ?? inputRef.value;
if (!candidate || typeof candidate.querySelector !== "function") {
return null;
}
return candidate.querySelector("input") as HTMLInputElement | null;
};
const focus = () => {
inputRef.value?.focus?.();
getInputElement()?.focus();
};
const setValue = (nextValue: any) => {
value.value = normalize(nextValue);
};
const getValue = () => value.value;
const isChanged = (rawValue: any) => normalize(rawValue) !== value.value;
const isValid = () => value.value.trim().length > 0;
const save = () => props.save();
onMounted(async () => {
await nextTick();
focus();
});
defineExpose({
focus,
setValue,
getValue,
isChanged,
isValid,
save
});
</script>
<template>
<v-sheet class="editor-shell" border elevation="2" rounded="lg">
<v-text-field
ref="inputRef"
v-model="value"
class="editor-field editor-field-text"
density="compact"
hide-details
type="text"
variant="outlined"
/>
</v-sheet>
</template>
<style scoped>
.editor-shell {
display: flex;
align-items: center;
gap: 8px;
background: #fff;
}
.editor-field {
flex: 0 0 auto;
}
.editor-field-text {
width: 100%;
min-width: 100%;
}
</style>
src/examples/shared/applyBatchChanges.ts View on GitHub
import type { BatchChanges, DataCallbackChange, Link, Task } from "@dhtmlx/trial-vue-gantt";
const toId = (value: string | number) => String(value);
export function applyTaskChanges(prevTasks: Task[], changes: DataCallbackChange[] = []): Task[] {
const nextTasks = [...prevTasks];
changes.forEach(change => {
const index = nextTasks.findIndex(task => toId(task.id as string | number) === toId(change.id));
if (change.action === "update" && index !== -1) {
nextTasks[index] = { ...nextTasks[index], ...(change.data as Task) };
return;
}
if (change.action === "create") {
nextTasks.push(change.data as Task);
return;
}
if (change.action === "delete" && index !== -1) {
nextTasks.splice(index, 1);
}
});
return nextTasks;
}
export function applyLinkChanges(prevLinks: Link[], changes: DataCallbackChange[] = []): Link[] {
const nextLinks = [...prevLinks];
changes.forEach(change => {
const index = nextLinks.findIndex(link => toId(link.id as string | number) === toId(change.id));
if (change.action === "update" && index !== -1) {
nextLinks[index] = { ...nextLinks[index], ...(change.data as Link) };
return;
}
if (change.action === "create") {
nextLinks.push(change.data as Link);
return;
}
if (change.action === "delete" && index !== -1) {
nextLinks.splice(index, 1);
}
});
return nextLinks;
}
export function applyBatchChanges(
tasks: Task[],
links: Link[],
changes: BatchChanges
): { tasks: Task[]; links: Link[] } {
return {
tasks: applyTaskChanges(tasks, changes.tasks),
links: applyLinkChanges(links, changes.links)
};
}
src/examples/shared/date.ts View on GitHub
const pad = (value: number) => String(value).padStart(2, "0");
export function formatDateYMD(input: unknown): string {
const date = input instanceof Date ? input : new Date(input as string | number | Date);
if (Number.isNaN(date.getTime())) {
return "";
}
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
return `${year}-${month}-${day}`;
}
export function parseInputDate(input: string): Date {
const [year, month, day] = input.split("-").map(Number);
if (!year || !month || !day) {
return new Date(input);
}
return new Date(year, month - 1, day);
}
src/examples/shared/projectData.ts View on GitHub
import type { Link, Task } from "@dhtmlx/trial-vue-gantt";
const baseTasks: Task[] = [
{ id: 1, text: "Office itinerancy", type: "project", start_date: new Date(2026, 6, 2), duration: 17, progress: 0.4, parent: 0, open: true },
{ id: 2, text: "Office facing", type: "project", start_date: new Date(2026, 6, 2), duration: 8, progress: 0.6, parent: 1, open: true },
{ id: 3, text: "Furniture installation", type: "project", start_date: new Date(2026, 6, 11), duration: 8, progress: 0.6, parent: 1, open: true },
{ id: 4, text: "The employee relocation", type: "project", start_date: new Date(2026, 6, 13), duration: 5, progress: 0.5, parent: 1, priority: 3, open: true },
{ id: 5, text: "Interior office", type: "task", start_date: new Date(2026, 6, 3), duration: 7, progress: 0.6, parent: 2, priority: 1 },
{ id: 6, text: "Air conditioners check", type: "task", start_date: new Date(2026, 6, 3), duration: 7, progress: 0.6, parent: 2, priority: 2 },
{ id: 7, text: "Workplaces preparation", type: "task", start_date: new Date(2026, 6, 12), duration: 8, progress: 0.6, parent: 3 },
{ id: 8, text: "Preparing workplaces", type: "task", start_date: new Date(2026, 6, 14), duration: 5, progress: 0.5, parent: 4, priority: 1 },
{ id: 9, text: "Workplaces importation", type: "task", start_date: new Date(2026, 6, 21), duration: 4, progress: 0.5, parent: 4 },
{ id: 10, text: "Workplaces exportation", type: "task", start_date: new Date(2026, 6, 27), duration: 3, progress: 0.5, parent: 4, priority: 2 },
{ id: 11, text: "Product launch", type: "project", start_date: new Date(2026, 6, 2), duration: 13, progress: 0.6, parent: 0, open: true },
{ id: 12, text: "Perform Initial testing", type: "task", start_date: new Date(2026, 6, 3), duration: 5, progress: 1, parent: 11 },
{ id: 13, text: "Development", type: "project", start_date: new Date(2026, 6, 3), duration: 11, progress: 0.5, parent: 11, open: true },
{ id: 14, text: "Analysis", type: "task", start_date: new Date(2026, 6, 3), duration: 6, progress: 0.8, parent: 11 },
{ id: 15, text: "Design", type: "project", start_date: new Date(2026, 6, 3), duration: 5, progress: 0.2, parent: 11, open: true },
{ id: 16, text: "Documentation creation", type: "task", start_date: new Date(2026, 6, 3), duration: 7, progress: 0, parent: 11, priority: 1 },
{ id: 17, text: "Develop System", type: "task", start_date: new Date(2026, 6, 3), duration: 2, progress: 1, parent: 13, priority: 2 },
{ id: 25, text: "Beta Release", type: "milestone", start_date: new Date(2026, 6, 6), duration: 0, progress: 0, parent: 13 },
{ id: 18, text: "Integrate System", type: "task", start_date: new Date(2026, 6, 10), duration: 2, progress: 0.8, parent: 13, priority: 3 },
{ id: 19, text: "Test", type: "task", start_date: new Date(2026, 6, 13), duration: 4, progress: 0.2, parent: 13 },
{ id: 20, text: "Marketing", type: "task", start_date: new Date(2026, 6, 13), duration: 4, progress: 0, parent: 13, priority: 1 },
{ id: 21, text: "Design database", type: "task", start_date: new Date(2026, 6, 3), duration: 4, progress: 0.5, parent: 15 },
{ id: 22, text: "Software design", type: "task", start_date: new Date(2026, 6, 3), duration: 4, progress: 0.1, parent: 15, priority: 1 },
{ id: 23, text: "Interface setup", type: "task", start_date: new Date(2026, 6, 3), duration: 5, progress: 0, parent: 15, priority: 1 },
{ id: 24, text: "Release v1.0", type: "milestone", start_date: new Date(2026, 6, 18), duration: 0, progress: 0, parent: 11 }
];
const baseLinks: 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" }
];
const cloneDate = (value: Date | undefined): Date | undefined => {
if (value instanceof Date) {
return new Date(value.getTime());
}
return value;
};
const cloneTask = (task: Task): Task => {
const next: Task = { ...task };
next.start_date = cloneDate(task.start_date);
next.end_date = cloneDate(task.end_date);
return next;
};
const cloneLink = (link: Link): Link => ({ ...link });
export function createProjectData() {
return {
tasks: baseTasks.map(cloneTask),
links: baseLinks.map(cloneLink)
};
}