Skip to content

Server-Side Loading

Expand a project to fetch its direct children, then scroll, sort, or filter. All four framework versions use HTTP requests and the same MSW demo API mock runner as the pagination example. The simulated backend schedules the complete project once per revision and serves slices of that schedule. This backend demonstrates name/progress filters and ISO date sorting.

Source code
import { defineCustomElements } from '@revolist/revogrid/loader';
import {
AdvanceFilterPlugin,
ColumnStretchPlugin,
RowOddPlugin,
} from '@revolist/revogrid-pro';
import { GanttPlugin } from '@revolist/gantt';
import { currentTheme } from '../composables/useRandomData';
import { createRemoteGanttColumns } from './gantt.remote.utils';
import { startRemoteGantt } from './gantt.remote.bootstrap';
defineCustomElements();
export function load(parentSelector: string) {
const parent = document.querySelector(parentSelector);
if (!parent) throw new Error('Remote gantt container not found.');
const shell = document.createElement('section');
shell.className = 'gantt-remote-demo';
Object.assign(shell.style, {
height: '420px',
minHeight: '420px',
minWidth: '0',
display: 'flex',
flexDirection: 'column',
flex: '1 1 auto',
});
parent.appendChild(shell);
const lifetime = new AbortController();
const status = document.createElement('p');
status.setAttribute('role', 'status');
status.textContent = 'Starting demo backend…';
shell.appendChild(status);
let grid: HTMLRevoGridElement | undefined;
void startRemoteGantt(lifetime.signal)
.then((gantt) => {
if (lifetime.signal.aborted) return;
grid = document.createElement('revo-grid');
grid.className = 'overflow-hidden cell-border';
Object.assign(grid.style, { height: '100%', minHeight: '0', minWidth: '0', flex: '1 1 0' });
grid.columns = createRemoteGanttColumns();
grid.filter = true;
grid.stretch = 'all';
Object.assign(grid, { gantt });
grid.plugins = [
GanttPlugin,
AdvanceFilterPlugin,
ColumnStretchPlugin,
RowOddPlugin,
];
grid.theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
grid.hideAttribution = true;
shell.appendChild(grid);
grid.source = [];
status.remove();
})
.catch((error) => {
if (lifetime.signal.aborted) return;
status.setAttribute('role', 'alert');
status.textContent = error.message;
});
return () => {
lifetime.abort();
grid?.remove();
shell.remove();
};
}

Register GanttPlugin and set gantt.remote before mounting. Gantt manages tree loading internally; do not also configure tree.remote. Keep source empty. The backend supplies metadata, calendars, a global timeline range and scheduled task pages.

import { GanttPlugin, type GanttRemoteConfig } from '@revolist/gantt';
import { createDataQueryContext } from '@revolist/revogrid-pro';
const context = createDataQueryContext();
const source = context.source({
sourceId: 'projects', schemaId: 'schedule-v1',
tenantId: 'actual-tenant', authorizationScope: 'actual-read-scope',
});
async function post(path: string, body: unknown, signal?: AbortSignal) {
const response = await fetch(`/api/projects/p/${path}`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), signal,
});
if (!response.ok) throw Object.assign(new Error('Project read failed'), {
status: response.status,
});
return response.json();
}
const remote: GanttRemoteConfig = {
chunkSize: 100,
query: { source, staleTime: 30_000, retry: 2 },
contextQuery: { staleTime: 30_000 },
dependencyQuery: { staleTime: 30_000 },
loadContext: (request, signal) => post('context', request, signal),
loadData: (skip, limit, order, singleConditionFilters,
multiConditionFilters, quickFilter, temporalContext, filterAst,
executionContext, requestContext) => post('children', {
parentId: requestContext?.parentId ?? null,
revision: requestContext?.revision,
skip, limit, order, singleConditionFilters, multiConditionFilters,
quickFilter, temporalContext, filterAst, executionContext,
}, requestContext?.signal),
loadDependencies: (request, signal) => post('dependencies', request, signal),
};
const grid = document.createElement('revo-grid');
grid.style.height = '500px';
grid.source = [];
grid.gantt = {
id: 'p', name: 'Project', version: '1', currency: 'USD',
timeZone: 'UTC', primaryCalendarId: 'standard',
updatedAt: '2026-05-01T00:00:00Z', remote,
readOnly: true, // This example opts out of editing; remote loading does not force it.
};
grid.plugins = [GanttPlugin];
document.body.append(grid);
// Teardown: remove grids first, then dispose the caller-owned context.

The query, contextQuery and dependencyQuery options use native shared Query policies for cache freshness, retries, polling, focus/reconnect, reconciliation and status. Context and dependency readers inherit query.source unless overridden. Other policies are configured separately for each read. Setting query: false also disables Query for context and dependencies unless their policies are explicitly supplied. See shared queries.

Replace the gantt configuration object and its remote object to change loaders or policies. A new visual configuration with the same remote object keeps its observers. The application owns any supplied Query context; removing the grid releases only its subscriptions.

loadContext({ projectId }, signal) returns GanttRemoteContext:

{
project: {
id: 'p', name: 'Program', version: '1', currency: 'USD',
timeZone: 'UTC', primaryCalendarId: 'standard',
updatedAt: '2026-05-01T00:00:00Z',
calendars: [{ id: 'standard', name: 'Standard', timeZone: 'UTC',
workingDays: [1, 2, 3, 4, 5], holidays: [], hoursPerDay: 8 }],
},
revision: 'schedule-42',
timelineRange: { startDate: '2026-05-01', endDate: '2026-08-31' },
scheduling: { excludeHolidaysFromDuration: false },
capabilities: { dependencies: true, criticalPath: true },
}

project contains project metadata and calendars, without tasks, dependencies, resources, assignments or baselines. revision is a nonempty opaque string identifying the entire immutable server schedule. Supply the global project extent in timelineRange; fit-to-project uses it even when only roots are loaded. The optional scheduling value describes the server’s duration conversion basis; it does not enable local scheduling. Critical-path highlighting requires capabilities.criticalPath: true and the corresponding visual option.

loadData has the same first nine positional arguments as infinity scroll. Its tenth context supplies { signal, parentId, revision }. parentId: null requests roots. skip counts direct siblings, not flattened rows. The response is:

{ data: scheduledRows, total: exactDirectChildCount, revision: 'schedule-42' }

Every row is a GanttRemoteTaskRow: the public semantic ScheduledTask plus boolean hasChildren. IDs are globally unique strings, root parentId is null, and projectId matches the context. Return schedule fields including dates, duration, progress, WBS, calendar, status, schedule origin, critical-path dates and slack. Use the exported type when implementing the service. Do not return pixel positions or timeline bar geometry.

Compute summary dates/progress, dependencies, WBS and critical-path values against the complete authorized project, then page the resulting rows. A summary row must retain those values before and after its children load. ScheduledTask.duration uses the scheduler’s day-based result; explicit duration units are converted for display using the supplied calendars and scheduling basis.

The tree paging rules apply: return exactly min(limit, max(0, total - skip)) direct children and an exact filtered total; stable sibling sorting needs an ID tie-breaker. Preserve ancestor paths of matches and calculate hasChildren from that filtered hierarchy. Both compatibility filters and canonical filterAst reach the loader. See the tree endpoint responsibilities for validation, authorization, filter compilation and consistent counts.

The optional loadDependencies({ projectId, revision, taskIds }, signal) returns { data: DependencyEntity[], revision }. It is called for loaded task IDs. Only edges whose two endpoints are loaded are drawn. Set capabilities.dependencies: false to suppress these reads. Omitting the loader displays no dependency edges; it does not change the server’s schedule.

All pages and edges must carry the pinned context revision. Reject expired revisions with an error whose status is 409 or 412; HTTP loaders must propagate that status. A mismatching response is rejected. Gantt attempts one context refresh per failed revision, disposes old observers and replaces branches together when a new revision arrives. Failed same-revision refreshes retain accepted data.

const plugins = await grid.getPlugins();
const gantt = plugins.find(plugin => plugin instanceof GanttPlugin)!;
await gantt.remote!.ready; // context ready; roots may still be loading
await gantt.remote!.tree!.loadChildren(null); // wait for root block
await gantt.remote!.refresh(); // context, then task roots
await gantt.remote!.tree!.retry('phase-a', 100); // retry a sibling block

Observe gantt-remote-state-changed for { phase: 'context' | 'dependencies', status } using the native read status. Tree block loading/errors and retry placeholders use tree-remote-state-changed. Expansion, viewport reads, collapse caching, sorting/filtering generations and cancellation follow the tree lifecycle.

Remote loading preserves your gantt configuration and respects the application’s gantt.readOnly setting. Set readOnly: true for a read-only view, or omit it / set it to false when the app handles edits. The live example explicitly chooses readOnly: true.

The application owns edit permissions, validation, sending commands to the backend, save errors, and revision conflicts. gantt.remote supplies read loaders; it does not automatically persist edits. Remote Gantt renders authoritative schedules and does not run the client scheduler over partial data.

For server-authoritative editing, intercept the cancelable gantt-before-task-change, gantt-before-dependency-change, and gantt-before-assignment-change events for the interactions your app enables. Call preventDefault() synchronously, before awaiting a server response, to stop the local mutation. Send the proposed change with the current revision to your application endpoint, let the server validate and schedule the complete project, then call gantt.remote.refresh() to load the accepted schedule. Keep failed edits out of the loaded schedule and show the error in your app. Handle paste, custom columns, and custom controls at their corresponding grid or application event boundary.

// Application-owned command endpoint; not a built-in Gantt remote write API.
grid.addEventListener('gantt-before-task-change', event => {
event.preventDefault();
const revision = gantt.remote?.context?.revision;
if (!revision) return;
const command = {
operationId: crypto.randomUUID(),
projectId: grid.gantt!.id,
revision,
action: event.detail.action,
taskId: event.detail.taskId,
changes: event.detail.changes,
};
void post('commands', command)
.then(() => gantt.remote!.refresh())
.catch(error => {
// Display the failure and resolve revision conflicts in your application.
console.error('Task change was not applied', error);
});
});

Install these handlers before enabling editing. Serialize conflicting saves or disable the affected controls while a save is pending. Use equivalent handlers for dependency and assignment changes. See task editing for event cancellation and payloads. The whole-project gantt-project-commit persistence flow requires a complete local project and is not emitted for paged snapshots.

The remote loading contract does not provide task creation, updateTask() / updateTasks() writes, baseline capture, resource planning, local task windows, or date-window paging. Provide application controls and server commands for operations that require unloaded records. Undo/redo of server commands also belongs to the application; local history cannot restore the complete server project.

Selection and grid export cover loaded records only; placeholders are excluded. getProjectSnapshot() and getProjectData() return null because loaded rows cannot represent a complete project. Complete-project loading/import methods reject while remote mode is active. For a complete export, provide an application-owned backend endpoint.

To return to local scheduling, replace gantt with a local configuration (remote: false or omitted), then supply the complete authored task and linked-entity collections. Local scheduling and the whole-project Query adapter remain available for projects loaded in full.