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.
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, display |
JSON is the recommended format for storing ProseKit documents because it preserves the exact structure and all attributes of your content.
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: () => NodeJSON
Return a JSON object representing the editor's current document.
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: BasicExtension
The extension to use when creating the editor.: defaultContent EditorOptions<E extends Extension>.defaultContent?: string | NodeJSON | HTMLElement | undefined
The starting document to use when creating the editor. It can be a ProseMirror node JSON object, a HTML string, or a HTML element instance., // Pass the JSON object directly }) json const json: NodeJSON
To save content when the document changes, use the useDocChange
hook:
import {
} from 'prosekit/react' useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.(() => { // This runs whenever the document changes const useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.= json const json: NodeJSON
. editor const editor: Editor<BasicExtension>
() getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSON
Return a JSON object representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem). JSON var JSON: JSON
An 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> | undefined
The editor to add the extension to. If not provided, it will use the editor from the nearest `ProseKit` component.
import {
} from 'prosekit/vue' useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.(() => { // This runs whenever the document changes const useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.= json const json: NodeJSON
. editor const editor: Editor<BasicExtension>
() getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSON
Return a JSON object representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem). JSON var JSON: JSON
An 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>> | undefined
The editor to add the extension to. If not provided, it will use the editor from the nearest `ProseKit` component.
import {
} from 'prosekit/svelte' useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.(() => { // This runs whenever the document changes const useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.= json const json: NodeJSON
. editor const editor: Editor<BasicExtension>
() getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSON
Return a JSON object representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem). JSON var JSON: JSON
An 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> | undefined
The editor to add the extension to. If not provided, it will use the editor from the nearest `ProseKit` component.
import {
} from 'prosekit/preact' useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.(() => { // This runs whenever the document changes const useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.= json const json: NodeJSON
. editor const editor: Editor<BasicExtension>
() getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSON
Return a JSON object representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem). JSON var JSON: JSON
An 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> | undefined
The editor to add the extension to. If not provided, it will use the editor from the nearest `ProseKit` component.
import {
} from 'prosekit/solid' useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.(() => { // This runs whenever the document changes const useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes.= json const json: NodeJSON
. editor const editor: Editor<BasicExtension>
() getDocJSON Editor<BasicExtension>.getDocJSON: () => NodeJSON
Return a JSON object representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem). JSON var JSON: JSON
An 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>> | undefined
The editor to add the extension to. If not provided, it will use the editor from the nearest `ProseKit` component.
Here’s a complete example of saving and loading content using JSON:
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/react' export default function EditorComponent({ editor, onDocChange, }: { editor: Editor onDocChange: () => void }) { useDocChange(onDocChange, { editor }) return ( <ProseKit editor={editor}> <div className='relative w-full flex-1 box-border overflow-y-scroll'> <div ref={editor.mount} className='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500'></div> </div> </ProseKit> ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, type NodeJSON, } from 'prosekit/core' import { useCallback, useMemo, useState, } from 'react' import EditorComponent from './editor-component' export default function Editor() { const [defaultContent, setDefaultContent] = useState<NodeJSON | undefined>() const [records, setRecords] = useState<string[]>([]) const [hasUnsavedChange, setHasUnsavedChange] = useState(false) const [key, setKey] = useState(1) // Create a new editor instance whenever `defaultContent` changes const editor = useMemo(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent }) }, [defaultContent]) // Enable the save button const handleDocChange = useCallback(() => setHasUnsavedChange(true), []) // Save the current document as a JSON string const handleSave = useCallback(() => { const record = JSON.stringify(editor.getDocJSON()) setRecords((records) => [...records, record]) setHasUnsavedChange(false) }, [editor]) // Load a document from a JSON string const handleLoad = useCallback((record: string) => { setDefaultContent(JSON.parse(record)) setHasUnsavedChange(false) setKey((key) => key + 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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent key={key} editor={editor} onDocChange={handleDocChange} /> </div> ) }
<script lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/svelte' export let editor: Editor export let onDocChange: () => void useDocChange( () => { onDocChange?.() }, { editor }, ) const mount = (element: HTMLElement) => { editor.mount(element) return { destroy: () => editor.unmount() } } </script> <ProseKit {editor}> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div use:mount class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500'></div> </div> </ProseKit>
<script lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import EditorComponent from './editor-component.svelte' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, type NodeJSON, } from 'prosekit/core' let defaultContent: NodeJSON | undefined let hasUnsavedChange = false let records: string[] = [] let key = 1 // Create a new editor instance whenever `defaultContent` changes $: editor = createEditor({ extension: defineBasicExtension(), defaultContent }) // Enable the save button function handleDocChange() { hasUnsavedChange = true } // Save the current document as a JSON string function handleSave() { const record = JSON.stringify(editor.getDocJSON()) records = [...records, record] hasUnsavedChange = false } // Load a document from a JSON string function handleLoad(record: string) { defaultContent = JSON.parse(record) key++ hasUnsavedChange = false } </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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-white'> <button on:click={handleSave} 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" > {hasUnsavedChange ? 'Save' : 'No changes to save'} </button> <ul class="border-b border-t border-solid text-sm"> {#each records as record} <li class="m-1 flex gap-2"> <button class="border border-solid bg-white px-2 py-1 text-black" on:click={() => handleLoad(record)} > Load </button> <span class="flex-1 overflow-x-scroll p-2"> <pre>{record}</pre> </span> </li> {/each} </ul> {#key key} <EditorComponent {editor} onDocChange={handleDocChange} /> {/key} </div>
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/vue' import { ref, watchPostEffect, } from 'vue' const props = defineProps<{ editor: Editor }>() const emit = defineEmits<{ docChange: [] }>() useDocChange( () => { emit('docChange') }, { editor: props.editor }, ) const editorRef = ref<HTMLDivElement | null>(null) watchPostEffect((onCleanup) => { const editor = props.editor editor.mount(editorRef.value) onCleanup(() => editor.unmount()) }) </script> <template> <ProseKit :editor="editor"> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div ref="editorRef" class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' /> </div> </ProseKit> </template>
<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 { computed, ref, } from 'vue' import EditorComponent from './editor-component.vue' const defaultContent = ref<NodeJSON | undefined>() const records = ref<string[]>([]) const hasUnsavedChange = ref(false) const key = ref(1) // Create a new editor instance whenever `defaultContent` changes const editor = computed(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent: defaultContent.value }) }) // Enable the save button const handleDocChange = () => (hasUnsavedChange.value = true) // Save the current document as a JSON string function handleSave() { const record = JSON.stringify(editor.value.getDocJSON()) records.value.push(record) hasUnsavedChange.value = false } // Load a document from a JSON string function handleLoad(record: string) { defaultContent.value = JSON.parse(record) hasUnsavedChange.value = false key.value++ } </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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent :key="key" :editor="editor" @doc-change="handleDocChange" /> </div> </template>
HTML is useful when you need to display your content outside the editor or integrate with systems that understand 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>
() // Store the HTML getDocHTML Editor<BasicExtension>.getDocHTML: (options?: getDocHTMLOptions) => string
Return a HTML string representing the editor's current document.. localStorage var localStorage: Storage
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/localStorage)('my-document-html', setItem Storage.setItem(key: string, value: string): void
Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously. Throws a "QuotaExceededError" DOMException exception if the new value couldn't be set. (Setting could fail if, e.g., the user has disabled storage for the site, or if the quota has been exceeded.) Dispatches a storage event on Window objects holding an equivalent Storage object. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/setItem)) html const html: string
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)('my-document-html') || '' // Create editor with the loaded HTML const getItem Storage.getItem(key: string): string | null
Returns the current value associated with the given key, or null if the given key does not exist. [MDN Reference](https://developer.mozilla.org/docs/Web/API/Storage/getItem)= editor const editor: Editor<BasicExtension>
({ createEditor createEditor<BasicExtension>(options: EditorOptions<BasicExtension>): Editor<BasicExtension>
, extension EditorOptions<BasicExtension>.extension: BasicExtension
The extension to use when creating the editor.: defaultContent EditorOptions<E extends Extension>.defaultContent?: string | NodeJSON | HTMLElement | undefined
The starting document to use when creating the editor. It can be a ProseMirror node JSON object, a HTML string, or a HTML element instance., // Pass the HTML string }) html const html: string
Here’s an example of saving and loading content using HTML:
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/react' export default function EditorComponent({ editor, onDocChange, }: { editor: Editor onDocChange: () => void }) { useDocChange(onDocChange, { editor }) return ( <ProseKit editor={editor}> <div className='relative w-full flex-1 box-border overflow-y-scroll'> <div ref={editor.mount} className='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500'></div> </div> </ProseKit> ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, jsonFromHTML, type NodeJSON, } from 'prosekit/core' import { useCallback, useMemo, useState, } from 'react' import EditorComponent from './editor-component' export default function Editor() { const [defaultContent, setDefaultContent] = useState<NodeJSON | undefined>() const [records, setRecords] = useState<string[]>([]) const [hasUnsavedChange, setHasUnsavedChange] = useState(false) const [key, setKey] = useState(1) // Create a new editor instance whenever `defaultContent` changes const editor = useMemo(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent }) }, [defaultContent]) // Enable the save button const handleDocChange = useCallback(() => setHasUnsavedChange(true), []) // Save the current document as a HTML string const handleSave = useCallback(() => { const record = editor.getDocHTML() setRecords((records) => [...records, record]) setHasUnsavedChange(false) }, [editor]) // Load a document from a HTML string const handleLoad = useCallback( (record: string) => { setDefaultContent(jsonFromHTML(record, { schema: editor.schema })) setHasUnsavedChange(false) setKey((key) => key + 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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent key={key} editor={editor} onDocChange={handleDocChange} /> </div> ) }
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/vue' import { ref, watchPostEffect, } from 'vue' const props = defineProps<{ editor: Editor }>() const emit = defineEmits<{ docChange: [] }>() useDocChange( () => { emit('docChange') }, { editor: props.editor }, ) const editorRef = ref<HTMLDivElement | null>(null) watchPostEffect((onCleanup) => { const editor = props.editor editor.mount(editorRef.value) onCleanup(() => editor.unmount()) }) </script> <template> <ProseKit :editor="editor"> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div ref="editorRef" class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' /> </div> </ProseKit> </template>
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, jsonFromHTML, type NodeJSON, } from 'prosekit/core' import { computed, ref, } from 'vue' import EditorComponent from './editor-component.vue' const defaultContent = ref<NodeJSON | undefined>() const records = ref<string[]>([]) const hasUnsavedChange = ref(false) const key = ref(1) // Create a new editor instance whenever `defaultContent` changes const editor = computed(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent: defaultContent.value }) }) // Enable the save button const handleDocChange = () => (hasUnsavedChange.value = true) // Save the current document as a HTML string function handleSave() { const record = editor.value.getDocHTML() records.value.push(record) hasUnsavedChange.value = false } // Load a document from a HTML string function handleLoad(record: string) { defaultContent.value = jsonFromHTML(record, { schema: editor.value.schema }) hasUnsavedChange.value = false key.value++ } </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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent :key="key" :editor="editor" @doc-change="handleDocChange" /> </div> </template>
While ProseKit doesn’t directly support Markdown as a storage format, you can add Markdown support using additional libraries.
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 type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/react' export default function EditorComponent({ editor, onDocChange, }: { editor: Editor onDocChange: () => void }) { useDocChange(onDocChange, { editor }) return ( <ProseKit editor={editor}> <div className='relative w-full flex-1 box-border overflow-y-scroll'> <div ref={editor.mount} className='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500'></div> </div> </ProseKit> ) }
import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, jsonFromHTML, type NodeJSON, } from 'prosekit/core' import { useCallback, useMemo, useState, } from 'react' import EditorComponent from './editor-component' import { htmlFromMarkdown, markdownFromHTML, } from './markdown' export default function Editor() { const [defaultContent, setDefaultContent] = useState<NodeJSON | undefined>() const [records, setRecords] = useState<string[]>([]) const [hasUnsavedChange, setHasUnsavedChange] = useState(false) const [key, setKey] = useState(1) // Create a new editor instance whenever `defaultContent` changes const editor = useMemo(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent }) }, [defaultContent]) // Enable the save button const handleDocChange = useCallback(() => setHasUnsavedChange(true), []) // Save the current document as a Markdown string const handleSave = useCallback(() => { const html = editor.getDocHTML() const record = markdownFromHTML(html) setRecords((records) => [...records, record]) setHasUnsavedChange(false) }, [editor]) // Load a document from a Markdown string const handleLoad = useCallback( (record: string) => { const html = htmlFromMarkdown(record) setDefaultContent(jsonFromHTML(html, { schema: editor.schema })) setHasUnsavedChange(false) setKey((key) => key + 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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent key={key} editor={editor} onDocChange={handleDocChange} /> </div> ) }
import rehypeParse from 'rehype-parse' import rehypeRemark from 'rehype-remark' 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(remarkStringify) .processSync(html) .toString() } export function htmlFromMarkdown(markdown: string): string { return unified() .use(remarkParse) .use(remarkHtml) .processSync(markdown) .toString() }
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import type { Editor } from 'prosekit/core' import { ProseKit, useDocChange, } from 'prosekit/vue' import { ref, watchPostEffect, } from 'vue' const props = defineProps<{ editor: Editor }>() const emit = defineEmits<{ docChange: [] }>() useDocChange( () => { emit('docChange') }, { editor: props.editor }, ) const editorRef = ref<HTMLDivElement | null>(null) watchPostEffect((onCleanup) => { const editor = props.editor editor.mount(editorRef.value) onCleanup(() => editor.unmount()) }) </script> <template> <ProseKit :editor="editor"> <div class='relative w-full flex-1 box-border overflow-y-scroll'> <div ref="editorRef" class='ProseMirror box-border min-h-full px-[max(4rem,_calc(50%-20rem))] py-8 outline-none outline-0 [&_span[data-mention="user"]]:text-blue-500 [&_span[data-mention="tag"]]:text-violet-500' /> </div> </ProseKit> </template>
<script setup lang="ts"> import 'prosekit/basic/style.css' import 'prosekit/basic/typography.css' import { defineBasicExtension } from 'prosekit/basic' import { createEditor, jsonFromHTML, type NodeJSON, } from 'prosekit/core' import { computed, ref, } from 'vue' import EditorComponent from './editor-component.vue' import { htmlFromMarkdown, markdownFromHTML, } from './markdown' const defaultContent = ref<NodeJSON | undefined>() const records = ref<string[]>([]) const hasUnsavedChange = ref(false) const key = ref(1) // Create a new editor instance whenever `defaultContent` changes const editor = computed(() => { const extension = defineBasicExtension() return createEditor({ extension, defaultContent: defaultContent.value }) }) // Enable the save button const handleDocChange = () => (hasUnsavedChange.value = true) // Save the current document as a Markdown string function handleSave() { const html = editor.value.getDocHTML() const record = markdownFromHTML(html) records.value.push(record) hasUnsavedChange.value = false } // Load a document from a Markdown string function handleLoad(record: string) { const html = htmlFromMarkdown(record) defaultContent.value = jsonFromHTML(html, { schema: editor.value.schema }) hasUnsavedChange.value = false key.value++ } </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 flex flex-col bg-white dark:bg-gray-950 color-black dark:color-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> <EditorComponent :key="key" :editor="editor" @doc-change="handleDocChange" /> </div> </template>
import rehypeParse from 'rehype-parse' import rehypeRemark from 'rehype-remark' 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(remarkStringify) .processSync(html) .toString() } export function htmlFromMarkdown(markdown: string): string { return unified() .use(remarkParse) .use(remarkHtml) .processSync(markdown) .toString() }
ProseKit provides utility functions for converting between plain JSON object, HTML string, and ProseMirrorNode
.