Task Editing
Task edits route through mutation services and scheduler recomputation. Before applying task changes, Gantt emits cancelable gantt-before-task-change; dependency field edits emit gantt-before-dependency-change; assignee edits emit gantt-before-assignment-change.
Command/event surface is defined in:
packages/gantt/src/gantt/grid/gantt-events.tspackages/gantt/src/gantt/grid/gantt-plugin.ts
For audited changes, pair with Gantt history tooling (features/history).
Inline Editing
Section titled “Inline Editing”Source code
---
import GanttScheduling from '@revolist/revogrid-examples/components/gantt/GanttScheduling.vue';
---
<GanttScheduling client:only="vue" />// src/components/gantt/GanttScheduling.ts
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/gantt';
import type { GanttTaskSourceRow, DependencyEntity, CalendarEntity } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';
// Calendar-aware scheduling: durations are in working days (Mon–Fri, excluding holidays)
const ganttConfig = {
id: PROJECT_ID,
name: 'Website Redesign',
version: '1',
currency: 'USD',
timeZone: 'America/New_York',
primaryCalendarId: CALENDAR_ID,
updatedAt: '2026-04-06T00:00:00Z',
statusDate: '2026-04-06',
zoomPreset: 'week' as const,
scheduling: {
excludeHolidaysFromDuration: true, // durations skip weekends and holidays
},
visuals: {
shadeNonWorkingTime: true, // shade weekend columns on the timeline
projectLineDate: '2026-04-06', // vertical status-date line
},
};
// US calendar with public holidays
const calendars: CalendarEntity[] = [
{
id: CALENDAR_ID,
name: 'US Standard',
timeZone: 'America/New_York',
workingDays: [1, 2, 3, 4, 5], // Mon–Fri
holidays: [
'2026-05-25', // Memorial Day
'2026-07-04', // Independence Day
],
hoursPerDay: 8,
},
];
const tasks: GanttTaskSourceRow[] = [
{
id: 't1', parentId: null,
name: 'Design', type: 'summary', workflowStatus: 'in-progress',
startDate: '2026-04-06', endDate: '2026-04-24', duration: 15,
percentDone: 60, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't2', parentId: 't1',
name: 'Wireframes', type: 'task', workflowStatus: 'done',
startDate: '2026-04-06', endDate: '2026-04-10', duration: 5,
percentDone: 100, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't3', parentId: 't1',
name: 'Design Review', type: 'milestone', workflowStatus: 'done',
startDate: '2026-04-10', endDate: '2026-04-10', duration: 0,
percentDone: 100, calendarId: CALENDAR_ID, tags: ['milestone'],
},
{
id: 't4', parentId: 't1',
name: 'Visual Design', type: 'task', workflowStatus: 'in-progress',
startDate: '2026-04-13', endDate: '2026-04-24', duration: 10,
percentDone: 40, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't5', parentId: null,
name: 'Development', type: 'summary', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't6', parentId: 't5',
name: 'Frontend', type: 'task', workflowStatus: 'not-started',
// constraint: cannot start before May 4 (waiting on external API)
startDate: '2026-05-04', endDate: '2026-05-20', duration: 13,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
constraintType: 'start-no-earlier-than',
constraintDate: '2026-05-04',
},
{
id: 't7', parentId: 't5',
name: 'Backend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
deadlineDate: '2026-05-22', // deadline marker — project expects early delivery
},
{
id: 't8', parentId: null,
name: 'Launch', type: 'milestone', workflowStatus: 'not-started',
startDate: '2026-05-28', endDate: '2026-05-28', duration: 0,
percentDone: 0, calendarId: CALENDAR_ID, tags: ['milestone'],
},
];
const dependencies: DependencyEntity[] = [
{ id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
{ id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
{ id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
{ id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
{ id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
{ id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];
export function load(parentSelector: string) {
const grid = document.createElement('revo-grid');
grid.theme = isDark() ? 'darkCompact' : 'compact';
grid.hideAttribution = true;
grid.plugins = [GanttPlugin];
grid.gantt = ganttConfig;
grid.ganttCalendars = calendars;
grid.ganttDependencies = dependencies;
grid.source = tasks;
grid.columns = [
createDefaultTaskTableColumn('wbs'),
createDefaultTaskTableColumn('name'),
];
document.querySelector(parentSelector)?.appendChild(grid);
}
<template>
<RevoGrid
hide-attribution
style="height: 500px"
:theme="isDark ? 'darkCompact' : 'compact'"
:plugins="plugins"
:source="tasks"
:columns="columns"
:gantt.prop="ganttConfig"
:gantt-dependencies.prop="dependencies"
:gantt-calendars.prop="calendars"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/gantt';
import type { GanttTaskSourceRow, DependencyEntity, CalendarEntity } from '@revolist/gantt';
import { currentThemeVue } from '../composables/useRandomData';
const { isDark } = currentThemeVue();
const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';
const plugins = ref([GanttPlugin]);
const ganttConfig = ref({
id: PROJECT_ID,
name: 'Website Redesign',
version: '1',
currency: 'USD',
timeZone: 'America/New_York',
primaryCalendarId: CALENDAR_ID,
updatedAt: '2026-04-06T00:00:00Z',
statusDate: '2026-04-06',
zoomPreset: 'week' as const,
scheduling: {
excludeHolidaysFromDuration: true,
},
visuals: {
shadeNonWorkingTime: true,
projectLineDate: '2026-04-06',
},
});
const calendars = ref<CalendarEntity[]>([
{
id: CALENDAR_ID,
name: 'US Standard',
timeZone: 'America/New_York',
workingDays: [1, 2, 3, 4, 5],
holidays: ['2026-05-25', '2026-07-04'],
hoursPerDay: 8,
},
]);
const tasks = ref<GanttTaskSourceRow[]>([
{
id: 't1', parentId: null,
name: 'Design', type: 'summary', workflowStatus: 'in-progress',
startDate: '2026-04-06', endDate: '2026-04-24', duration: 15,
percentDone: 60, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't2', parentId: 't1',
name: 'Wireframes', type: 'task', workflowStatus: 'done',
startDate: '2026-04-06', endDate: '2026-04-10', duration: 5,
percentDone: 100, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't3', parentId: 't1',
name: 'Design Review', type: 'milestone', workflowStatus: 'done',
startDate: '2026-04-10', endDate: '2026-04-10', duration: 0,
percentDone: 100, calendarId: CALENDAR_ID, tags: ['milestone'],
},
{
id: 't4', parentId: 't1',
name: 'Visual Design', type: 'task', workflowStatus: 'in-progress',
startDate: '2026-04-13', endDate: '2026-04-24', duration: 10,
percentDone: 40, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't5', parentId: null,
name: 'Development', type: 'summary', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't6', parentId: 't5',
name: 'Frontend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-05-04', endDate: '2026-05-20', duration: 13,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
constraintType: 'start-no-earlier-than',
constraintDate: '2026-05-04',
},
{
id: 't7', parentId: 't5',
name: 'Backend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
deadlineDate: '2026-05-22',
},
{
id: 't8', parentId: null,
name: 'Launch', type: 'milestone', workflowStatus: 'not-started',
startDate: '2026-05-28', endDate: '2026-05-28', duration: 0,
percentDone: 0, calendarId: CALENDAR_ID, tags: ['milestone'],
},
]);
const dependencies = ref<DependencyEntity[]>([
{ id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
{ id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
{ id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
{ id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
{ id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
{ id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
]);
const columns = ref([
createDefaultTaskTableColumn('wbs'),
createDefaultTaskTableColumn('name'),
]);
</script>
import React, { useMemo } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/gantt';
import type { GanttTaskSourceRow, DependencyEntity, CalendarEntity } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
const { isDark } = currentTheme();
const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';
const tasks: GanttTaskSourceRow[] = [
{
id: 't1', parentId: null,
name: 'Design', type: 'summary', workflowStatus: 'in-progress',
startDate: '2026-04-06', endDate: '2026-04-24', duration: 15,
percentDone: 60, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't2', parentId: 't1',
name: 'Wireframes', type: 'task', workflowStatus: 'done',
startDate: '2026-04-06', endDate: '2026-04-10', duration: 5,
percentDone: 100, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't3', parentId: 't1',
name: 'Design Review', type: 'milestone', workflowStatus: 'done',
startDate: '2026-04-10', endDate: '2026-04-10', duration: 0,
percentDone: 100, calendarId: CALENDAR_ID, tags: ['milestone'],
},
{
id: 't4', parentId: 't1',
name: 'Visual Design', type: 'task', workflowStatus: 'in-progress',
startDate: '2026-04-13', endDate: '2026-04-24', duration: 10,
percentDone: 40, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't5', parentId: null,
name: 'Development', type: 'summary', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't6', parentId: 't5',
name: 'Frontend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-05-04', endDate: '2026-05-20', duration: 13,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
constraintType: 'start-no-earlier-than',
constraintDate: '2026-05-04',
},
{
id: 't7', parentId: 't5',
name: 'Backend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
deadlineDate: '2026-05-22',
},
{
id: 't8', parentId: null,
name: 'Launch', type: 'milestone', workflowStatus: 'not-started',
startDate: '2026-05-28', endDate: '2026-05-28', duration: 0,
percentDone: 0, calendarId: CALENDAR_ID, tags: ['milestone'],
},
];
const dependencies: DependencyEntity[] = [
{ id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
{ id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
{ id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
{ id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
{ id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
{ id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];
const calendars: CalendarEntity[] = [
{
id: CALENDAR_ID,
name: 'US Standard',
timeZone: 'America/New_York',
workingDays: [1, 2, 3, 4, 5],
holidays: ['2026-05-25', '2026-07-04'],
hoursPerDay: 8,
},
];
function GanttScheduling() {
const ganttConfig = useMemo(() => ({
id: PROJECT_ID,
name: 'Website Redesign',
version: '1',
currency: 'USD',
timeZone: 'America/New_York',
primaryCalendarId: CALENDAR_ID,
updatedAt: '2026-04-06T00:00:00Z',
statusDate: '2026-04-06',
zoomPreset: 'week' as const,
scheduling: {
excludeHolidaysFromDuration: true,
},
visuals: {
shadeNonWorkingTime: true,
projectLineDate: '2026-04-06',
},
}), []);
const columns = useMemo(() => [
createDefaultTaskTableColumn('wbs'),
createDefaultTaskTableColumn('name'),
], []);
return (
<RevoGrid
style={{ height: '500px' }}
theme={isDark() ? 'darkCompact' : 'compact'}
hideAttribution
plugins={[GanttPlugin]}
source={tasks}
columns={columns}
gantt={ganttConfig}
ganttDependencies={dependencies}
ganttCalendars={calendars}
/>
);
}
export default GanttScheduling;
import { Component, NO_ERRORS_SCHEMA, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { GanttPlugin, createDefaultTaskTableColumn } from '@revolist/gantt';
import type { GanttTaskSourceRow, DependencyEntity, CalendarEntity } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
const PROJECT_ID = 'project-web-redesign';
const CALENDAR_ID = 'cal-us';
@Component({
selector: 'gantt-scheduling-grid',
standalone: true,
imports: [RevoGrid],
// Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
schemas: [NO_ERRORS_SCHEMA],
template: `
<revo-grid
style="min-height: 500px"
[theme]="theme"
[hideAttribution]="true"
[plugins]="plugins"
[source]="tasks"
[columns]="columns"
[gantt]="ganttConfig"
[ganttDependencies]="dependencies"
[ganttCalendars]="calendars"
></revo-grid>
`,
encapsulation: ViewEncapsulation.None,
})
export class GanttSchedulingGridComponent {
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
plugins = [GanttPlugin];
ganttConfig = {
id: PROJECT_ID,
name: 'Website Redesign',
version: '1',
currency: 'USD',
timeZone: 'America/New_York',
primaryCalendarId: CALENDAR_ID,
updatedAt: '2026-04-06T00:00:00Z',
statusDate: '2026-04-06',
zoomPreset: 'week' as const,
scheduling: {
excludeHolidaysFromDuration: true,
},
visuals: {
shadeNonWorkingTime: true,
projectLineDate: '2026-04-06',
},
};
calendars: CalendarEntity[] = [
{
id: CALENDAR_ID,
name: 'US Standard',
timeZone: 'America/New_York',
workingDays: [1, 2, 3, 4, 5],
holidays: ['2026-05-25', '2026-07-04'],
hoursPerDay: 8,
},
];
tasks: GanttTaskSourceRow[] = [
{
id: 't1', parentId: null,
name: 'Design', type: 'summary', workflowStatus: 'in-progress',
startDate: '2026-04-06', endDate: '2026-04-24', duration: 15,
percentDone: 60, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't2', parentId: 't1',
name: 'Wireframes', type: 'task', workflowStatus: 'done',
startDate: '2026-04-06', endDate: '2026-04-10', duration: 5,
percentDone: 100, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't3', parentId: 't1',
name: 'Design Review', type: 'milestone', workflowStatus: 'done',
startDate: '2026-04-10', endDate: '2026-04-10', duration: 0,
percentDone: 100, calendarId: CALENDAR_ID, tags: ['milestone'],
},
{
id: 't4', parentId: 't1',
name: 'Visual Design', type: 'task', workflowStatus: 'in-progress',
startDate: '2026-04-13', endDate: '2026-04-24', duration: 10,
percentDone: 40, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't5', parentId: null,
name: 'Development', type: 'summary', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
},
{
id: 't6', parentId: 't5',
name: 'Frontend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-05-04', endDate: '2026-05-20', duration: 13,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
constraintType: 'start-no-earlier-than',
constraintDate: '2026-05-04',
},
{
id: 't7', parentId: 't5',
name: 'Backend', type: 'task', workflowStatus: 'not-started',
startDate: '2026-04-27', endDate: '2026-05-28', duration: 24,
percentDone: 0, calendarId: CALENDAR_ID, tags: [],
deadlineDate: '2026-05-22',
},
{
id: 't8', parentId: null,
name: 'Launch', type: 'milestone', workflowStatus: 'not-started',
startDate: '2026-05-28', endDate: '2026-05-28', duration: 0,
percentDone: 0, calendarId: CALENDAR_ID, tags: ['milestone'],
},
];
dependencies: DependencyEntity[] = [
{ id: 'd1', predecessorTaskId: 't2', successorTaskId: 't3', type: 'finish-to-start', lagDays: 0 },
{ id: 'd2', predecessorTaskId: 't3', successorTaskId: 't4', type: 'finish-to-start', lagDays: 1 },
{ id: 'd3', predecessorTaskId: 't4', successorTaskId: 't6', type: 'finish-to-start', lagDays: 1 },
{ id: 'd4', predecessorTaskId: 't4', successorTaskId: 't7', type: 'finish-to-start', lagDays: 1 },
{ id: 'd5', predecessorTaskId: 't6', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
{ id: 'd6', predecessorTaskId: 't7', successorTaskId: 't8', type: 'finish-to-start', lagDays: 0 },
];
columns = [
createDefaultTaskTableColumn('wbs'),
createDefaultTaskTableColumn('name'),
];
}
Inline Gantt edits update the task through the Gantt mutation services, recompute the project, and emit history snapshots for user changes. Initial project hydration is not recorded as a history step.
Call event.preventDefault() in a before-change listener to reject a field edit before the task, dependency, or assignment store is mutated. Rejected predecessor/successor edits leave the old dependency rows in place; rejected assignee edits leave the old assignment rows in place.
Edit validation boundary
Section titled “Edit validation boundary”resolveTaskEdit() is the stable validation entry point for inline task fields. It converts a raw editor value into a validated TaskUpdate patch or returns an invalid-field, invalid-value, or readonly-derived-field failure. It does not mutate task data itself. The task mutation service applies the accepted patch, and the scheduling engine then recomputes derived dates and project state.
Task-table text-entry editors follow spreadsheet-style keyboard entry, including text, numeric, percentage, date-text, Duration, Remaining Duration, and Work fields. With a cell selected, typing a printable character starts a new value and replaces the displayed value; for example, pressing 4 opens the editor with 4. Press Enter or double-click instead when you want to open and modify the existing value. Choice, checkbox, and calendar controls keep their domain-specific input behavior.
The task table and Task Information dialog select controls from the same framework-neutral TaskEditorFieldSchema.kind contract. Duration, Remaining Duration, and Work use the explicit duration kind, so both surfaces share text input semantics, duration-field identification, formatting, and parsing. Grid adapters still own RevoGrid EditorBase lifecycle and keyboard seeds, while dialog adapters own controlled form state; this keeps behavior reusable without coupling modal controls to grid DOM lifecycle.
A zero Duration schedules the row as a milestone, but its Duration remains editable. Entering a positive Duration in either the grid or Task Information dialog converts it back to a regular task automatically. Summary Duration remains read-only because the scheduler derives it from the child-task span.
Dialog control rendering is decomposed by responsibility under packages/gantt/src/gantt-task-editor-dialog/field-controls/: input, date, select, switch, tags, textarea, and labels/resource controls each own one control category. The top-level FieldControl only dispatches by the shared field kind.
The implementation is split by value category under packages/gantt/src/gantt/engine/task-edit-service/internal/: date, duration, numeric, boolean, progress, name, status, task-mode, effort-mode, and constraint handlers each own their parsing, validation messages, and patch creation. Consumers link directly to engine/task-edit-service/index, whose only responsibility is dispatching to the appropriate handler. This keeps custom label handling and schedule-aware validation consistent without collecting every edit rule into one service file or exposing handler registries as public API.
The split is internal and does not change imports, event names, field behavior, or TaskEditResult. Existing integrations should continue importing Gantt APIs from @revolist/gantt.
Task Editor Dialog
Section titled “Task Editor Dialog”GanttPlugin installs the packaged task editor dialog by default, so every Gantt grid gets the row context-menu editor without registering an extra plugin. The same editor contract is also available as presentation-free helpers: render controls from TASK_EDITOR_FIELD_SCHEMA, seed them with createTaskEditorFormValues(), and turn submitted values into a TaskUpdate patch with normalizeTaskEditorSubmit().
Source code
import { defineCustomElements } from '@revolist/revogrid/loader';
defineCustomElements();
import {
defineGanttToolbar,
GanttPlugin,
} from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import './gantt-task-editor-form.css';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_DEPENDENCIES,
SHOWCASE_RESOURCES,
SHOWCASE_TOOLBAR_COLUMNS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import {
createInitialTaskSource,
TASK_EDITOR_COLUMNS,
TASK_EDITOR_DIALOG_CONFIG,
TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
const { isDark } = currentTheme();
type TaskEditorGridElement = HTMLRevoGridElement & Record<string, any>;
function createElement<K extends keyof HTMLElementTagNameMap>(
tagName: K,
className?: string,
): HTMLElementTagNameMap[K] {
const element = document.createElement(tagName);
if (className) {
element.className = className;
}
return element;
}
export function load(parentSelector: string | Element) {
const parent = typeof parentSelector === 'string'
? document.querySelector(parentSelector)
: parentSelector;
if (!parent) {
return () => {};
}
const taskSource = createInitialTaskSource();
const root = createElement('section', 'gantt-task-editor-demo');
const toolbar = createElement('div', 'gantt-task-editor-toolbar');
const grid = document.createElement('revo-grid') as TaskEditorGridElement;
grid.className = 'gantt-task-editor-grid';
grid.theme = isDark() ? 'darkCompact' : 'compact';
grid.hideAttribution = true;
grid.plugins = [GanttPlugin as any];
grid.columns = TASK_EDITOR_COLUMNS;
grid.gantt = TASK_EDITOR_GANTT_CONFIG;
grid.ganttCalendars = [STANDARD_CALENDAR];
grid.ganttDependencies = SHOWCASE_DEPENDENCIES;
grid.ganttResources = SHOWCASE_RESOURCES;
grid.ganttAssignments = SHOWCASE_ASSIGNMENTS;
grid.ganttTaskEditorDialog = TASK_EDITOR_DIALOG_CONFIG;
root.appendChild(toolbar);
root.appendChild(grid);
parent.appendChild(root);
defineGanttToolbar(toolbar, {
grid,
columns: SHOWCASE_TOOLBAR_COLUMNS,
controls: {
export: false,
baseline: false,
},
});
grid.source = taskSource;
return () => {
root.remove();
};
}
import React, { useEffect, useMemo, useRef } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_DEPENDENCIES,
SHOWCASE_RESOURCES,
SHOWCASE_TOOLBAR_COLUMNS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import {
createInitialTaskSource,
TASK_EDITOR_COLUMNS,
TASK_EDITOR_DIALOG_CONFIG,
TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
import './gantt-task-editor-form.css';
const { isDark } = currentTheme();
export default function GanttTaskEditorForm() {
const gridRef = useRef<HTMLRevoGridElement>(null);
const toolbarRef = useRef<HTMLDivElement>(null);
const source = useMemo(() => createInitialTaskSource(), []);
const plugins = useMemo(() => [GanttPlugin], []);
const columns = useMemo(() => TASK_EDITOR_COLUMNS, []);
const gantt = useMemo(() => TASK_EDITOR_GANTT_CONFIG, []);
const calendars = useMemo(() => [STANDARD_CALENDAR], []);
const dependencies = useMemo(() => SHOWCASE_DEPENDENCIES, []);
const resources = useMemo(() => SHOWCASE_RESOURCES, []);
const assignments = useMemo(() => SHOWCASE_ASSIGNMENTS, []);
const taskEditorDialog = useMemo(() => TASK_EDITOR_DIALOG_CONFIG, []);
useEffect(() => {
const toolbar = toolbarRef.current;
const grid = gridRef.current;
if (!toolbar || !grid) {
return undefined;
}
defineGanttToolbar(toolbar, {
grid,
columns: SHOWCASE_TOOLBAR_COLUMNS,
controls: {
export: false,
baseline: false,
},
});
return () => {
toolbar.textContent = '';
};
}, []);
return (
<section className="gantt-task-editor-demo">
<div ref={toolbarRef} className="gantt-task-editor-toolbar" />
<RevoGrid
ref={gridRef}
className="gantt-task-editor-grid"
theme={isDark() ? 'darkCompact' : 'compact'}
hideAttribution
plugins={plugins}
source={source}
columns={columns}
gantt={gantt}
ganttCalendars={calendars}
ganttDependencies={dependencies}
ganttResources={resources}
ganttAssignments={assignments}
ganttTaskEditorDialog={taskEditorDialog}
/>
</section>
);
}
<template>
<section class="gantt-task-editor-demo">
<div ref="toolbarRef" class="gantt-task-editor-toolbar"></div>
<RevoGrid
ref="gridRef"
class="gantt-task-editor-grid"
hide-attribution
:theme="isDark ? 'darkCompact' : 'compact'"
:plugins="plugins"
:source="tasks"
:columns="columns"
:gantt.prop="ganttConfig"
:gantt-calendars.prop="calendars"
:gantt-dependencies.prop="dependencies"
:gantt-resources.prop="resources"
:gantt-assignments.prop="assignments"
:gantt-task-editor-dialog.prop="taskEditorDialog"
/>
</section>
</template>
<script setup lang="ts">
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
import RevoGrid from '@revolist/vue3-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/gantt';
import { currentThemeVue } from '../composables/useRandomData';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_DEPENDENCIES,
SHOWCASE_RESOURCES,
SHOWCASE_TOOLBAR_COLUMNS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import {
createInitialTaskSource,
TASK_EDITOR_COLUMNS,
TASK_EDITOR_DIALOG_CONFIG,
TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
import './gantt-task-editor-form.css';
const { isDark } = currentThemeVue();
const plugins = ref([GanttPlugin]);
const columns = ref(TASK_EDITOR_COLUMNS);
const tasks = ref(createInitialTaskSource());
const ganttConfig = ref(TASK_EDITOR_GANTT_CONFIG);
const calendars = ref([STANDARD_CALENDAR]);
const dependencies = ref(SHOWCASE_DEPENDENCIES);
const resources = ref(SHOWCASE_RESOURCES);
const assignments = ref(SHOWCASE_ASSIGNMENTS);
const taskEditorDialog = ref(TASK_EDITOR_DIALOG_CONFIG);
const gridRef = ref<InstanceType<typeof RevoGrid> | HTMLRevoGridElement | null>(null);
const toolbarRef = ref<HTMLElement | null>(null);
let toolbarMounted = false;
let toolbarFrame = 0;
function getGridEl(): HTMLRevoGridElement | null {
const refValue = gridRef.value as (InstanceType<typeof RevoGrid> & { $el?: HTMLRevoGridElement }) | HTMLRevoGridElement | null;
const candidate = (refValue && '$el' in refValue ? refValue.$el : refValue) ?? null;
return candidate instanceof HTMLElement && candidate.tagName.toLowerCase() === 'revo-grid'
? candidate as HTMLRevoGridElement
: null;
}
function mountToolbar() {
if (toolbarMounted) {
return;
}
const grid = getGridEl();
if (!toolbarRef.value || !grid) {
toolbarFrame = requestAnimationFrame(mountToolbar);
return;
}
toolbarMounted = true;
defineGanttToolbar(toolbarRef.value, {
grid,
columns: SHOWCASE_TOOLBAR_COLUMNS,
controls: {
export: false,
baseline: false,
},
});
}
onMounted(async () => {
await nextTick();
mountToolbar();
});
onBeforeUnmount(() => {
if (toolbarFrame) {
cancelAnimationFrame(toolbarFrame);
}
if (toolbarRef.value) {
toolbarRef.value.textContent = '';
}
});
</script>
import { AfterViewInit, Component, ElementRef, NO_ERRORS_SCHEMA, ViewChild, ViewEncapsulation } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { defineGanttToolbar, GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import {
SHOWCASE_ASSIGNMENTS,
SHOWCASE_DEPENDENCIES,
SHOWCASE_RESOURCES,
SHOWCASE_TOOLBAR_COLUMNS,
STANDARD_CALENDAR,
} from './shared/gantt-project-data';
import {
createInitialTaskSource,
TASK_EDITOR_COLUMNS,
TASK_EDITOR_DIALOG_CONFIG,
TASK_EDITOR_GANTT_CONFIG,
} from './GanttTaskEditorFormShared';
@Component({
selector: 'gantt-task-editor-form-grid',
standalone: true,
// Allows Angular demos to bind RevoGrid plugin props that are not wrapper inputs.
schemas: [NO_ERRORS_SCHEMA],
imports: [RevoGrid],
template: `
<section class="gantt-task-editor-demo">
<div #toolbar class="gantt-task-editor-toolbar"></div>
<revo-grid
#grid
class="gantt-task-editor-grid"
[theme]="theme"
[hideAttribution]="true"
[plugins]="plugins"
[source]="tasks"
[columns]="columns"
[gantt]="ganttConfig"
[ganttCalendars]="calendars"
[ganttDependencies]="dependencies"
[ganttResources]="resources"
[ganttAssignments]="assignments"
[ganttTaskEditorDialog]="taskEditorDialog"
></revo-grid>
</section>
`,
styleUrls: ['./gantt-task-editor-form.css'],
encapsulation: ViewEncapsulation.None,
})
export class GanttTaskEditorFormGridComponent implements AfterViewInit {
@ViewChild('grid', { read: ElementRef }) gridRef!: ElementRef<HTMLRevoGridElement>;
@ViewChild('toolbar', { read: ElementRef }) toolbarRef!: ElementRef<HTMLElement>;
theme = currentTheme().isDark() ? 'darkCompact' : 'compact';
plugins = [GanttPlugin];
columns = TASK_EDITOR_COLUMNS;
tasks = createInitialTaskSource();
ganttConfig = TASK_EDITOR_GANTT_CONFIG;
calendars = [STANDARD_CALENDAR];
dependencies = SHOWCASE_DEPENDENCIES;
resources = SHOWCASE_RESOURCES;
assignments = SHOWCASE_ASSIGNMENTS;
taskEditorDialog = TASK_EDITOR_DIALOG_CONFIG;
ngAfterViewInit(): void {
defineGanttToolbar(this.toolbarRef.nativeElement, {
grid: this.gridRef.nativeElement,
columns: SHOWCASE_TOOLBAR_COLUMNS,
controls: {
export: false,
baseline: false,
},
});
}
}
/**
* Shared project data for all Gantt documentation examples.
*
* This shared module is the import surface for demos that reuse the full
* project fixture. Constants are grouped by responsibility in adjacent files.
*/
export * from './gantt-project-base-data';
export * from './gantt-showcase-data';
export * from './gantt-showcase-columns';
The packaged dialog is responsive for mobile use. On phone-width viewports it becomes a full-viewport dialog, switches task fields and dependency rows to a single column, keeps the form body scrollable, and uses larger controls for touch input. The same validation and submit flow runs on desktop, tablet, and phone layouts.
How It Works
Section titled “How It Works”The task editor demo opens the editor from the Gantt row context menu and includes the packaged Gantt toolbar:
- The grid renders normal Gantt tasks, dependencies, calendars, resources, and assignments.
- Right-click a task row and choose Edit….
- Use row context-menu Add to insert a
New task Nrow without opening the editor, or toolbar Task to create a task and open the same editor for the new row. - The auto-installed task editor dialog adds the row-menu edit item and owns the native HTML
<dialog>. - New tasks open in the editor by default after the Gantt plugin emits its post-create lifecycle event. Set
ganttTaskEditorDialog.openOnCreatetofalsewhen task creation should stay silent. - The dialog renders a tabbed task information UI from
TASK_EDITOR_FIELD_SCHEMA. - General contains task name, status, task kind, dates, duration, and progress.
- Advanced contains effort mode, calendar, tags, constraints, manual scheduling, work, actual dates, inactive state, and leveling controls.
- Predecessors edits incoming dependency target, type, and lead/lag days. Successors remains available through customization but is hidden by default.
- Resources renders the resource assignment picker when resources and assignments are available.
- Notes provides a dedicated long-form notes editor.
- The patch preview calls
normalizeTaskEditorSubmit(task, values)on every edit. - The submit button applies the returned patch through the Gantt runtime/provider edit path, updates
grid.ganttAssignmentsandgrid.ganttDependencies, and refreshes the project.
grid.plugins = [GanttPlugin];grid.ganttTaskEditorDialog = { title: 'Task details', description: 'Update task fields and save the changes to the project schedule.', applyLabel: 'Save changes', localeText: { tabs: { assignments: 'Team', }, fields: { effortMode: 'Task type', }, resourcesSearch: { placeholder: 'Find a resource', }, },};
const values = createTaskEditorFormValues(task, resources, assignments);const result = normalizeTaskEditorSubmit(task, values);
if (result.ok) { updateTask(task.id, result.patch);}Fields
Section titled “Fields”The schema includes editable task fields such as name, startDate, endDate, duration, progressPercent, constraints, deadlines, actual dates, work, effort mode, effort-driven scheduling, inactive state, leveling controls, manual scheduling, and notes. It also includes display fields such as resourceLabels.
Date fields open as date-only controls by default. Use the field-level Time toggle in the packaged dialog when you need minute-level scheduling; the dialog stores the submitted value as a UTC ISO datetime.
Set gantt.dateFormats.editor when the packaged dialog should show custom date text instead of native date inputs. In that mode, the dialog uses a text input, calls your parser as the user types, and still submits ISO dates to normalizeTaskEditorSubmit() and the Gantt mutation services:
grid.gantt = { ...project, dateFormats: { locale: 'en-GB', timeZone: 'UTC', table: { day: '2-digit', month: '2-digit', year: 'numeric' }, tooltip: { day: '2-digit', month: 'short', year: 'numeric' }, editor: { options: { day: '2-digit', month: '2-digit', year: 'numeric' }, parser(value) { const match = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value.trim()); return match ? `${match[3]}-${match[2]}-${match[1]}` : null; }, }, },};Custom date formats are presentation and input helpers only. Keep persisted tasks as YYYY-MM-DD dates or UTC datetimes ending in Z.
resourceLabels is read-only in the schema helper because TaskUpdate does not own assignment rows. The packaged dialog upgrades that field into a resource assignment picker and writes the selected resources to grid.ganttAssignments. Set ganttTaskEditorDialog.resources to false when you want the field to stay read-only.
Resource options show ResourceEntity.avatarUrl as a circular profile image. When no image is configured—or an image fails to load—the picker renders deterministic initials from the resource name, so every assignment remains visually identifiable without requiring avatar assets.
Entity panels include search controls: resource assignment searches by resource name and role, while predecessor and successor panels search task names. Resource lists, task dropdowns, dependency type dropdowns, and schema-backed select options are sorted A-Z by their displayed labels.
Use ganttTaskEditorDialog.localeText to translate or customize packaged dialog text. It covers tabs, field labels, field option labels, dependency type labels, empty states, search placeholders, switch labels, validation headings, save errors, and context-menu text. Existing top-level title, description, applyLabel, resetLabel, closeLabel, and menuItemName still work as direct overrides for those common labels.
Customizing Tabs, Fields, and Controls
Section titled “Customizing Tabs, Fields, and Controls”Use ganttTaskEditorDialog.customization to change the packaged editor without replacing it. Built-in tab ids are details, predecessors, successors, assignments, advanced, and notes. Set an item to false to remove it, use hidden, title, and fields to reconfigure it, and use order to place built-in and custom tabs. Nested customization takes precedence over the legacy top-level fields allow-list.
grid.ganttTaskEditorDialog = { fields: ['name', 'status', 'type', 'startDate', 'endDate', 'calendarId', 'tags', 'notes'], customization: { controls: { description: false, reset: false, }, tabs: { order: ['details', 'successors', 'advanced', 'notes'], items: { details: { title: 'Main', fields: ['name', 'status', 'type', 'startDate', 'endDate'] }, successors: { hidden: false }, assignments: false, }, }, },};The shell controls are header, title, description, close, tabs, validation, preview, reset, apply, and footer. Setting one to false hides only the packaged control; custom content can still call the corresponding controller method.
Adding a Custom Page
Section titled “Adding a Custom Page”Custom pages use a framework-neutral DOM lifecycle. mount runs once when the page is shown, update receives fresh task/project/draft context, and destroy runs when the page is removed. The controller can update built-in values, resources, dependencies, application-owned custom values, validation errors, and the reset/close/submit lifecycle.
const auditPage = { mount(container, context) { let currentContext = context; const input = document.createElement('input'); input.placeholder = 'External approval reference'; container.appendChild(input);
const onInput = () => { currentContext.controller.setCustomValue('approvalReference', input.value); currentContext.controller.setValidationErrors(input.value.trim() ? [] : ['Approval reference is required.']); }; input.addEventListener('input', onInput);
return { update(next) { currentContext = next; input.value = String(next.state.customValues.approvalReference ?? ''); }, destroy() { input.removeEventListener('input', onInput); }, }; },};
grid.ganttTaskEditorDialog = { customization: { tabs: { order: ['details', 'audit', 'notes'], items: { audit: { title: 'Audit', renderer: auditPage } }, }, }, async onSubmit({ result, customValues }) { await saveTaskAndApproval(result.patch, customValues.approvalReference); },};Custom values are passed to onSubmit but are never persisted implicitly. Custom validation errors block packaged submission until cleared.
Replacing the Entire Editor
Section titled “Replacing the Entire Editor”Set customization.editor to the same DOM renderer contract to replace the packaged dialog completely. The replacement receives the live project snapshot and shared draft controller, including reset(), close(), and submit(). Its lifecycle is destroyed when the editor closes or the plugin is removed.
Calendar and Tags
Section titled “Calendar and Tags”calendarId is populated from the live Gantt calendars and validated before mutation. Changing it uses the normal task mutation, rescheduling, and history path. tags accepts comma-separated input; values are trimmed, blank entries are removed, and duplicates are removed while preserving their first-seen order.
Use ganttTaskEditorDialog.openOnCreate = false to keep toolbar or programmatic task creation from opening the dialog. The Gantt row context menu always creates silently through the default Add action when task creation is allowed, and the editor plugin prepends its Edit… item while preserving custom rowContextMenu or contextMenu items and resolve behavior. Set gantt.contextMenu = false to opt out of generated Gantt menu items, or set ganttTaskEditorDialog.contextMenu when only the packaged editor menu item should change.
Task Colors
Section titled “Task Colors”The Gantt row context menu and packaged task editor use the same shared color palette. Choosing a color updates the task through the normal Gantt mutation path, so source synchronization, cancelable before-change events, history, scheduling, and rendering stay consistent. Default clears the authored task color; taskBarColorHook remains authoritative when it returns a color.
Use options to replace the shared palette or extendOptions to add project colors. Any swatch can be disabled, as can the Default choice or the complete Color command:
grid.gantt = { ...project, contextMenu: { colorPalette: { extendOptions: [ { value: '#7c3aed', label: 'Project violet' }, { value: '#0f766e', label: 'Reserved teal', disabled: true }, ], defaultOption: { label: 'Use project default' }, colorAriaLabel: (option) => `Task color: ${option.label}`, }, disabled: { delete: true, }, },};Set contextMenu.colorPalette to false, contextMenu.hidden.color to true, or contextMenu.disabled.color to true to remove or disable this functionality. Set colorPalette.defaultOption to false to remove Default, or colorPalette.disabled to true to disable the whole submenu. The same hidden and disabled maps apply to the other built-in Gantt commands. Application commands supplied through RevoGrid’s row-context-menu configuration continue to compose with the generated Gantt items.
The task editor inherits gantt.contextMenu.colorPalette by default. Use ganttTaskEditorDialog.colorPalette to configure its palette independently, or set it to false to remove the Color field from the editor. The editor-specific setting supports the same options, extendOptions, disabled choices, Default configuration, and accessible option-label resolver.
Validation
Section titled “Validation”normalizeTaskEditorSubmit() validates values before returning a patch:
namecannot be empty.- Date values must be ISO dates like
2026-05-12or UTC datetimes ending inZ. endDatecannot be earlier thanstartDate.- Numeric fields must be non-negative.
progressPercentis clamped to0..100.- Unsupported constraint values return field errors.
If both startDate and endDate are present, the helper recalculates duration from the date range. If values are unchanged, the default result omits them from the patch.
Production Wiring
Section titled “Production Wiring”In production, keep the same flow but replace the demo’s local source update with your persistence and project-state update path:
async function submitTaskEditor(task, values) { const result = normalizeTaskEditorSubmit(task, values);
if (!result.ok) { renderFieldErrors(result.errors); return; }
await api.updateTask(task.id, result.patch); grid.source = grid.source.map((row) => row.id === task.id ? { ...row, ...result.patch } : row, );}The Gantt plugin still owns timeline projection and scheduling when the refreshed task source is applied.
The packaged dialog defaults to applying task field patches through the Gantt runtime/provider edit path and replacing the edited task’s assignment rows in grid.ganttAssignments. This keeps task editor saves visible to HistoryPlugin instead of bypassing grid edit tracking with direct grid.source replacement. To persist edits first, provide ganttTaskEditorDialog.onSubmit; return false from that callback when your application has already applied the task and assignment updates and the plugin should skip the default patch.
Dependency tabs also patch grid.ganttDependencies by replacing links that involve the edited task. Existing dependency ids are preserved, new rows get deterministic ids, and lag supports negative lead or positive lag days.
Set preview: true only for diagnostics or documentation when you want to show the generated TaskUpdate JSON.