Collaboration
Let multiple users edit the same board and see each other's changes. Complete collaboration integration for the backend, authentication, permissions, and storage, then configure the license.
Configure and load a document
Add Collaboration Client configuration to your existing Boards editor using the Collaboration Quick Start. That example uses Sheets; your backend must create and persist a Boards document and return its ID to the browser.
Set the current user before loading the document. See Collaboration integration.
import '@univerjs-pro/boards/facade'import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const document = await univerAPI.getCollaboration().loadBoardAsync('your-document-id')if (!document) throw new Error('Document could not be loaded')Use the same ID when reopening or sharing the document. Do not create a local blank document instead of loading the server document.
Use synchronization status, retries, and conflict recovery to build saving indicators and protect unacknowledged edits before reloading.
Removal of built-in URL loading for collaborative documents
Note
Starting with version 1.2.0, Univer has removed built-in URL loading for collaborative documents. It no longer automatically reads the unit and type URL parameters to load a collaborative document.
If your application relies on this behavior, you can restore it with a custom plugin.
Integrate the custom plugin
Copy the four files below into your project using this directory structure:
custom-url-load/├── plugin.ts├── controllers/│ └── data-loader.controller.ts└── services/ ├── url.service.ts └── web-url.service.tsAfter initializing the editor and collaboration plugins, set the current user before registering CustomUrlLoadPlugin.
import { UserManagerService } from '@univerjs/core'import { CustomUrlLoadPlugin } from './custom-url-load/plugin'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })univer.registerPlugin(CustomUrlLoadPlugin)Once registered, the plugin loads the document, so you do not need to call loadBoardAsync() for the same document. If your application already loads documents through the Facade, you can keep the approach in the previous section and handle URLs according to your application’s needs.
Complete plugin code
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'import { CollaborationDataLoaderController } from './controllers/data-loader.controller'import { IURLService } from './services/url.service'import { WebURLService } from './services/web-url.service'@DependentOn(UniverCollaborationClientPlugin)export class CustomUrlLoadPlugin extends Plugin { static override pluginName = 'CUSTOM_URL_LOAD_PLUGIN' constructor( _config: undefined, @Inject(Injector) protected override readonly _injector: Injector ) { super() } override onStarting(): void { this._injector.add([IURLService, { useClass: WebURLService }]) this._injector.add([CollaborationDataLoaderController]) this.disposeWithMe(this._injector.get(IURLService)) this.disposeWithMe(this._injector.get(CollaborationDataLoaderController)) }}import { DataLoaderService } from '@univerjs-pro/collaboration-client'import { ILogService, Inject, RxDisposable } from '@univerjs/core'import { IURLService } from '../services/url.service'/** * Load a collaborative document from URL parameters. */export class CollaborationDataLoaderController extends RxDisposable { constructor( @IURLService private readonly _urlService: IURLService, @ILogService private readonly _logService: ILogService, @Inject(DataLoaderService) private readonly _dataLoaderService: DataLoaderService ) { super() const unitId = this._urlService.getParam('unit') const type = this._urlService.getParam('type') if (!unitId || !type) { return } this._dataLoaderService.loadUnit(unitId, Number(type)) .catch((error: unknown) => { this._logService.error('[CollaborationDataLoaderController]', error) }) }}import type { IDisposable } from '@univerjs/core'import type { Observable } from 'rxjs'import { createIdentifier } from '@univerjs/core'export interface IURLService extends IDisposable { getParam: (key: string) => string | undefined setParam: (key: string, value: string, useReplace?: boolean) => void removeParam: (key: string, useReplace?: boolean) => void readonly urlChange$: Observable<void>}export const IURLService = createIdentifier<IURLService>('demo.custom-url-load.url-service')import type { IURLService } from './url.service'import { RxDisposable } from '@univerjs/core'import { fromEvent, map, shareReplay, takeUntil } from 'rxjs'export class WebURLService extends RxDisposable implements IURLService { readonly urlChange$ = fromEvent(window, 'popstate').pipe( map(() => undefined), takeUntil(this.dispose$), shareReplay({ bufferSize: 1, refCount: true }) ) getParam(key: string): string | undefined { return new URL(window.location.href).searchParams.get(key) ?? undefined } setParam(key: string, value: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.set(key, value) this._updateUrl(url, useReplace) } removeParam(key: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.delete(key) this._updateUrl(url, useReplace) } private _updateUrl(url: URL, useReplace: boolean): void { if (useReplace) { window.history.replaceState(window.history.state, '', url.toString()) } else { window.history.pushState(window.history.state, '', url.toString()) } }}Verify synchronization and permissions
Open the same ID in two independent browser sessions. Edit text or move an element in one session and check the other. Verify that content survives page reloads, reconnections, and a service restart. Use viewer and unauthorized accounts to verify that the server rejects forbidden writes and reads.
Edit history and Import and export require separate configuration; collaboration does not enable them automatically.
How is this guide?