Collaboration
Follow Collaboration integration to run the official example, connect your application, and verify persistence and authorization. Implementation details and runnable Web/Server examples are maintained at Collaboration Quick Start.
Create the Unit through an authenticated business API calling Collaboration Service, then load the returned identifier in the browser. Configure HTTP, Session Ticket, and WebSocket entry points together using Quick Start. Persist data with a Database Adapter and enforce read, JOIN, and edit policies through server middleware.
Before using these APIs, verify that your target backend supports PDF Units. A working Sheets collaboration example does not establish PDF support.
Load a collaborative PDF
After creating a PDF unit through the server API or importPdfToUnitIdAsync, load it with the collaboration Facade:
Set the current user before loading the document. See Collaboration integration.
import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const pdf = await univerAPI.getCollaboration().loadPdfAsync(unitId)if (!pdf) { throw new Error(`PDF unit not found: ${unitId}`)}console.log(pdf.getId(), pdf.getName())loadPdfAsync loads the server snapshot, prepares the collaboration session, and returns an FPdf. It returns null when the unit cannot be associated with a PDF.
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 loadPdfAsync() 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()) } }}How is this guide?