Editing
A user changes a cell. EventManagerPlugin normalizes that edit so collaborative editing sees cell edits, paste, range edits, undo, and redo through one pipeline.
CollaborativeEditingPlugin lets several users work on the same grid data at the same time. One user edits a cell, the edit is written into a shared Yjs document, and the other connected grids can receive the same change.
You do not need to know Yjs before using the plugin. Think of Yjs as the shared notebook where the browser stores collaborative changes. RevoGrid still renders the grid, your app still owns users and business rules, and your backend still decides what is finally saved.
Install collaborative editing as a separate package so Yjs is only loaded by applications that need realtime editing:
pnpm add @revolist/revogrid-collaborative-editing @revolist/revogrid-proThe demo catalog includes a complete split-view example with two users editing the same shared document. Open it when you want to see the moving parts together instead of reading isolated snippets.
Open the Collaborative Editing demo
The demo shows:
CollaborativePresencePlugin;Task column pinned while the second grid keeps it unpinned;EventManagerPlugin with applyEventsToSource: false;commitAdapter that logs server commits so the backend boundary is visible.Collaborative editing has a few moving parts. The names are technical, but the ideas are simple.
Editing
A user changes a cell. EventManagerPlugin normalizes that edit so collaborative editing sees cell edits, paste, range edits, undo, and redo through one pipeline.
Presence
Presence means “where are the other users looking or editing?” Add CollaborativePresencePlugin when you want remote cursors, ranges, colors, and labels.
Pending Changes
In review mode, edits stay pending until your UI commits or discards them. This is useful when a manager, workflow, or server must approve changes.
Commits
A commit is the point where your app asks the backend to accept changes. The plugin calls your commitAdapter; your backend can accept, rewrite, or reject each edit.
Conflicts
A conflict happens when users edit the same row and column concurrently. The default policy keeps the change pending so your UI can show a decision.
Audit
Audit records explain who changed what and when. Add AuditHistoryPlugin when committed collaborative edits should be logged.
Start with the smallest local setup, then add the pieces your product needs.
This example needs no server and no transport. It is useful for learning the plugin and verifying that local cell edits enter the collaborative pipeline.
Import the base plugins.
import { EventManagerPlugin,} from '@revolist/revogrid-pro';
import { CollaborativeEditingPlugin, type CollaborativeEditingConfig,} from '@revolist/revogrid-collaborative-editing';Give every row a stable id.
const rows = [ { id: 'task-1', task: 'Revenue forecast', owner: 'Avery', status: 'Draft' }, { id: 'task-2', task: 'Vendor renewal', owner: 'Mina', status: 'Review' }, { id: 'task-3', task: 'Headcount plan', owner: 'Jon', status: 'Approved' },];
const columns = [ { prop: 'task', name: 'Task' }, { prop: 'owner', name: 'Owner' }, { prop: 'status', name: 'Status' },];Register plugins and bind configuration.
const grid = document.querySelector('revo-grid')!;
grid.columns = columns;grid.source = rows;grid.plugins = [ EventManagerPlugin, CollaborativeEditingPlugin,];
grid.eventManager = { applyEventsToSource: false };
const collaborativeEditing: CollaborativeEditingConfig = { documentId: 'local-learning-session', user: { id: 'avery', name: 'Avery Stone', color: '#2563eb', permissionLevel: 'editor', }, rowIdProp: 'id',};
grid.collaborativeEditing = collaborativeEditing;grid.eventManager = { applyEventsToSource: false } is important. It lets collaborative editing own how edits are applied, reviewed, committed, or rolled back instead of letting each raw grid edit immediately mutate source data twice.
Use a shared Yjs document when two grids on one page should represent the same collaborative session. This is the easiest way to understand how two users share edits before connecting a real transport.
import * as Y from 'yjs';import { CollaborativePresencePlugin, EventManagerPlugin,} from '@revolist/revogrid-pro';import { CollaborativeEditingPlugin,} from '@revolist/revogrid-collaborative-editing';
const doc = new Y.Doc();const plugins = [ EventManagerPlugin, CollaborativePresencePlugin, CollaborativeEditingPlugin,];
function configureGrid(grid: HTMLRevoGridElement, user: { id: string; name: string; color: string }) { grid.columns = columns; grid.source = rows.map(row => ({ ...row })); grid.plugins = plugins; grid.eventManager = { applyEventsToSource: false }; grid.collaborativeEditing = { documentId: 'same-page-demo', user, rowIdProp: 'id', doc, presence: { enabled: true, showLabels: true, staleAfterMs: 15_000, }, };}
configureGrid(gridA, { id: 'avery', name: 'Avery Stone', color: '#2563eb' });configureGrid(gridB, { id: 'grace', name: 'Grace Hopper', color: '#16a34a' });Both grids use the same documentId and the same doc, so they are intentionally part of the same collaborative document.
Use review mode when users should make changes locally, inspect the pending list, then commit or discard those changes from your UI.
grid.collaborativeEditing = { documentId: `budget:${budgetId}`, user: currentUser, rowIdProp: 'id', mode: 'review', conflictPolicy: 'manual',};Read the plugin instance when your UI needs pending changes or review actions.
const plugins = await grid.getPlugins();const collaborative = plugins.find( plugin => plugin instanceof CollaborativeEditingPlugin,) as CollaborativeEditingPlugin | undefined;
const pending = collaborative?.getPendingChanges() ?? [];
await collaborative?.commitPendingChanges(pending.map(change => change.id));collaborative?.discardPendingChanges();Review mode is a good default for finance, procurement, admin, support, planning, and any workflow where users should not silently overwrite each other.
Use commitAdapter when your backend must validate, persist, approve, reject, version, or rewrite edits.
grid.collaborativeEditing = { documentId: `forecast:${forecastId}`, user: currentUser, rowIdProp: 'id', mode: 'autoCommit', commitAdapter: { async commit(changes, context) { const response = await fetch(`/api/forecasts/${forecastId}/changes`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ documentId: context.documentId, user: context.user, changes, }), });
return response.json(); }, },};The response should describe which changes were committed and which were rejected. Rejected changes are rolled back to their previous local value.
[ { id: 'change-1', status: 'committed' }, { id: 'change-2', status: 'rejected', reason: 'Budget is locked' },]Use permissions.canEditCell for user, role, row, tenant, or workflow-specific edit rules.
grid.collaborativeEditing = { documentId: `pricing:${priceBookId}`, user: { id: currentUser.id, name: currentUser.name, roles: currentUser.roles, permissionLevel: currentUser.permissionLevel, }, rowIdProp: 'sku', permissions: { canEditCell({ user, row, prop, newValue }) { if (row?.locked) return false; if (prop === 'approvedPrice') return user.roles?.includes('pricing-admin') ?? false; if (prop === 'discount' && Number(newValue) > 0.2) return user.permissionLevel === 'manager'; return user.permissionLevel === 'editor'; }, },};Client permissions improve UX and prevent obvious invalid edits early. They are not a security boundary; repeat important checks in your backend through commitAdapter.
A shared local Y.Doc is enough for demos and same-page tests. For real users in different browsers, add a transport.
import { createYjsWebSocketTransport,} from '@revolist/revogrid-collaborative-editing';
grid.collaborativeEditing = { documentId: `project:${projectId}:tasks`, user: currentUser, rowIdProp: 'id', transport: createYjsWebSocketTransport({ url: 'wss://collaboration.example.com', params: { token: sessionToken }, }),};Use WebSocket when your product needs a controlled server, authentication, observability, and enterprise networking. Use WebRTC for peer-to-peer collaboration where your deployment can support signaling and peer connectivity. Use a custom transport when your application already wraps Yjs providers.
The default conflictPolicy is manual. When two users edit the same row and column concurrently, the plugin emits collaborativeconflict and keeps the local change pending.
grid.addEventListener('collaborativeconflict', (event) => { const conflict = event.detail;
showConflictDialog({ rowId: conflict.local.rowId, column: conflict.local.prop, localValue: conflict.local.newValue, remoteValue: conflict.remoteValue, user: conflict.remoteUser, });});Set conflictPolicy: 'lastWriteWins' only for low-risk cells such as notes, draft comments, or transient planning values where silent overwrite is acceptable.
| Property | Type | Default | First-time guidance |
|---|---|---|---|
documentId | string | 'revogrid-collaborative-document' | The shared session name. Use the same value for every user editing the same business object, such as budget:2026 or project:alpha:tasks. |
user | CollaborativeEditingUser | undefined | The current editor. Provide at least id and name; without a user, local edits are ignored because edits must be attributable. |
rowIdProp | string | 'id' | The row field that uniquely identifies the business record. Use your real primary key, such as invoiceId, sku, taskId, or employeeId. |
commitAdapter | CollaborativeEditingCommitAdapter | undefined | Your backend boundary. Add it when changes must be saved, validated, approved, rejected, versioned, or audited by a server. |
permissions | CollaborativeEditingPermissions | undefined | Client-side business rules. Use this for role checks, locked rows, approval status, tenant rules, or field-level permissions. |
audit | CollaborativeEditingAuditConfig | boolean | enabled | Audit integration. Keep it enabled for operational, financial, admin, support, or regulated workflows. |
conflictPolicy | 'manual' | 'lastWriteWins' | 'manual' | How same-cell concurrent edits are handled. Keep manual for business data. |
| Property | Type | Default | First-time guidance |
|---|---|---|---|
enabled | boolean | true | Turns collaboration on or off without changing the plugin list. Useful for feature flags or read-only screens. |
doc | Y.Doc | new isolated document | Pass this when your app owns the Yjs document or when several grids should share one local document. |
mode | 'autoCommit' | 'review' | 'autoCommit' | Use autoCommit for immediate server reconciliation. Use review when users need commit and discard buttons. |
transport | CollaborativeEditingTransport | undefined | Connects different browsers. Without transport, collaboration is local to the current page or shared doc. |
presence | { enabled, showLabels, staleAfterMs } | enabled, labels shown | Controls the bridge into CollaborativePresencePlugin. Use it when users should see who is focused or editing. |
user tells the plugin who is editing. It is used for presence labels, permission checks, commit requests, audit records, and conflict metadata.
const user = { id: 'u-123', name: 'Avery Stone', initials: 'AS', color: '#2563eb', roles: ['planner', 'budget-editor'], permissionLevel: 'editor',};Use a stable application user id. Avoid session ids or display names as id, because audit history and server reconciliation need durable identity.
documentIddocumentId means “which shared document is this grid editing?”
documentId: `forecast:${forecastId}`documentId: `project:${projectId}:schedule`documentId: `tenant:${tenantId}:price-book:${priceBookId}`Use the same documentId for every client editing the same dataset. Use a different documentId when two grids should not share edits or presence.
rowIdPropCollaborative editing stores cell changes by stable row identity. If rowIdProp is wrong, edits may apply to the wrong row after sorting or filtering.
const rows = [ { invoiceId: 'INV-1001', status: 'Draft', total: 1200 },];
grid.collaborativeEditing = { documentId: 'invoices:open', user, rowIdProp: 'invoiceId',};Use id only when every row really has a durable id. For generated, aggregated, or temporary rows, provide a stable key before enabling collaboration.
commitAdaptercommitAdapter means “ask my backend whether these edits are accepted.”
commitAdapter: { async commit(changes, context) { const response = await fetch('/api/reconcile-grid-changes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ documentId: context.documentId, user: context.user, changes, }), });
return response.json() as Promise<Array<{ id: string; status: 'committed' | 'rejected'; value?: unknown; reason?: string; metadata?: Record<string, unknown>; }>>; },}Return committed when the server accepts a change. Return rejected when validation, permissions, workflow status, version checks, or business rules fail.
permissionspermissions.canEditCell runs before a local edit is accepted. It complements RevoGrid’s readonly support:
readonly for static UI-level rulespermissions.canEditCell for user, row, tenant, or workflow-specific rulespermissions: { canEditCell({ user, row, prop, newValue }) { if (row?.approvalStatus === 'Approved') return false; if (prop === 'budgetLimit') return user.roles?.includes('finance-admin') ?? false; if (prop === 'discount' && Number(newValue) > 0.2) return user.permissionLevel === 'manager'; return user.permissionLevel === 'editor'; },}When AuditHistoryPlugin is registered, collaboration commits are recorded as audit events by default.
audit: { enabled: true, actionType: 'collaborative-price-change', source: 'api',}Disable audit only for temporary or non-business data:
audit: falseUse mode: 'autoCommit' when the server should accept or reject each edit as soon as it is made.
Use mode: 'review' when users need an explicit commit/discard workflow. In review mode, edits are optimistic locally and remain in getPendingChanges() until the host commits or discards them through the plugin instance.
When AuditHistoryPlugin is registered and audit.enabled !== false, committed collaborative changes call AuditHistoryPlugin.recordEvent() with:
type: 'collaborative-edit'Use the Audit History storage hooks to persist those records to your backend.
| Mistake | What happens | Fix |
|---|---|---|
Missing EventManagerPlugin | Edits do not enter the normalized collaborative edit pipeline. | Register EventManagerPlugin before CollaborativeEditingPlugin. |
Missing user | Local edits are ignored because the plugin cannot attribute them. | Pass a stable user with at least id and name. |
Missing or unstable rowIdProp | Edits can target the wrong row after sorting, filtering, or reordering. | Use a durable business id field. |
Different documentId values | Users do not share the same collaborative document. | Use one stable documentId for every client in the same session. |
| Expecting automatic persistence | Local/Yjs state changes, but your backend is not updated. | Add a commitAdapter. |
| Expecting visible labels without presence plugin | Editing works, but remote cursors and ranges do not render. | Add CollaborativePresencePlugin and enable presence. |