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: () => NodeJSON
Return 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: 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. : 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): void
Calls the given handler whenever the editor document changes. } 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 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) .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) ('my-document', 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 { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes. } 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 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) .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) ('my-document', 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 { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes. } 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 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) .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) ('my-document', 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 { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes. } 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 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) .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) ('my-document', 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 { useDocChange function useDocChange(handler: (doc: Node) => void, options?: UseExtensionOptions): void
Calls the given handler whenever the editor document changes. } 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 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) .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) ('my-document', 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. })
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 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) as NodeJSON)
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>
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) => string
Return 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): 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) ('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 | 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) ('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: 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. : 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 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>
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 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()
}
Conversion Utilities
Section titled “Conversion Utilities”ProseKit provides utility functions for converting between plain JSON object, HTML string, and ProseMirrorNode
.