Skip to main content
The Editor class is the central orchestrator in ProseKit. It manages the editor state, view, and provides methods to interact with the editor.

Creating an Editor

Create an editor using the createEditor() function:
import { createEditor } from '@prosekit/core'

const editor = createEditor({
  extension: myExtension,
  defaultContent: '<p>Hello world!</p>',
  defaultSelection: 'end',
})

Editor Options

The EditorOptions interface (packages/core/src/editor/editor.ts):
export interface EditorOptions<E extends Extension> {
  /**
   * The extension to use when creating the editor.
   */
  extension: E

  /**
   * The starting document. Can be:
   * - ProseMirror node JSON object
   * - HTML string
   * - DOM Element instance
   */
  defaultContent?: NodeJSON | string | Element

  /**
   * A JSON object representing the starting selection.
   * Only used when defaultContent is also provided.
   */
  defaultSelection?: SelectionJSON
}
The defaultContent is processed through defineDefaultState() extension internally, which converts it to a ProseMirror document.

Editor Lifecycle

The editor has three main lifecycle states:
1
1. Created (Unmounted)
2
When first created, the editor exists but is not attached to the DOM:
3
const editor = createEditor({ extension })

console.log(editor.mounted) // false
console.log(editor.state)   // EditorState exists
4
In this state:
5
  • ✅ Editor state is available
  • ✅ Commands can be executed
  • ✅ Content can be get/set
  • ❌ View is not available
  • ❌ DOM interactions not possible
  • 6
    2. Mounted
    7
    Mount the editor to a DOM element:
    8
    const element = document.getElementById('editor')
    editor.mount(element)
    
    console.log(editor.mounted) // true
    console.log(editor.view)    // EditorView instance
    
    9
    In this state:
    10
  • ✅ Full editor functionality available
  • ✅ DOM element contains editable content
  • ✅ User can interact with the editor
  • ✅ View plugins are active
  • 11
    3. Unmounted
    12
    Unmount the editor from the DOM:
    13
    editor.unmount()
    
    console.log(editor.mounted) // false
    
    14
    In this state:
    15
  • ✅ State is preserved
  • ✅ Can be re-mounted
  • ❌ View is destroyed
  • ❌ DOM element cleared
  • Accessing editor.view when unmounted throws a ProseKitError. Always check editor.mounted first or handle the error.

    Editor Implementation

    ProseKit uses a two-class design pattern:

    EditorInstance (Internal)

    The internal EditorInstance class (packages/core/src/editor/editor.ts:85-338):
    export class EditorInstance {
      view: EditorView | null = null
      schema: Schema
      nodes: Record<string, NodeAction>
      marks: Record<string, MarkAction>
      commands: Record<string, CommandAction> = {}
    
      private tree: FacetNode
      private directEditorProps: DirectEditorProps
      private afterMounted: Array<VoidFunction> = []
    
      constructor(extension: Extension) {
        // Build facet tree
        this.tree = (extension as BaseExtension).getTree()
        
        // Extract configuration
        const payload = this.tree.getRootOutput()
        const schema = payload.schema
        const stateConfig = payload.state
        
        // Create initial state
        const state = EditorState.create(stateConfig)
        
        // Register commands
        for (const [name, commandCreator] of Object.entries(payload.commands)) {
          this.defineCommand(name, commandCreator)
        }
        
        // Create node and mark actions
        this.nodes = createNodeActions(schema, this.getState)
        this.marks = createMarkActions(schema, this.getState)
        
        // Store configuration
        this.schema = state.schema
        this.directEditorProps = { state, ...payload.view }
      }
    }
    
    Key responsibilities:
    • Manages the facet tree
    • Creates and updates EditorView
    • Handles dynamic extension registration
    • Provides internal state access

    Editor (Public API)

    The public Editor class wraps EditorInstance (packages/core/src/editor/editor.ts:343-517):
    export class Editor<E extends Extension = any> {
      private instance: EditorInstance
    
      constructor(instance: EditorInstance) {
        this.instance = instance
      }
      
      // Getters for state access
      get mounted(): boolean
      get view(): EditorView
      get schema(): Schema<ExtractNodeNames<E>, ExtractMarkNames<E>>
      get state(): EditorState
      get focused(): boolean
      
      // Lifecycle methods
      mount(place: HTMLElement | null | undefined): void | VoidFunction
      unmount(): void
      focus(): void
      blur(): void
      
      // Extension management
      use(extension: Extension): VoidFunction
      
      // State management
      updateState(state: EditorState): void
      setContent(content: ProseMirrorNode | NodeJSON | string | Element, selection?: SelectionJSON | Selection | 'start' | 'end'): void
      
      // Content access
      getDocJSON(): NodeJSON
      getDocHTML(options?: getDocHTMLOptions): string
      
      // Command execution
      exec(command: Command): boolean
      canExec(command: Command): boolean
      
      // Typed action access
      get commands(): ExtractCommandActions<E>
      get nodes(): ExtractNodeActions<E>
      get marks(): ExtractMarkActions<E>
    }
    
    The two-class design separates internal implementation from public API, making it easier to maintain type safety and prevent misuse.

    Working with the Editor

    Mounting and Unmounting

    // Mount and get cleanup function
    const unmount = editor.mount(element)
    
    // Later, unmount
    unmount()
    
    // Or use the method directly
    editor.unmount()
    
    // Re-mount to a different element
    editor.mount(anotherElement)
    
    Mounting an already-mounted editor to a different element throws an error. Unmount first, then mount to the new element.

    State Access

    // Get current state
    const state = editor.state
    
    // Access document
    const doc = state.doc
    
    // Access selection
    const selection = state.selection
    
    // Check if focused
    if (editor.focused) {
      console.log('Editor has focus')
    }
    

    Content Management

    // Get content as JSON
    const json = editor.getDocJSON()
    
    // Get content as HTML
    const html = editor.getDocHTML()
    
    // Set content from JSON
    editor.setContent({ type: 'doc', content: [...] })
    
    // Set content from HTML
    editor.setContent('<p>New content</p>', 'start')
    
    // Set content from DOM element
    const div = document.createElement('div')
    div.innerHTML = '<p>Content</p>'
    editor.setContent(div, 'end')
    

    Using Commands

    // Execute a command
    const success = editor.commands.toggleBold()
    
    // Check if command can be executed
    if (editor.commands.toggleBold.canExec()) {
      editor.commands.toggleBold()
    }
    
    // Execute a ProseMirror command directly
    import { undo } from '@prosekit/pm/history'
    
    if (editor.canExec(undo)) {
      editor.exec(undo)
    }
    

    Using Node and Mark Actions

    // Create nodes
    const para = editor.nodes.paragraph('Hello')
    const heading = editor.nodes.heading({ level: 1 }, 'Title')
    
    // Check if node is active
    if (editor.nodes.heading.isActive({ level: 1 })) {
      console.log('Heading 1 is active')
    }
    
    // Apply marks
    const boldText = editor.marks.bold('Bold text')
    
    // Check if mark is active
    if (editor.marks.bold.isActive()) {
      console.log('Bold is active')
    }
    

    Dynamic Extensions

    Register extensions dynamically after editor creation:
    // Register an extension
    const remove = editor.use(defineHighlight())
    
    // Later, remove the extension
    remove()
    

    How Dynamic Extensions Work

    When you call editor.use(extension) (packages/core/src/editor/editor.ts:233-254):
    1. Before Mount: Extension is queued for later registration
    2. After Mount: Extension is immediately added to the editor
      • Facet tree is updated using unionFacetNode()
      • Plugins are reconfigured if changed
      • Commands are registered if new
    public use(extension: Extension): VoidFunction {
      if (!this.mounted) {
        // Queue for after mount
        let canceled = false
        let lazyRemove: VoidFunction | null = null
    
        const lazyCreate = () => {
          if (!canceled) {
            lazyRemove = this.use(extension)
          }
        }
    
        this.afterMounted.push(lazyCreate)
    
        return () => {
          canceled = true
          lazyRemove?.()
        }
      }
    
      // Add extension immediately
      this.updateExtension(extension, true)
      return () => this.updateExtension(extension, false)
    }
    
    Dynamic extensions cannot change the schema. The schema is frozen at editor creation. Attempting to add nodes or marks dynamically will throw a ProseKitError.

    State Updates

    The editor’s state can be updated in two ways:
    // Commands internally dispatch transactions
    editor.commands.insertText('Hello')
    
    // Or dispatch manually
    const tr = editor.state.tr.insertText('Hello')
    editor.view.dispatch(tr)
    

    2. Direct State Update (Advanced)

    // Create a new state
    const newState = EditorState.create({
      doc: newDoc,
      plugins: editor.state.plugins,
    })
    
    // Update the editor
    editor.updateState(newState)
    
    Direct state updates bypass plugins and may break editor functionality. Only use this for specific advanced use cases.

    Type Safety

    The Editor class is generic over the extension type:
    const extension = union(
      defineDoc(),
      defineText(),
      defineParagraph(),
      defineBold(),
    )
    
    type MyExtension = typeof extension
    
    const editor = createEditor({ extension })
    // editor: Editor<MyExtension>
    
    // TypeScript knows about all available commands
    editor.commands.toggleBold() // ✅ Autocomplete works
    editor.commands.toggleItalic() // ❌ Type error
    
    // TypeScript knows about all node types
    editor.nodes.paragraph('text') // ✅
    editor.nodes.heading({ level: 1 }, 'text') // ❌ Type error (heading not defined)
    
    This enables:
    • Autocomplete: IDE suggests available commands, nodes, and marks
    • Type Checking: Invalid calls are caught at compile time
    • Refactoring: Safe renames and updates across codebase

    Best Practices

    1. Always check mounted before accessing view
      if (editor.mounted) {
        editor.view.focus()
      }
      
    2. Use commands instead of direct state manipulation
      // ✅ Good
      editor.commands.toggleBold()
      
      // ❌ Avoid
      const tr = editor.state.tr.addMark(...)
      editor.view.dispatch(tr)
      
    3. Clean up dynamic extensions
      const remove = editor.use(extension)
      // When done:
      remove()
      
    4. Preserve editor instances
      // ✅ Good: Create once, reuse
      const editor = createEditor({ extension })
      editor.mount(element1)
      editor.unmount()
      editor.mount(element2)
      
      // ❌ Avoid: Creating new editors unnecessarily
      const editor1 = createEditor({ extension })
      const editor2 = createEditor({ extension })
      

    Next Steps

    Extensions

    Learn how to create and compose extensions

    Commands

    Understand the command system