# 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](/en/classic/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.

### classic/persistence/observer.ts

```ts
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.

### classic/persistence/save.ts

```ts
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;
}

```

### classic/persistence/types.ts

```ts
import type { ClassicEditorInstance } from "@quirekit/classic-editor";

/** Application-owned destination for one explicitly captured draft. */
export interface ClassicDraftRepository {
  /**
   * Persists one complete serialized draft, rejecting on failure.
   * @param source - Versioned JSON captured when saving began.
   * @returns Completion after storage acknowledges the write.
   */
  write(source: string): Promise<void>;
}

/** Product facts required to determine whether a saved draft is still current. */
export type ClassicDraftOwner = Pick<ClassicEditorInstance, "editor" | "getJSON" | "isDestroyed">;

```

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](/en/build/resources).

## 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](/en/guide/content) and [file operations](/en/build/files). Invalid input must leave the current draft intact.

Before opening another business document, resolve unsaved changes and establish a new session:

```ts [Commit an already validated backup]
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.

```ts [Install through the quick start's plugins option]
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](/en/plugins/reference/create-classic-word-docx-plugin).

## 5. Call a file service

```ts [Pass file bytes and obtain an export artifact]
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.

```ts [Cancel an open request]
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](/en/classic/api) for the complete context.

Export snapshot timing belongs to each service; official DOCX export captures the starting document. See [durable media](/en/classic/media) for staged images and cleanup. Custom Core products still use the [host parse/validate/commit flow](/en/build/files).

See [Classic instance and configuration reference](/en/classic/reference) for complete public fields.
