Saving and Loading Content
Saving and loading content is a fundamental requirement for any editor. ProseKit provides flexible options for persisting your content in different formats, primarily JSON and HTML. This guide will show you how to implement content persistence in your ProseKit editor.
Supported Formats
Section titled “Supported Formats”ProseKit supports multiple content formats:
| Format | Description | Best For |
|---|---|---|
| JSON | Native ProseMirror document structure | Long-term storage, preserving all features |
| HTML | Standard HTML markup | Interoperability with other systems, rendering content on the server side |
Working with JSON
Section titled “Working with JSON”JSON is the recommended format for storing ProseKit documents because it preserves the exact structure and all attributes of your content.
Saving Content as JSON
Section titled “Saving Content as JSON”To get your document as JSON, use the getDocJSON() method:
// Get the current document as a JSON object
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()Loading Content from JSON
Section titled “Loading Content from JSON”To load content from JSON when creating an editor:
// Create editor with the loaded content
const editor const editor: Editor<BasicExtension> = createEditor createEditor<BasicExtension>(options: EditorOptions<BasicExtension>): Editor<BasicExtension> ({
extension EditorOptions<BasicExtension>.extension: BasicExtensionThe extension to use when creating the editor. ,
defaultContent EditorOptions<E extends Extension>.defaultContent?: string | NodeJSON | HTMLElement | undefinedThe starting document to use when creating the editor. It can be a
ProseMirror node JSON object, a HTML string, or a HTML element instance. : json const json: NodeJSON , // Pass the JSON object directly
})Detecting Document Changes
Section titled “Detecting Document Changes”To save content when the document changes, use the useDocChange hook:
import { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. } from 'prosekit/react'
useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. (() => {
// This runs whenever the document changes
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document', JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. (json const json: NodeJSON ))
}, { editor UseExtensionOptions.editor?: Editor<any> | undefinedThe editor to add the extension to. If not provided, it will use the
editor from the nearest `<ProseKit>` component. })import { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. } from 'prosekit/vue'
useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. (() => {
// This runs whenever the document changes
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document', JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. (json const json: NodeJSON ))
}, { editor UseExtensionOptions.editor?: MaybeRefOrGetter<Editor<any>> | undefinedThe editor to add the extension to. If not provided, it will use the
editor from the nearest `<ProseKit>` component. })import { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. } from 'prosekit/svelte'
useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. (() => {
// This runs whenever the document changes
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document', JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. (json const json: NodeJSON ))
}, { editor UseExtensionOptions.editor?: Editor<any> | undefinedThe editor to add the extension to. If not provided, it will use the
editor from the nearest `<ProseKit>` component. })import { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. } from 'prosekit/preact'
useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. (() => {
// This runs whenever the document changes
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document', JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. (json const json: NodeJSON ))
}, { editor UseExtensionOptions.editor?: Editor<any> | undefinedThe editor to add the extension to. If not provided, it will use the
editor from the nearest `<ProseKit>` component. })import { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. } from 'prosekit/solid'
useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): voidCalls the given handler whenever the editor document changes. (() => {
// This runs whenever the document changes
const json const json: NodeJSON = editor const editor: Editor<BasicExtension> .getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSONReturn a JSON object representing the editor's current document. ()
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document', JSON var JSON: JSONAn intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format. .stringify JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)Converts a JavaScript value to a JavaScript Object Notation (JSON) string. (json const json: NodeJSON ))
}, { editor UseExtensionOptions.editor?: MaybeAccessor<Editor<any>> | undefinedThe editor to add the extension to. If not provided, it will use the
editor from the nearest `<ProseKit>` component. })JSON Example
Section titled “JSON Example”Here's a complete example of saving and loading content using JSON:
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
useCallback,
useMemo,
useState,
} from 'preact/hooks'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/preact'
export default function Editor() {
// A list of saved documents, stored as JSON strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const record = JSON.stringify(editor.getDocJSON())
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback((record: string) => {
editor.setContent(JSON.parse(record) as NodeJSON)
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
}, [])
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}export { default as ExampleEditor } from './editor'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/react'
import {
useCallback,
useMemo,
useState,
} from 'react'
export default function Editor() {
// A list of saved documents, stored as JSON strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const record = JSON.stringify(editor.getDocJSON())
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback((record: string) => {
editor.setContent(JSON.parse(record) as NodeJSON)
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
}, [])
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}'use client'
export { default as ExampleEditor } from './editor'<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/svelte'
// A list of saved documents, stored as JSON strings
let records = $state<string[]>([])
// Whether there are unsaved changes
let hasUnsavedChange = $state(false)
// A key to force a re-render of the editor
let key = $state(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const record = JSON.stringify(editor.getDocJSON())
records = [...records, record]
hasUnsavedChange = false
}
function handleLoad(record: string) {
editor.setContent(JSON.parse(record) as NodeJSON)
hasUnsavedChange = false
key += 1
}
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
disabled={!hasUnsavedChange}
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
onclick={handleSave}
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul class="border-b border-t border-solid text-sm">
{#each records as record, index (index)}
<li class="m-1 flex gap-2">
<button
class="border border-solid bg-white px-2 py-1 text-black"
onclick={() => handleLoad(record)}
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
{/each}
</ul>
{#key key}
<ProseKit {editor}>
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
{/key}
</div>export { default as ExampleEditor } from './editor.svelte'<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
type NodeJSON,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
// A list of saved documents, stored as JSON strings
const records = ref<string[]>([])
// Whether there are unsaved changes
const hasUnsavedChange = ref(false)
// A key to force a re-render of the editor
const key = ref(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange.value = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const record = JSON.stringify(editor.getDocJSON())
records.value = [...records.value, record]
hasUnsavedChange.value = false
}
function handleLoad(record: string) {
editor.setContent(JSON.parse(record) as NodeJSON)
hasUnsavedChange.value = false
key.value += 1
}
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
:disabled="!hasUnsavedChange"
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
@click="handleSave"
>
{{ hasUnsavedChange ? 'Save' : 'No changes to save' }}
</button>
<ul class="border-b border-t border-solid text-sm">
<li
v-for="(record, index) in records"
:key="index"
class="m-1 flex gap-2"
>
<button
class="border border-solid bg-white px-2 py-1 text-black"
@click="handleLoad(record)"
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{{ record }}</pre>
</span>
</li>
</ul>
<ProseKit :key="key" :editor="editor">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref="editorRef" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
</template>export { default as ExampleEditor } from './editor.vue'Working with HTML
Section titled “Working with HTML”HTML is useful when you need to display your content outside the editor or integrate with systems that understand HTML.
Saving Content as HTML
Section titled “Saving Content as HTML”To convert your document to HTML:
// Get the current document as an HTML string
const html const html: string = editor const editor: Editor<BasicExtension> .getDocHTML Editor<BasicExtension>.getDocHTML: (options?: getDocHTMLOptions) => stringReturn a HTML string representing the editor's current document. ()
// Store the HTML
localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .setItem Storage.setItem(key: string, value: string): voidThe **`setItem()`** method of the Storage interface, when passed a key name and value, will add that key to the given `Storage` object, or update that key's value if it already exists.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem) ('my-document-html', html const html: string )Loading Content from HTML
Section titled “Loading Content from HTML”To load content from HTML:
// Retrieve stored HTML
const html const html: string = localStorage var localStorage: Storage[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage) .getItem Storage.getItem(key: string): string | nullThe **`getItem()`** method of the Storage interface, when passed a key name, will return that key's value, or `null` if the key does not exist, in the given `Storage` object.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem) ('my-document-html') || ''
// Create editor with the loaded HTML
const editor const editor: Editor<BasicExtension> = createEditor createEditor<BasicExtension>(options: EditorOptions<BasicExtension>): Editor<BasicExtension> ({
extension EditorOptions<BasicExtension>.extension: BasicExtensionThe extension to use when creating the editor. ,
defaultContent EditorOptions<E extends Extension>.defaultContent?: string | NodeJSON | HTMLElement | undefinedThe starting document to use when creating the editor. It can be a
ProseMirror node JSON object, a HTML string, or a HTML element instance. : html const html: string , // Pass the HTML string
})HTML Example
Section titled “HTML Example”Here's an example of saving and loading content using HTML:
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
useCallback,
useMemo,
useState,
} from 'preact/hooks'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/preact'
export default function Editor() {
// A list of saved documents, stored as HTML strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const record = editor.getDocHTML()
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback(
(record: string) => {
editor.setContent(jsonFromHTML(record, { schema: editor.schema }))
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
},
[editor.schema],
)
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}export { default as ExampleEditor } from './editor'import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/react'
import {
useCallback,
useMemo,
useState,
} from 'react'
export default function Editor() {
// A list of saved documents, stored as HTML strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const record = editor.getDocHTML()
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback(
(record: string) => {
editor.setContent(jsonFromHTML(record, { schema: editor.schema }))
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
},
[editor.schema],
)
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}'use client'
export { default as ExampleEditor } from './editor'<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/svelte'
// A list of saved documents, stored as HTML strings
let records = $state<string[]>([])
// Whether there are unsaved changes
let hasUnsavedChange = $state(false)
// A key to force a re-render of the editor
let key = $state(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const record = editor.getDocHTML()
records = [...records, record]
hasUnsavedChange = false
}
function handleLoad(record: string) {
editor.setContent(jsonFromHTML(record, { schema: editor.schema }))
hasUnsavedChange = false
key += 1
}
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
disabled={!hasUnsavedChange}
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
onclick={handleSave}
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul class="border-b border-t border-solid text-sm">
{#each records as record, index (index)}
<li class="m-1 flex gap-2">
<button
class="border border-solid bg-white px-2 py-1 text-black"
onclick={() => handleLoad(record)}
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
{/each}
</ul>
{#key key}
<ProseKit {editor}>
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
{/key}
</div>export { default as ExampleEditor } from './editor.svelte'<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
// A list of saved documents, stored as HTML strings
const records = ref<string[]>([])
// Whether there are unsaved changes
const hasUnsavedChange = ref(false)
// A key to force a re-render of the editor
const key = ref(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange.value = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const record = editor.getDocHTML()
records.value = [...records.value, record]
hasUnsavedChange.value = false
}
function handleLoad(record: string) {
editor.setContent(jsonFromHTML(record, { schema: editor.schema }))
hasUnsavedChange.value = false
key.value += 1
}
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
:disabled="!hasUnsavedChange"
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
@click="handleSave"
>
{{ hasUnsavedChange ? 'Save' : 'No changes to save' }}
</button>
<ul class="border-b border-t border-solid text-sm">
<li
v-for="(record, index) in records"
:key="index"
class="m-1 flex gap-2"
>
<button
class="border border-solid bg-white px-2 py-1 text-black"
@click="handleLoad(record)"
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{{ record }}</pre>
</span>
</li>
</ul>
<ProseKit :key="key" :editor="editor">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref="editorRef" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
</template>export { default as ExampleEditor } from './editor.vue'Working with Markdown
Section titled “Working with Markdown”While ProseKit doesn't directly support Markdown as a storage format, you can add Markdown support using additional libraries.
Markdown Example
Section titled “Markdown Example”Here's an example of using Markdown for saving and loading. It uses the remark and rehype libraries to convert between Markdown and HTML.
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import {
useCallback,
useMemo,
useState,
} from 'preact/hooks'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/preact'
import {
htmlFromMarkdown,
markdownFromHTML,
} from './markdown'
export default function Editor() {
// A list of saved documents, stored as Markdown strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const html = editor.getDocHTML()
const record = markdownFromHTML(html)
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback(
(record: string) => {
const html = htmlFromMarkdown(record)
editor.setContent(jsonFromHTML(html, { schema: editor.schema }))
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
},
[editor.schema],
)
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}export { default as ExampleEditor } from './editor'import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import { unified } from 'unified'
export function markdownFromHTML(html: string): string {
return unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
.processSync(html)
.toString()
}
export function htmlFromMarkdown(markdown: string): string {
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkHtml)
.processSync(markdown)
.toString()
}import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/react'
import {
useCallback,
useMemo,
useState,
} from 'react'
import {
htmlFromMarkdown,
markdownFromHTML,
} from './markdown'
export default function Editor() {
// A list of saved documents, stored as Markdown strings
const [records, setRecords] = useState<string[]>([])
// Whether there are unsaved changes
const [hasUnsavedChange, setHasUnsavedChange] = useState(false)
// A key to force a re-render of the editor
const [key, setKey] = useState(1)
const editor = useMemo(() => {
const extension = defineBasicExtension()
return createEditor({ extension })
}, [])
const handleDocChange = useCallback(() => setHasUnsavedChange(true), [])
useDocChange(handleDocChange, { editor })
const handleSave = useCallback(() => {
const html = editor.getDocHTML()
const record = markdownFromHTML(html)
setRecords((prev) => [...prev, record])
setHasUnsavedChange(false)
}, [editor])
const handleLoad = useCallback(
(record: string) => {
const html = htmlFromMarkdown(record)
editor.setContent(jsonFromHTML(html, { schema: editor.schema }))
setHasUnsavedChange(false)
setKey((prev) => prev + 1)
},
[editor.schema],
)
return (
<div className="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
onClick={handleSave}
disabled={!hasUnsavedChange}
className="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul className="border-b border-t border-solid text-sm">
{records.map((record, index) => (
<li key={index} className="m-1 flex gap-2">
<button
className="border border-solid bg-white px-2 py-1 text-black"
onClick={() => handleLoad(record)}
>
Load
</button>
<span className="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
))}
</ul>
<ProseKit editor={editor} key={key}>
<div className="relative w-full flex-1 box-border overflow-y-auto">
<div ref={editor.mount} className="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
)
}'use client'
export { default as ExampleEditor } from './editor'import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import { unified } from 'unified'
export function markdownFromHTML(html: string): string {
return unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
.processSync(html)
.toString()
}
export function htmlFromMarkdown(markdown: string): string {
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkHtml)
.processSync(markdown)
.toString()
}<script lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/svelte'
import {
htmlFromMarkdown,
markdownFromHTML,
} from './markdown'
// A list of saved documents, stored as Markdown strings
let records = $state<string[]>([])
// Whether there are unsaved changes
let hasUnsavedChange = $state(false)
// A key to force a re-render of the editor
let key = $state(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const html = editor.getDocHTML()
const record = markdownFromHTML(html)
records = [...records, record]
hasUnsavedChange = false
}
function handleLoad(record: string) {
const html = htmlFromMarkdown(record)
editor.setContent(jsonFromHTML(html, { schema: editor.schema }))
hasUnsavedChange = false
key += 1
}
const mount = (element: HTMLElement) => {
editor.mount(element)
return { destroy: () => editor.unmount() }
}
</script>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
disabled={!hasUnsavedChange}
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
onclick={handleSave}
>
{hasUnsavedChange ? 'Save' : 'No changes to save'}
</button>
<ul class="border-b border-t border-solid text-sm">
{#each records as record, index (index)}
<li class="m-1 flex gap-2">
<button
class="border border-solid bg-white px-2 py-1 text-black"
onclick={() => handleLoad(record)}
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{record}</pre>
</span>
</li>
{/each}
</ul>
{#key key}
<ProseKit {editor}>
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div use:mount class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
{/key}
</div>export { default as ExampleEditor } from './editor.svelte'import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import { unified } from 'unified'
export function markdownFromHTML(html: string): string {
return unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
.processSync(html)
.toString()
}
export function htmlFromMarkdown(markdown: string): string {
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkHtml)
.processSync(markdown)
.toString()
}<script setup lang="ts">
import 'prosekit/basic/style.css'
import 'prosekit/basic/typography.css'
import { defineBasicExtension } from 'prosekit/basic'
import {
createEditor,
jsonFromHTML,
} from 'prosekit/core'
import {
ProseKit,
useDocChange,
} from 'prosekit/vue'
import {
ref,
watchPostEffect,
} from 'vue'
import {
htmlFromMarkdown,
markdownFromHTML,
} from './markdown'
// A list of saved documents, stored as Markdown strings
const records = ref<string[]>([])
// Whether there are unsaved changes
const hasUnsavedChange = ref(false)
// A key to force a re-render of the editor
const key = ref(1)
const extension = defineBasicExtension()
const editor = createEditor({ extension })
function handleDocChange() {
hasUnsavedChange.value = true
}
useDocChange(handleDocChange, { editor })
function handleSave() {
const html = editor.getDocHTML()
const record = markdownFromHTML(html)
records.value = [...records.value, record]
hasUnsavedChange.value = false
}
function handleLoad(record: string) {
const html = htmlFromMarkdown(record)
editor.setContent(jsonFromHTML(html, { schema: editor.schema }))
hasUnsavedChange.value = false
key.value += 1
}
const editorRef = ref<HTMLDivElement | null>(null)
watchPostEffect((onCleanup) => {
editor.mount(editorRef.value)
onCleanup(() => editor.unmount())
})
</script>
<template>
<div class="box-border h-full w-full min-h-36 overflow-y-hidden overflow-x-hidden rounded-md border border-solid border-gray-200 dark:border-gray-700 shadow-sm flex flex-col bg-white dark:bg-gray-950 text-black dark:text-white">
<button
:disabled="!hasUnsavedChange"
class="m-1 border border-solid bg-white px-2 py-1 text-sm text-black disabled:cursor-not-allowed disabled:text-gray-500"
@click="handleSave"
>
{{ hasUnsavedChange ? 'Save' : 'No changes to save' }}
</button>
<ul class="border-b border-t border-solid text-sm">
<li
v-for="(record, index) in records"
:key="index"
class="m-1 flex gap-2"
>
<button
class="border border-solid bg-white px-2 py-1 text-black"
@click="handleLoad(record)"
>
Load
</button>
<span class="flex-1 overflow-x-scroll p-2">
<pre>{{ record }}</pre>
</span>
</li>
</ul>
<ProseKit :key="key" :editor="editor">
<div class="relative w-full flex-1 box-border overflow-y-auto">
<div ref="editorRef" class="ProseMirror box-border min-h-full px-[max(4rem,calc(50%-20rem))] py-8 outline-hidden outline-0 [&_span[data-mention=user]]:text-blue-500 [&_span[data-mention=tag]]:text-violet-500"></div>
</div>
</ProseKit>
</div>
</template>export { default as ExampleEditor } from './editor.vue'import rehypeParse from 'rehype-parse'
import rehypeRemark from 'rehype-remark'
import remarkGfm from 'remark-gfm'
import remarkHtml from 'remark-html'
import remarkParse from 'remark-parse'
import remarkStringify from 'remark-stringify'
import { unified } from 'unified'
export function markdownFromHTML(html: string): string {
return unified()
.use(rehypeParse)
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
.processSync(html)
.toString()
}
export function htmlFromMarkdown(markdown: string): string {
return unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkHtml)
.processSync(markdown)
.toString()
}Conversion Utilities
Section titled “Conversion Utilities”ProseKit provides utility functions for converting between plain JSON object, HTML string, and ProseMirrorNode.