Executing Commands

PreviousNext

So far, the formatting examples have lived directly inside event handlers. That works for a tiny editor, but it gets repetitive once the same behavior is used from keyboard shortcuts, toolbar buttons, menu items, or tests.

A command is just reusable editor logic. Keep command reads in editor.read(...) and command writes in editor.update(...).

Extracting Commands

Start by moving the bold and code-block logic into plain functions:

import { ElementApi, type Editor } from '@platejs/slate'
 
const isBoldActive = (editor: Editor) => {
  return editor.read(state => state.marks.get()?.bold === true)
}
 
const isCodeBlockActive = (editor: Editor) => {
  return editor.read(state => {
    const match = state.nodes.find({
      match: node => ElementApi.isElement(node) && node.type === 'code',
    })
 
    return Boolean(match)
  })
}
 
const toggleBold = (editor: Editor) => {
  editor.update(tx => {
    tx.marks.toggle('bold')
  })
}
 
const toggleCodeBlock = (editor: Editor) => {
  const isActive = isCodeBlockActive(editor)
 
  editor.update(tx => {
    tx.nodes.set(
      { type: isActive ? 'paragraph' : 'code' },
      {
        match: node => ElementApi.isElement(node) && !tx.schema.isInline(node),
      }
    )
  })
}
import { ElementApi, type Editor } from '@platejs/slate'
 
const isBoldActive = (editor: Editor) => {
  return editor.read(state => state.marks.get()?.bold === true)
}
 
const isCodeBlockActive = (editor: Editor) => {
  return editor.read(state => {
    const match = state.nodes.find({
      match: node => ElementApi.isElement(node) && node.type === 'code',
    })
 
    return Boolean(match)
  })
}
 
const toggleBold = (editor: Editor) => {
  editor.update(tx => {
    tx.marks.toggle('bold')
  })
}
 
const toggleCodeBlock = (editor: Editor) => {
  const isActive = isCodeBlockActive(editor)
 
  editor.update(tx => {
    tx.nodes.set(
      { type: isActive ? 'paragraph' : 'code' },
      {
        match: node => ElementApi.isElement(node) && !tx.schema.isInline(node),
      }
    )
  })
}

These functions are not added to the editor object. They are normal JavaScript functions that receive an editor.

Using Commands From Editor Events

Use Editable onKeyDown for keyboard shortcuts that belong to one editor UI:

import { Editable, Slate, useSlateEditor } from '@platejs/slate-react'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        onKeyDown={(event, { editor }) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
            toggleCodeBlock(editor)
            return true
          }
 
          if (event.key === 'b' && event.ctrlKey) {
            event.preventDefault()
            toggleBold(editor)
            return true
          }
        }}
        renderElement={renderElement}
        renderLeaf={renderLeaf}
      />
    </Slate>
  )
}
import { Editable, Slate, useSlateEditor } from '@platejs/slate-react'
 
const App = () => {
  const editor = useSlateEditor({ initialValue })
 
  return (
    <Slate editor={editor}>
      <Editable
        onKeyDown={(event, { editor }) => {
          if (event.key === '`' && event.ctrlKey) {
            event.preventDefault()
            toggleCodeBlock(editor)
            return true
          }
 
          if (event.key === 'b' && event.ctrlKey) {
            event.preventDefault()
            toggleBold(editor)
            return true
          }
        }}
        renderElement={renderElement}
        renderLeaf={renderLeaf}
      />
    </Slate>
  )
}

Use extension transforms for behavior that maps to Slate transform names. That keeps model behavior available to keyboard input, native input, toolbar logic, programmatic calls, and tests.

import { defineEditorExtension, ElementApi, PointApi, RangeApi } from '@platejs/slate'
 
const markdownBlocks = defineEditorExtension({
  name: 'markdown-blocks',
  transforms: {
    deleteBackward({ editor, next, unit }) {
      const selection = editor.read(state => state.selection.get())
 
      if (
        unit === 'character' &&
        selection &&
        RangeApi.isCollapsed(selection)
      ) {
        const blockEntry = editor.read(state =>
          state.nodes.above({
            at: selection,
            match: node =>
              ElementApi.isElement(node) && state.nodes.isBlock(node),
          })
        )
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry
          const start = editor.read(state => state.points.start(blockPath))
 
          if (
            ElementApi.isElement(block) &&
            block.type !== 'paragraph' &&
            PointApi.equals(selection.anchor, start)
          ) {
            editor.update(tx => {
              tx.nodes.set(
                { type: 'paragraph' },
                {
                  at: blockPath,
                  match: node =>
                    ElementApi.isElement(node) && tx.nodes.isBlock(node),
                }
              )
            })
            return true
          }
        }
      }
 
      return next({ unit })
    },
    insertBreak({ editor, next }) {
      const selection = editor.read(state => state.selection.get())
 
      if (selection && RangeApi.isCollapsed(selection)) {
        const blockEntry = editor.read(state =>
          state.nodes.above({
            at: selection,
            match: node =>
              ElementApi.isElement(node) && state.nodes.isBlock(node),
          })
        )
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry
 
          if (ElementApi.isElement(block) && block.type === 'heading-one') {
            const start = editor.read(state => state.points.start(blockPath))
 
            if (PointApi.equals(selection.anchor, start)) {
              const result = next()
 
              editor.update(tx => {
                tx.nodes.set(
                  { type: 'paragraph' },
                  {
                    at: blockPath,
                    match: node =>
                      ElementApi.isElement(node) && tx.nodes.isBlock(node),
                  }
                )
              })
 
              return result
            }
          }
        }
      }
 
      return next()
    },
  },
})
 
const App = () => {
  const editor = useSlateEditor({
    extensions: [markdownBlocks],
    initialValue,
  })
 
  return (
    <Slate editor={editor}>
      <Editable renderElement={renderElement} renderLeaf={renderLeaf} />
    </Slate>
  )
}
import { defineEditorExtension, ElementApi, PointApi, RangeApi } from '@platejs/slate'
 
const markdownBlocks = defineEditorExtension({
  name: 'markdown-blocks',
  transforms: {
    deleteBackward({ editor, next, unit }) {
      const selection = editor.read(state => state.selection.get())
 
      if (
        unit === 'character' &&
        selection &&
        RangeApi.isCollapsed(selection)
      ) {
        const blockEntry = editor.read(state =>
          state.nodes.above({
            at: selection,
            match: node =>
              ElementApi.isElement(node) && state.nodes.isBlock(node),
          })
        )
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry
          const start = editor.read(state => state.points.start(blockPath))
 
          if (
            ElementApi.isElement(block) &&
            block.type !== 'paragraph' &&
            PointApi.equals(selection.anchor, start)
          ) {
            editor.update(tx => {
              tx.nodes.set(
                { type: 'paragraph' },
                {
                  at: blockPath,
                  match: node =>
                    ElementApi.isElement(node) && tx.nodes.isBlock(node),
                }
              )
            })
            return true
          }
        }
      }
 
      return next({ unit })
    },
    insertBreak({ editor, next }) {
      const selection = editor.read(state => state.selection.get())
 
      if (selection && RangeApi.isCollapsed(selection)) {
        const blockEntry = editor.read(state =>
          state.nodes.above({
            at: selection,
            match: node =>
              ElementApi.isElement(node) && state.nodes.isBlock(node),
          })
        )
 
        if (blockEntry) {
          const [block, blockPath] = blockEntry
 
          if (ElementApi.isElement(block) && block.type === 'heading-one') {
            const start = editor.read(state => state.points.start(blockPath))
 
            if (PointApi.equals(selection.anchor, start)) {
              const result = next()
 
              editor.update(tx => {
                tx.nodes.set(
                  { type: 'paragraph' },
                  {
                    at: blockPath,
                    match: node =>
                      ElementApi.isElement(node) && tx.nodes.isBlock(node),
                  }
                )
              })
 
              return result
            }
          }
        }
      }
 
      return next()
    },
  },
})
 
const App = () => {
  const editor = useSlateEditor({
    extensions: [markdownBlocks],
    initialValue,
  })
 
  return (
    <Slate editor={editor}>
      <Editable renderElement={renderElement} renderLeaf={renderLeaf} />
    </Slate>
  )
}

Using Commands From UI

The same functions can be called from toolbar buttons:

const Toolbar = ({ editor }) => {
  return (
    <div>
      <button
        onMouseDown={event => {
          event.preventDefault()
          toggleBold(editor)
        }}
      >
        Bold
      </button>
      <button
        onMouseDown={event => {
          event.preventDefault()
          toggleCodeBlock(editor)
        }}
      >
        Code Block
      </button>
    </div>
  )
}
const Toolbar = ({ editor }) => {
  return (
    <div>
      <button
        onMouseDown={event => {
          event.preventDefault()
          toggleBold(editor)
        }}
      >
        Bold
      </button>
      <button
        onMouseDown={event => {
          event.preventDefault()
          toggleCodeBlock(editor)
        }}
      >
        Code Block
      </button>
    </div>
  )
}

Extension Commands

Plain functions are enough for app code. Extensions can expose typed state and tx namespaces when a behavior needs to be shared across editors.

Raw Slate does not ship product commands like lists, headings, or links. Those belong in extensions or higher-level frameworks.