Frameworks
Vue
npm i @matrajs/vue Vue is first-class here rather than a community package that lags the React one. Both bindings are written together, tested together and released together, against the same version number. The engine comes with it · there is no second package.
An editor
<script setup lang="ts">
import { starterKit } from '@matrajs/core'
import { EditorContent, useEditor } from '@matrajs/vue'
const editor = useEditor({
extensions: starterKit,
content: '<p>Hello.</p>',
})
</script>
<template>
<EditorContent :editor="editor" />
</template>
The editor is destroyed when the component unmounts, or when the surrounding effect scope is
stopped — so it also works outside a component, in a composable or a store. It is
markRawped, so Vue never walks the document making it reactive: the editor
publishes its own changes and a proxy over a live document would cost a great deal to
achieve nothing.
A toolbar that stays honest
Commands mutate the document; Vue does not hear about that on its own, so a naive toolbar
goes stale the moment the caret moves. useEditorState returns a readonly ref that
follows both changes and selection changes.
<script setup lang="ts">
import type { Editor } from '@matrajs/core'
import { useEditorState } from '@matrajs/vue'
const props = defineProps<{ editor: Editor }>()
const bold = useEditorState(props.editor, (e) => e.isActive('bold'))
const canBold = useEditorState(props.editor, (e) => e.can.toggleBold())
const characters = useEditorState(props.editor, (e) => e.getText().length)
</script>
<template>
<button
:aria-pressed="bold"
:disabled="!canBold"
@mousedown.prevent="editor.commands.toggleBold()"
>
Bold
</button>
<span>{{ characters }}</span>
</template> @mousedown.prevent rather than @click. A click moves focus to the
button first and collapses the selection, so the command runs against a caret instead of the
words that were highlighted.
can is the same command asking rather than doing, which is how the button knows to
be disabled instead of looking enabled and doing nothing — the caret is in a code block, or the
selection cannot hold that mark.
Focus
const focused = useEditorFocus(editor) A readonly ref · useful for showing a toolbar only while the editor has the caret.
Saving
<script setup lang="ts">
import { onScopeDispose } from 'vue'
const editor = useEditor({ extensions: starterKit })
onScopeDispose(editor.on('change', () => save(editor.getJSON())))
</script> on returns its own unsubscribe function, so it hands straight to
onScopeDispose with nothing in between.
v-model, if you want it
There is no v-model on EditorContent on purpose: a two-way binding over
a live document means re-parsing it on every keystroke, and the parse is what loses a selection.
Write it yourself where you actually need it, and only in one direction.
const model = defineModel<string>()
const editor = useEditor({
extensions: starterKit,
content: model.value,
})
onScopeDispose(editor.on('change', () => {
model.value = editor.getHTML()
})) model.value from outside will not push back into the editor, which is the
behaviour you want · call editor.commands or
editor.setContent() for a deliberate change instead.
Nuxt
The editor touches the DOM only when it mounts, so it renders on the server without a shim.
Keep EditorContent out of the server pass and the rest works untouched:
<template>
<ClientOnly>
<EditorContent :editor="editor" />
</ClientOnly>
</template> useEditor itself runs on the server — the editor exists before anything is on screen,
so getJSON(), getHTML() and getText() all answer during
a server render, with no DOM polyfill.
content: '<p>…</p>' needs a browser
· content: fromMarkdown(text) and content: storedJson do not. Writing
HTML out with getHTML() works either way.
KeepAlive
The mount is guarded, so a component brought back by <KeepAlive> or a hot reload
does not attach a second view to one element and leave two carets fighting over it.