Save, open and convert files
Connect unsaved state, versioned JSON and DOCX services while handling storage failures and conversion loss.
Classic does not automatically write documents to your database. The application owns storage, document identity and unsaved indicators; the product exposes content and installed format services. Start with a mounted instance from the quick start.
1. Observe actual document changes
A product plugin's onUpdate also runs for selection and interface refreshes. Subscribe to Core's update in onMount, compare immutable document identities, and unsubscribe in onUnmount. Structural updates rebind to the new Core; locale, editability and built-in theme updates retain the existing subscription.
import type { QuireEditor, QuireEditorUpdateEvent } from "@quirekit/core";
import { defineClassicEditorPlugin } from "@quirekit/classic-editor";
import type { ClassicEditorPlugin } from "@quirekit/classic-editor";
/**
* Observes document changes across product remounts without treating UI updates as edits.
* @param onChange - Host callback that marks the current document as unsaved.
* @returns Product plugin; create a separate plugin object for each Classic instance.
*/
export function createClassicDraftObserver(onChange: () => void): ClassicEditorPlugin {
let owner: QuireEditor | undefined;
/** @param event - Accepted update, which can also represent an editability change. */
function onUpdate(event: QuireEditorUpdateEvent): void {
// Core already suppresses equal-content updates; an editability notification
// retains the same immutable document. The observer needs only identity.
if (event.previousState.doc !== event.editor.state.doc) onChange();
}
/** Releases the subscription belonging to the previous mounted Core. */
function detach(): void {
owner?.off("update", onUpdate);
owner = undefined;
}
return defineClassicEditorPlugin({
id: "app.draft-observer",
/** @param context - Current mount, whose Core owns this subscription. */
onMount(context) {
detach();
owner = context.editor;
owner?.on("update", onUpdate);
},
onUnmount: detach,
onDestroy: detach,
});
}
Add createClassicDraftObserver(() => { dirty = true }) to the instance's plugins. dirty is application state; create a separate plugin object for each instance. The recipe avoids serializing the entire document per update and ignores editability-only changes. Restoration using emitUpdate: false must explicitly establish the host's saved baseline.
2. Save a specific revision
instance.getJSON() reads canonical content. Include your model, version and document ID in backups. Set the visible name with instance.setDocumentName(name) and persist it separately: getJSON() does not contain the product document name.
The Word draft recipe captures content before awaiting storage. A true result means the active page still owns that saved revision. false means writing succeeded but editing, remounting or exit occurred meanwhile; retain the unsaved indicator.
import type { ClassicDraftOwner, ClassicDraftRepository } from "./types";
/**
* Saves one revision without clearing changes made while storage was pending.
* @param instance - Mounted product that owns the current document.
* @param repository - Application storage; it must serialize writes for the same document.
* @returns Whether the acknowledged revision still belongs to the active Core and document.
*/
export async function saveClassicDraft(
instance: ClassicDraftOwner,
repository: ClassicDraftRepository
): Promise<boolean> {
const editor = instance.editor;
if (instance.isDestroyed || !editor || editor.isDestroyed) {
throw new Error("Mount the document before saving");
}
// Capture before awaiting: a later edit or product remount owns a different revision.
const document = editor.state.doc;
const source = JSON.stringify({ version: 1, model: "word-v1", content: instance.getJSON() });
await repository.write(source);
return !instance.isDestroyed && !editor.isDestroyed
&& instance.editor === editor && editor.state.doc === document;
}
Connect repository.write(source) to storage and reject on failure. Disable saving while pending, or serialize writes for the same document in the repository. This result protects UI acknowledgement; it cannot prevent out-of-order server writes. Shared editing additionally requires a version-conflict protocol. The recipe persists JSON only; image bytes need durable media services.
3. Open a JSON backup
The built-in json service accepts raw canonical document JSON and strictly replaces through the same guarded commit. It does not decode the version/model/content envelope used by this page's business backup. Validate and unwrap that envelope in the application before following the opening flow below.
Read the original file, validate its model/version, nodes, marks, attributes and Schema, then commit. See content validation and file operations. Invalid input must leave the current draft intact.
Before opening another business document, resolve unsaved changes and establish a new session:
instance.update({ document: validatedContent })
instance.setDocumentName('Project notes')
This destroys the old Core and mounts the replacement document without preserving undo history. Plugins rebind through mount hooks. Switch application document ID, storage key and saved baseline together, and cancel old save work. Do not pass document repeatedly while typing.
To restore content within the existing session, Core's replaceDocument() also replaces root attributes and excludes this replacement from history. Its lifecycle differs from a new session.
4. Install DOCX / DOTX services
createClassicWordDocxPlugin() registers docx and dotx by default. Install it once through plugins alongside the Word preset; selecting a preset alone does not register these services.
plugins: [createClassicWordDocxPlugin({
onReport(report) {
showConversionReport(report.warnings, report.diagnostics)
},
})]
showConversionReport is your interface function. Present parser warnings and diagnostics describing approximation, preservation or loss. Successful import does not prove lossless conversion: invalid blocks can become plain text and report schema-invalid-block. See DOCX plugin options.
5. Call a file service
await instance.importDocument('docx', {
data: new Uint8Array(await file.arrayBuffer()),
fileName: file.name,
mimeType: file.type,
})
const artifact = await instance.exportDocument('docx')
file is a host-selected File. DOCX accepts bytes rather than text; use the dotx service ID for templates. Export returns data, fileName and mimeType; the host downloads or uploads the artifact. File generation does not acknowledge database storage. Missing services, unsupported operations and unmounted products reject the Promise; catch conversion errors as well.
6. Cancellation and stale results
importDocument() captures the current Core and document revision. A newer import, document edit (including edit then undo), editability update, unmount or destruction invalidates an older uncommitted import. Moving the caret does not invalidate content. Official DOCX import rechecks ownership at its synchronous commit boundary. Stale operations reject with AbortError, preserving current content and name without a successful conversion report.
const opening = new AbortController()
try {
const data = new Uint8Array(await file.arrayBuffer())
await instance.importDocument('docx', {
data, fileName: file.name, mimeType: file.type,
}, { signal: opening.signal })
} catch (error) {
if (!(error instanceof DOMException && error.name === 'AbortError')) throw error
}
// The host calls opening.abort() on cancellation, replacement or exit.
Create the controller before reading bytes. Cancel the preceding selection before reading another file, so a late arrayBuffer() cannot start an obsolete import. Abort ends the caller's wait promptly; a non-cooperative codec may finish parsing privately, but cannot commit a stale result. Cancellation after an accepted commit does not undo it.
Read-only state is checked at entry and before commit. The application still checks business authorization. Opening a different business document also requires dirty-state, storage-key and session management.
7. Migrate custom import services
Parse and validate first, then execute one synchronous command inside context.commit(() => editor.commands.replaceDocument(...)) and report success afterwards. Do not pass an async callback or write outside commit. Runtime cannot roll back arbitrary side effects from an older plugin that bypasses this contract; upgrade it before depending on stale-result protection. See Classic API for the complete context.
Export snapshot timing belongs to each service; official DOCX export captures the starting document. See durable media for staged images and cleanup. Custom Core products still use the host parse/validate/commit flow.
See Classic instance and configuration reference for complete public fields.