Frameworks

Plain JavaScript

No binding, no build step, no framework. The core is the whole thing · the bindings that exist for React, Vue, Svelte and Solid only wrap what is on this page.

npm i @matrajs/core

An editor

import { createEditor, starterKit } from '@matrajs/core'

const editor = createEditor({
  extensions: starterKit,
  content: '<p>Hello.</p>',
  element: document.querySelector('#editor'),
})

element mounts it immediately. Leave it out and you get an unmounted editor to editor.mount(el) yourself later, which is what the bindings do when the element does not exist until after a render.

Without a bundler

A <script type="module"> and a CDN is enough. Every editor on this site is exactly that.

<div id="editor"></div>
<script type="module">
  import { createEditor, starterKit } from 'https://esm.sh/@matrajs/core'

  createEditor({
    extensions: starterKit,
    content: '<p>Hello.</p>',
    element: document.querySelector('#editor'),
  })
</script>

A toolbar

The two things worth knowing without a binding: editor.on() returns its own unsubscribe, and editor.isActive() reads the answer out of the document rather than out of a variable you have to keep in step.

const bold = document.querySelector('#bold')

bold.addEventListener('click', () => {
  editor.commands.toggleBold()
  editor.focus()
})

const off = editor.on('selectionChange', () => {
  bold.setAttribute('aria-pressed', String(editor.isActive('bold')))
})

editor.focus() after a command, always. A click on a button moves focus to the button, and a command that runs with the caret gone looks like a command that did nothing.

Reading it back

editor.getJSON()   // the document · store this
editor.getHTML()   // markup
toMarkdown(editor.getJSON())

Store the JSON, not the HTML. HTML has to be parsed back through the schema on the way in, and anything the schema does not recognise is dropped · JSON round-trips exactly.

Tearing it down

off()             // any listener you registered
editor.destroy()  // the editor, its view and its plugins

destroy() leaves the element where it was and safe to mount into again. Not calling it on a page that swaps editors in and out is the one leak worth watching for.

On a server

None of this needs a browser. createEditor builds a document with no view, and renderToHTML skips the editor entirely · both run in Node, in a worker and at the edge.

import { renderToHTML, starterKit } from '@matrajs/core'

// no editor, no DOM, no schema build
const html = renderToHTML(storedJson, starterKit)

One condition on a server: load JSON or Markdown, not an HTML string. Reading HTML is a DOM job, so content: '<p>…</p>' needs a browser while content: storedJson and content: fromMarkdown(text) do not. Writing HTML out works either way.

TypeScript

Commands are inferred from the extensions you pass, so this is a compile error rather than a silent no-op at run time — with no generics to thread and nothing to declare.

const editor = createEditor({ extensions: [document, paragraph, text] })

editor.commands.toggleHeading(2)
//              ^ no extension defines this

Edit this page on GitHub