Skip to content

Composables ​

Blockslides provides Vue composables for creating and managing editor instances with full integration into Vue's reactivity system.

useSlideEditor ​

The primary composable for initializing a slide editor with the ExtensionKit bundle. This composable handles all the configuration needed for a fully-featured slide editor.

ts
import { useSlideEditor, EditorContent } from '@blockslides/vue-3-prebuilts'

const { editor, presets } = useSlideEditor({
  content: initialContent,
  onChange: (doc, editor) => {
    console.log('Content changed:', doc)
  }
})
html
<EditorContent v-if="editor" :editor="editor" />

Configuration ​

ts
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor, presets } = useSlideEditor({
  // Initial content (defaults to a single blank slide)
  content: {
    type: 'doc',
    content: [/* slides */]
  },
  
  // Called on every document update
  onChange: (doc, editor) => {
    saveToDatabase(doc)
  },
  
  // Additional extensions beyond ExtensionKit
  extensions: [MyCustomExtension],
  
  // Configure or disable ExtensionKit features
  extensionKitOptions: {
    bubbleMenu: false,
    youtube: { autoplay: false }
  },
  
  // Custom preset templates for add-slide button
  presetTemplates: customPresets,
  
  // Theme (applies CSS class)
  theme: 'light',
  
  // Lifecycle hooks
  onEditorReady: (editor) => {
    console.log('Editor initialized')
  },
  
  // Additional editor options
  editorOptions: {
    autofocus: true,
    editable: true
  }
})

Parameters:

  • content (JSONContent) - Initial document content. Omit to start with a default blank slide.
  • onChange ((doc: JSONContent, editor: Editor) => void) - Callback fired after each update with the current document JSON.
  • extensions (AnyExtension[]) - Additional extensions appended after ExtensionKit.
  • extensionKitOptions (ExtensionKitOptions) - Customize ExtensionKit configuration or disable specific extensions.
  • presetTemplates (PresetTemplates) - Custom template list for the add-slide button. Defaults to built-in presets.
  • theme ('light' | 'dark') - Theme name applied as CSS class to editor.
  • editorProps (EditorProps) - ProseMirror EditorProps for advanced configuration.
  • editorOptions (Partial<EditorOptions>) - Additional editor options to pass through to the core editor (e.g., autofocus, editable). Also used to pass lifecycle hooks like onCreate, onBeforeCreate, onDestroy, onFocus, onBlur, onSelectionUpdate, onTransaction.
  • onEditorReady ((editor: Editor) => void) - Called once when the editor instance is ready.
  • onUpdate ((props: EditorEvents['update']) => void) - Called on every editor update. Can be passed directly or via editorOptions.

Returns:

ts
{
  editor: ShallowRef<Editor | undefined>
  presets: PresetTemplates
}
  • editor (ShallowRef<Editor | undefined>) - The editor instance wrapped in a shallow ref. Will be undefined until mounted.
  • presets - Preset templates list used by the add-slide button.

Server-Side Rendering ​

For SSR frameworks like Nuxt.js, wrap the editor in ClientOnly:

html
  <ClientOnly>
    <EditorContent v-if="editor" :editor="editor" />
  </ClientOnly>

Or use lazy loading:

ts
import { defineAsyncComponent } from 'vue'

const SlideEditor = defineAsyncComponent(() => 
  import('@blockslides/vue-3-prebuilts').then(m => m.SlideEditor)
)
html
<SlideEditor :content="content" :onChange="handleChange" />

Managing Updates ​

The composable provides two ways to capture content changes:

onChange callback:

ts
const { editor } = useSlideEditor({
  onChange: (doc, editor) => {
    // Receives JSON on every update
    autosave(doc)
  }
})

Watching editor updates:

ts
import { watch } from 'vue'

const { editor } = useSlideEditor()

watch(editor, (editorInstance) => {
  if (!editorInstance) return
  
  editorInstance.on('update', () => {
    const json = editorInstance.getJSON()
    const html = editorInstance.getHTML()
    console.log('Updated:', json)
  })
}, { immediate: true })

Customizing ExtensionKit ​

ts
const { editor } = useSlideEditor({
  extensionKitOptions: {
    youtube: false,
    bubbleMenu: false,
    fileHandler: {
      onDrop: (editor, files, pos) => {
        handleFileUpload(files, pos)
      }
    }
  }
})

Full Configuration Options

See Extension Kit Overview for all available configuration options.

Passing Editor Options ​

Use the editorOptions parameter to pass additional editor configuration:

ts
const { editor } = useSlideEditor({
  editorOptions: {
    autofocus: 'end', // Focus at end of document
    editable: false,  // Read-only mode
    editorProps: {
      attributes: {
        spellcheck: 'false'
      }
    },
    // Lifecycle hooks
    onCreate: ({ editor }) => {
      console.log('Editor created:', editor)
    },
    onDestroy: () => {
      console.log('Editor destroyed')
    },
    onFocus: ({ editor, event }) => {
      console.log('Editor focused')
    },
    onBlur: ({ editor, event }) => {
      console.log('Editor blurred')
    }
  }
})

This is particularly useful for:

  • autofocus - Control initial focus behavior
  • editable - Toggle edit/read-only mode
  • editorProps - Pass custom attributes or handlers
  • injectCSS - Control CSS injection
  • Lifecycle hooks - onCreate, onBeforeCreate, onDestroy, onFocus, onBlur, onSelectionUpdate, onTransaction
  • Any other EditorOptions from @blockslides/core

Adding Custom Extensions ​

Extend the editor with your own extensions:

ts
import { MyCustomNode } from './extensions/MyCustomNode'

const { editor } = useSlideEditor({
  extensions: [
    MyCustomNode.configure({
      // custom options
    })
  ]
})

Extensions in the extensions array are appended after ExtensionKit, allowing your extensions to override or extend default behavior.

useEditor ​

Lower-level composable for creating an editor instance without ExtensionKit. Use this when building a custom editor from scratch.

ts
import { useEditor, EditorContent } from '@blockslides/vue-3'
import { Document } from '@blockslides/extension-document'
import { Paragraph } from '@blockslides/extension-paragraph'
import { Text } from '@blockslides/extension-text'

const editor = useEditor({
  extensions: [Document, Paragraph, Text],
  content: '<p>Hello world</p>',
  autofocus: true,
  editable: true
})
html
<EditorContent v-if="editor" :editor="editor" />

Configuration ​

ts
import { useEditor } from '@blockslides/vue-3'
import { Document } from '@blockslides/extension-document'
import { Paragraph } from '@blockslides/extension-paragraph'
import { Text } from '@blockslides/extension-text'
import { Bold } from '@blockslides/extension-bold'
import { Italic } from '@blockslides/extension-italic'

const editor = useEditor({
  // Required: Extensions array
  extensions: [Document, Paragraph, Text, Bold, Italic],
  
  // Initial content
  content: '<p>Hello</p>',
  
  // Editor configuration
  autofocus: true,
  editable: true,
  editorProps: {
    attributes: {
      class: 'prose'
    }
  },
  
  // Lifecycle callbacks
  onCreate: ({ editor }) => {
    console.log('Created')
  },
  onUpdate: ({ editor, transaction }) => {
    console.log('Updated')
  }
})

Parameters:

All parameters from EditorOptions are supported, including:

  • extensions (AnyExtension[], required) - Array of extensions to use
  • content (string | JSONContent) - Initial editor content
  • autofocus (boolean | 'start' | 'end' | number) - Focus the editor on mount
  • editable (boolean) - Whether the editor is editable
  • editorProps (EditorProps) - ProseMirror EditorProps for advanced configuration
  • onCreate, onUpdate, onDestroy, onFocus, onBlur, onSelectionUpdate, onTransaction - Lifecycle callbacks

Returns:

  • ShallowRef<Editor | undefined> - Editor instance wrapped in a shallow ref, or undefined during initial render.

useEditor vs useSlideEditor ​

FeatureuseEditoruseSlideEditor
ExtensionKit included❌ No✅ Yes
Add-slide button❌ Manual✅ Built-in
Preset templates❌ Manual✅ Built-in
Return valueEditor ref onlyEditor ref + presets
Use caseCustom editorsSlide presentations

Use useEditor when you need full control over the extension stack. Use useSlideEditor for slide presentation editors with sensible defaults.

Reactivity Patterns ​

Vue's reactivity system works seamlessly with Blockslides. The editor instance is returned as a shallowRef, which means Vue won't deeply watch internal ProseMirror state (for performance). Use computed() to derive reactive values from the editor.

Building Reactive Toolbar Buttons ​

ts
import { computed } from 'vue'
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor } = useSlideEditor()

// Derive reactive state
const isBold = computed(() => editor.value?.isActive('bold') ?? false)
const isItalic = computed(() => editor.value?.isActive('italic') ?? false)
const canUndo = computed(() => editor.value?.can().undo() ?? false)
const canRedo = computed(() => editor.value?.can().redo() ?? false)
html
<div class="toolbar">
  <button 
    @click="editor?.chain().focus().toggleBold().run()"
    :class="{ active: isBold }"
    :disabled="!editor"
  >
    Bold
  </button>
  
  <button 
    @click="editor?.chain().focus().toggleItalic().run()"
    :class="{ active: isItalic }"
    :disabled="!editor"
  >
    Italic
  </button>
  
  <button 
    @click="editor?.chain().focus().undo().run()"
    :disabled="!canUndo"
  >
    Undo
  </button>
  
  <button 
    @click="editor?.chain().focus().redo().run()"
    :disabled="!canRedo"
  >
    Redo
  </button>
</div>
css
.toolbar button.active {
  background-color: #0066ff;
  color: white;
}

Watching Editor State ​

Use watch to react to editor changes:

ts
import { ref, watch } from 'vue'
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor } = useSlideEditor()
const wordCount = ref(0)
const charCount = ref(0)

// Watch for editor updates
watch(editor, (editorInstance) => {
  if (!editorInstance) return
  
  // Set up update listener
  editorInstance.on('update', () => {
    const text = editorInstance.state.doc.textContent
    wordCount.value = text.split(/\s+/).filter(Boolean).length
    charCount.value = text.length
  })
  
  // Initial count
  const text = editorInstance.state.doc.textContent
  wordCount.value = text.split(/\s+/).filter(Boolean).length
  charCount.value = text.length
}, { immediate: true })
html
<div class="stats">
  <span>{{ wordCount }} words</span>
  <span>{{ charCount }} characters</span>
</div>

Computing Node Counts ​

ts
import { computed } from 'vue'
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor } = useSlideEditor()

const slideCount = computed(() => {
  if (!editor.value) return 0
  
  let count = 0
  editor.value.state.doc.descendants(node => {
    if (node.type.name === 'slide') count++
  })
  return count
})

const imageCount = computed(() => {
  if (!editor.value) return 0
  
  let count = 0
  editor.value.state.doc.descendants(node => {
    if (node.type.name === 'image') count++
  })
  return count
})
html
<div class="document-stats">
  <div>Slides: {{ slideCount }}</div>
  <div>Images: {{ imageCount }}</div>
</div>

Performance Optimization ​

For frequently updating UI that depends on editor state, use computed() to avoid unnecessary recalculations:

ts
import { computed } from 'vue'

const { editor } = useSlideEditor()

// Efficiently compute active states
const formattingState = computed(() => {
  if (!editor.value) {
    return {
      bold: false,
      italic: false,
      underline: false,
      strike: false
    }
  }
  
  return {
    bold: editor.value.isActive('bold'),
    italic: editor.value.isActive('italic'),
    underline: editor.value.isActive('underline'),
    strike: editor.value.isActive('strike')
  }
})
html
<div class="toolbar">
  <button :class="{ active: formattingState.bold }">Bold</button>
  <button :class="{ active: formattingState.italic }">Italic</button>
  <button :class="{ active: formattingState.underline }">Underline</button>
  <button :class="{ active: formattingState.strike }">Strike</button>
</div>

Reactive Content Binding ​

While you can't use v-model directly with the editor, you can create a two-way sync pattern:

ts
import { ref, watch } from 'vue'
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])

const { editor } = useSlideEditor({
  content: props.modelValue,
  onChange: (doc) => {
    emit('update:modelValue', doc)
  }
})

// Sync external changes to editor
watch(() => props.modelValue, (newContent) => {
  if (!editor.value) return
  
  const currentContent = editor.value.getJSON()
  if (JSON.stringify(currentContent) !== JSON.stringify(newContent)) {
    editor.value.commands.setContent(newContent, false)
  }
})

Accessing Editor in Lifecycle Hooks ​

ts
import { onMounted, onUnmounted } from 'vue'
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor } = useSlideEditor()

onMounted(() => {
  // Editor is available in onMounted
  if (editor.value) {
    editor.value.commands.focus()
  }
})

onUnmounted(() => {
  // Cleanup is handled automatically, but you can do additional cleanup here
  console.log('Editor component unmounting')
})

Custom Event Handlers ​

ts
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor } = useSlideEditor({
  // Direct callback props
  onChange: (doc, editor) => {
    console.log('Content changed:', doc)
  },
  
  onUpdate: ({ editor, transaction }) => {
    console.log('Editor updated:', editor)
    console.log('Transaction:', transaction)
  },
  
  onEditorReady: (editor) => {
    console.log('Editor ready:', editor)
  },
  
  // Other lifecycle hooks via editorOptions
  editorOptions: {
    onCreate: ({ editor }) => {
      console.log('Editor created:', editor)
    },
    
    onSelectionUpdate: ({ editor }) => {
      console.log('Selection changed:', editor.state.selection)
    },
    
    onFocus: ({ editor, event }) => {
      console.log('Editor focused')
    },
    
    onBlur: ({ editor, event }) => {
      console.log('Editor blurred')
    },
    
    onDestroy: () => {
      console.log('Editor destroyed')
    }
  }
})

Composing Multiple Editors ​

You can use multiple editor instances on the same page:

ts
import { EditorContent, useSlideEditor } from '@blockslides/vue-3-prebuilts'

const { editor: editor1 } = useSlideEditor({
  content: content1
})

const { editor: editor2 } = useSlideEditor({
  content: content2
})
html
<div class="multi-editor">
  <div class="editor-panel">
    <h3>Editor 1</h3>
    <EditorContent v-if="editor1" :editor="editor1" />
  </div>
  
  <div class="editor-panel">
    <h3>Editor 2</h3>
    <EditorContent v-if="editor2" :editor="editor2" />
  </div>
</div>

TypeScript Support ​

All composables are fully typed:

ts
import { useSlideEditor } from '@blockslides/vue-3-prebuilts'
import type { Editor, JSONContent } from '@blockslides/core'

const { editor, presets } = useSlideEditor({
  content: {
    type: 'doc',
    content: []
  } as JSONContent,
  
  onChange: (doc: JSONContent, editor: Editor) => {
    console.log('Changed:', doc)
  }
})

// editor has type: ShallowRef<Editor | undefined>
// presets has type: PresetTemplates