# Vue integration

Mount the editor within your existing component tree, preserving themes, injection, and view lifecycles.

## Create and mount

`useMsfeEditor()` returns the shallow `editor` ref and component `host`. Render `VueNodeViewHost` in your template so component views retain your application's `provide/inject` context.

```vue
<script setup lang="ts">
import { shallowRef } from 'vue'
import { useMsfeEditor, VueNodeViewHost } from '@quirekit/msfe-vue'
import { StarterKit } from '@quirekit/msfe-starter-kit'

const parent = shallowRef<HTMLElement | null>(null)
const { editor, host } = useMsfeEditor(parent, () => ({
  markdown: '## Start writing',
  mode: 'live',
  extensions: [StarterKit],
}))
</script>

<template>
  <div ref="parent" />
  <VueNodeViewHost :host="host" />
</template>
```

The composable creates the instance after mounting and disposes of the editor and component host on unmount. Do not create a separate Vue application for node rendering.

`options.onError` receives runtime errors and exceptions from `createMsfeEditor()` during creation.
After failed creation, `editor.value` remains `undefined` and the component host is disposed.
Display an error and preserve the source before explicitly remounting with corrected configuration.
Without `onError`, creation exceptions continue to Vue's error handling. Exceptions from the options
factory itself also remain Vue-owned. The composable never retries or rebuilds a document automatically.

## Update content and mode

Initial options are not deeply watched. After mounting, use `setMode()`, `setReadOnly()`, `applyChanges()`, or `replaceDocument()`. Avoid writing the entire document back after each keystroke through a full-text `v-model`.

### basic/editor.vue

```vue
<script setup lang="ts">
import { computed, ref, shallowRef } from "vue";
import { useMsfeEditor, VueNodeViewHost } from "@quirekit/msfe-vue";
import { StarterKit } from "@quirekit/msfe-starter-kit";
import { QuireButton, QuireSegmented, QuireProseSource } from "@quirekit/ui/vue";
import { Appearance } from "../shared/appearance";
import { exampleMessages } from "../shared/messages";
import type { ExampleLocale } from "../shared/types";

const props = withDefaults(defineProps<{ /** Language supplied by the application. */ locale?: ExampleLocale }>(), { locale: "zh-CN" });
const text = computed(() => exampleMessages[props.locale]);
const initial = text.value.initial;
const parent = shallowRef<HTMLElement | null>(null);
const mode = ref("live");
const markdown = ref<string>();
const items = computed(() => [{ value: "live", label: text.value.live }, { value: "source", label: text.value.source }, { value: "locked", label: text.value.locked }]);

// One editor instance owns source, selection and history for all three modes.
const { editor, host } = useMsfeEditor(parent, () => ({ markdown: initial, mode: "live",
  extensions: [StarterKit, Appearance], label: text.value.editor }));

/** @param value - Mode selected in the shared control. */
function changeMode(value: string | null | undefined): void {
  if (value === "source" || value === "live" || value === "locked") { mode.value = value; editor.value?.setMode(value); }
}
/** Serializes only for an explicit save/read action; typing never mirrors the entire source. */
function readMarkdown(): void { markdown.value = editor.value?.getMarkdown(); }
/** Starts a fresh document and history after the user explicitly resets this example. */
function reset(): void { editor.value?.replaceDocument(initial, "example-reset"); markdown.value = undefined; }
</script>
<template>
  <div class="grid gap-4 p-qk-surface">
    <div class="flex flex-wrap items-center justify-between gap-3">
      <QuireSegmented :model-value="mode" :items="items" :label="text.modes" size="sm" @update:model-value="changeMode" />
      <div class="flex gap-2">
        <QuireButton size="sm" variant="ghost" :disabled="mode === 'locked'" @click="editor?.undo()">{{ text.undo }}</QuireButton>
        <QuireButton size="sm" variant="ghost" @click="reset">{{ text.reset }}</QuireButton>
        <QuireButton size="sm" variant="outline" @click="readMarkdown">{{ text.readMarkdown }}</QuireButton>
      </div>
    </div>
    <div ref="parent" class="overflow-hidden rounded-qk-control border border-qk-border-subtle" />
    <VueNodeViewHost :host="host" />
    <QuireProseSource v-if="markdown !== undefined" :code="markdown" language="markdown" filename="document.md" :max-height="260" />
  </div>
</template>

```

### shared/appearance.ts

```ts
import { Extension } from "@quirekit/msfe-core";
import { EditorView } from "@quirekit/cm-view";
import { Prec } from "@quirekit/cm-state";
import { HighlightStyle, syntaxHighlighting } from "@quirekit/cm-language";
import { tags } from "@lezer/highlight";
import { themeVariableReferences as token } from "@quirekit/ui";

/** Consumer-owned theme; source semantics stay in the installed feature packages. */
export const Appearance = Extension.create({ name: "documentationAppearance",
  /** @returns Scoped typography, surfaces and syntax colors from the host design system. */
  addCodeMirrorExtensions() {
    return Prec.high([
      EditorView.theme({
        "&": { ...token.typography.body, color: token.color.text.primary, backgroundColor: token.color.surface.page, minHeight: "180px" },
        ".cm-scroller": { fontFamily: "inherit", lineHeight: "1.8" },
        ".cm-content": { padding: token.spacing.surface, caretColor: token.color.text.primary },
        ".cm-line": { padding: "0" },
        ".cm-gutters": { color: token.color.text.tertiary, backgroundColor: token.color.surface.subtle, border: "none" },
        ".cm-activeLine, .cm-activeLineGutter": { backgroundColor: "transparent" },
        ".cm-selectionBackground, &.cm-focused .cm-selectionBackground": { backgroundColor: token.color.selection.document },
        ".cm-cursor": { borderLeftColor: token.color.text.primary },
        ".msfe-h1": { ...token.typography.pageTitle },
        ".msfe-h2, .msfe-h3": { ...token.typography.sectionTitle },
        ".msfe-heading": { paddingBlock: token.spacing.inline },
        ".msfe-link": { color: token.color.text.accent },
        ".msfe-code-line, .msfe-inline-code": { ...token.typography.code },
        ".consumer-status": { verticalAlign: "middle" },
        "&.cm-focused": { outline: "none" },
      }),
      syntaxHighlighting(HighlightStyle.define([
        { tag: tags.heading, color: token.color.text.primary, fontWeight: "600" },
        { tag: tags.strong, fontWeight: "700" },
        { tag: tags.emphasis, fontStyle: "italic" },
        { tag: [tags.link, tags.url], color: token.color.text.accent },
        { tag: tags.processingInstruction, color: token.color.text.tertiary },
        { tag: tags.monospace, color: token.color.text.secondary },
      ])),
    ]);
  },
});

```

### shared/messages.ts

```ts
const chinese = {
  editor: "Markdown 示例编辑器", modes: "编辑器模式", source: "源码", live: "实时预览", locked: "只读",
  undo: "撤销", redo: "重做", reset: "重置", readMarkdown: "读取 Markdown", readingPreview: "阅读预览", insertStatus: "插入状态", done: "已完成", todo: "待处理",
  nativeEditor: "原生 DOM 示例编辑器", statusEditor: "自定义状态编辑器",
  initial: "## 把想法写下来\n\n在这里编辑 **Markdown**，也可以切换到源码。\n\n- [x] 保留 Markdown 原文\n- [ ] 试试修改这一行\n",
  nativeInitial: "## 原生 DOM 接入\n\n这个实例直接使用 `createMsfeEditor()`。\n",
  statusInitial: "## 自定义状态\n\n设计评审：[[todo]]\n\n点击状态按钮，然后读取 Markdown 或撤销。\n",
};
const english: { [Key in keyof typeof chinese]: typeof chinese[Key] } = {
  editor: "Markdown example editor", modes: "Editor mode", source: "Source", live: "Live", locked: "Locked",
  undo: "Undo", redo: "Redo", reset: "Reset", readMarkdown: "Read Markdown", readingPreview: "Reading preview", insertStatus: "Insert status", done: "Done", todo: "Pending",
  nativeEditor: "Native DOM example editor", statusEditor: "Custom status editor",
  initial: "## Write your ideas\n\nEdit **Markdown** here, or switch to source.\n\n- [x] Preserve the Markdown source\n- [ ] Try editing this line\n",
  nativeInitial: "## Native DOM integration\n\nThis instance uses `createMsfeEditor()` directly.\n",
  statusInitial: "## Custom status\n\nDesign review: [[todo]]\n\nClick the status, then read the Markdown or undo.\n",
};
export const exampleMessages = { "zh-CN": chinese, en: english };

```

### shared/types.ts

```ts
/** Example host language; independent of the documentation router. */
export type ExampleLocale = "zh-CN" | "en";

```

## Observe editor state in Vue

Use `useMsfeState()` inside `setup()` instead of wiring `watch(editor)`, initial snapshots and cleanup yourself:

```ts
import { useMsfeState } from '@quirekit/msfe-vue'

const state = useMsfeState(editor) // Pass directly to MdProse.state
const cursor = useMsfeState(editor, {
  select: state => state.selection.main.head,
})
```

The source accepts an instance, ref or getter such as `() => props.editor`. The result is a readonly
shallow ref. It is `undefined` before creation, after failed creation, or when the instance is cleared
or destroyed; use `v-if="state"` to wait for a snapshot. Scope disposal and instance replacement
automatically unsubscribe, without destroying a borrowed editor.

The composable connects to MSFE's `subscribeState()` and observes accepted CodeMirror states only.
References update synchronously without cloning state, reparsing, serializing the document or writing
back. Components that do not call this composable gain no subscription. `select` and optional `equal`
follow the core contract: selectors must be pure and bounded, run once per accepted update, and notify
Vue only when their result changes. Equality defaults to `Object.is`. For newly allocated objects,
provide a comparator such as `equal: (a, b) => a.from === b.from && a.to === b.to`.

The selector is fixed when the composable is created. It depends on the supplied immutable state and
does not track other Vue refs read inside it. Combine reactive inputs such as locale with the returned
value in a separate `computed()`. Use full snapshots for readers and narrow fields for small controls.
`MdProse` still decides whether selection-only changes can reuse reading content.

## Nuxt and server rendering

The editor requires the browser DOM. In Nuxt, use a client component or `ClientOnly` around the editor mount; documentation and navigation can still render on the server. Reserve reasonable placeholder height to limit layout shifts during loading.

## Custom components

Use `VueNodeViewRenderer()` to replace a source node's UI, or `VueComponentViewRenderer()` for controls such as search panels. See [Custom nodes](/en/extensions/custom-node) for the complete field-update lifecycle.

## Providers and NodeViews

Provide locale, permissions and host services through the existing application tree. NodeViews and ComponentViews retain that context through the Host; `shared/locale.ts` demonstrates localization. Do not persist reactive UI objects or services in Markdown attributes.

Changing the complete extension set requires an explicit lifecycle decision: preserve source and required host state, then recreate. The options factory is an initialization entry, not a deep reactive configuration or hot plugin loader.

## Nuxt checks

1. Create the DOM editor in client-only components and render explanatory content/placeholders on the server.
2. Avoid duplicate Vue and CodeMirror state/view copies; deduplicate linked workspace packages in the bundler.
3. Install component dependencies in the consuming app rather than relying on documentation-site aliases.
4. Resolve unsaved state before routing away. Unmount cleanup alone cannot guarantee persistence.

For complex components see [nodes](/en/extensions/custom-node) and [diagrams](/en/extensions/diagrams).
