Skip to content
Froala Documentation

Migrate from ProseMirror to Froala

This guide maps the current ProseMirror toolkit (the prosemirror-state, prosemirror-view, and prosemirror-model modules) to the Froala WYSIWYG Editor. You get side-by-side code for every step of the move: initialization, configuration, plugins, reading and writing content, events, methods, image upload, and React, so you can swap editors without relearning rich text from scratch.

What you need to know first

ProseMirror and Froala solve the same problem from opposite directions. ProseMirror is a toolkit: you define a Schema that lists every node and mark your document may contain, build an immutable EditorState, render it through an EditorView, and route every change through a Transaction. Nothing (not even a toolbar or the Enter key) exists until you wire it in. The document is a custom tree, not HTML, and you serialize to HTML only when you ask for it.

Froala inverts that model. You point new FroalaEditor() at an element, pass a plain options object, and a fully featured editor appears: toolbar, 45+ plugins, image handling, and clean HTML in and out. The document is HTML, so editor.html.get() and editor.html.set() are all you need to move content around. You trade ProseMirror's fine-grained control over the document model for speed of integration and a batteries-included feature set.

Licensing also differs. ProseMirror is open source under the MIT license and free to use. Froala is a commercial editor: you activate it with a license key option, which also unlocks support and updates. Plan to add your key during initialization.

Concept ProseMirror Froala
Initialization new EditorView(mount, { state }) new FroalaEditor('#editor', options)
Element targeting A mount DOM node passed to the view; ProseMirror renders the editable surface inside it CSS selector or DOM node: div, textarea, span, even an img
Enabling features Hand-built Schema plus a composed plugins array (history, keymap, menu) 45+ pluginsEnabled plugins
Toolbar Not built in, supplied by prosemirror-menu / prosemirror-example-setup toolbarButtons array of button groups
Reading content DOMSerializer on view.state.doc, or doc.toJSON() editor.html.get()
Writing content DOMParser.parse(), then dispatch a replace transaction editor.html.set(html)
Events dispatchTransaction prop and plugin props events: { contentChanged: … }
Teardown view.destroy() editor.destroy()
Licensing MIT, open source (free) Commercial license key (key option)

The one mental shift: stop thinking in terms of a schema, state, and transactions you assemble and maintain yourself. In Froala you point at an element, pass an options object, and read or write HTML directly. The editor owns the document model so you can focus on your application.

Swap ProseMirror for Froala in three steps

Remove the ProseMirror module stack, add the single Froala bundle, then initialize. Most apps complete the swap in well under an hour.

Step 1: Remove ProseMirror

ProseMirror is assembled from many small packages and almost always bundled. Delete the imports and the state/view setup, and drop the dependencies from package.json.

// Remove the module imports…
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
import {Schema, DOMParser, DOMSerializer} from "prosemirror-model"
import {schema} from "prosemirror-schema-basic"
import {exampleSetup} from "prosemirror-example-setup"

// …and drop the packages from package.json:
// prosemirror-state, prosemirror-view, prosemirror-model,
// prosemirror-schema-basic, prosemirror-schema-list,
// prosemirror-example-setup, prosemirror-commands,
// prosemirror-keymap, prosemirror-history

Step 2 — Add Froala

Froala ships as one packaged build. Use the .pkgd files from the jsDelivr CDN for a quick start, or install the npm package for a bundled app. The packaged build already includes the 45+ official plugins.

<link href="https://cdn.jsdelivr.net/npm/froala-editor@latest/css/froala_editor.pkgd.min.css" rel="stylesheet" />
<script src="https://cdn.jsdelivr.net/npm/froala-editor@latest/js/froala_editor.pkgd.min.js"></script>
# One package, all plugins bundled
npm install froala-editor

Step 3 — Initialize the editor

ProseMirror needs a schema, a state, and a view before anything is editable. Froala needs one constructor call. Whatever HTML already lives inside the target element becomes the editor's initial content, and you add your license key here.

import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
import {DOMParser} from "prosemirror-model"
import {schema} from "prosemirror-schema-basic"
import {exampleSetup} from "prosemirror-example-setup"

const mount = document.querySelector("#editor")
const view = new EditorView(mount, {
  state: EditorState.create({
    doc: DOMParser.fromSchema(schema).parse(mount),
    plugins: exampleSetup({schema})
  })
})
const editor = new FroalaEditor('#editor', {
  key: 'YOUR_LICENSE_KEY'
});

// Froala accepts a CSS selector or a DOM node, and works on
// a div, textarea, span, or even an img element.
const onNode = new FroalaEditor(document.getElementById('editor'));

Configuration and options

In ProseMirror, behavior is configured by composing a plugins array and a custom Schema, plus view-level props. In Froala, behavior is a flat options object passed to the constructor. The most common settings map cleanly.

// Placeholder, height, and toolbar are all opt-in pieces.
import {placeholder} from "./placeholder-plugin" // custom

const state = EditorState.create({
  schema,
  plugins: [
    ...exampleSetup({schema, menuBar: true}),
    placeholder("Start typing…")
  ]
})

const view = new EditorView(mount, {
  state,
  attributes: { style: "min-height: 200px" }
})
const editor = new FroalaEditor('#editor', {
  key: 'YOUR_LICENSE_KEY',
  placeholderText: 'Start typing…',
  heightMin: 200,
  charCounterCount: true,
  toolbarButtons: [
    ['bold', 'italic', 'underline'],
    ['formatOL', 'formatUL'],
    ['insertLink', 'insertImage'],
    ['undo', 'redo']
  ]
});

Browse the full list of settings in the Froala options reference.

Plugins

ProseMirror plugins are JavaScript objects you instantiate and add to the state's plugins array. Order matters, because props are resolved in sequence. Froala bundles 45+ plugins in the packaged build; you control which are active with pluginsEnabled, or use the inverse pluginsDisabled to keep everything except a few.

import {history, undo, redo} from "prosemirror-history"
import {keymap} from "prosemirror-keymap"
import {baseKeymap} from "prosemirror-commands"

const state = EditorState.create({
  schema,
  plugins: [
    history(),
    keymap({"Mod-z": undo, "Mod-y": redo}),
    keymap(baseKeymap)
  ]
})
// Turn ON only the plugins you want…
new FroalaEditor('#editor', {
  pluginsEnabled: ['align', 'lists', 'link', 'image', 'table']
});

// …or keep everything EXCEPT a few (the inverse).
new FroalaEditor('#editor', {
  pluginsDisabled: ['video', 'emoticons']
});

Getting and setting content

This is where the two models feel most different. ProseMirror stores a custom document tree, so reading HTML means serializing the doc, and writing HTML means parsing it back and dispatching a replace transaction. Froala stores HTML, so get and set are one-liners.

Reading content

import {DOMSerializer} from "prosemirror-model"

// Serialize the document fragment to an HTML string.
function getHTML(view) {
  const serializer = DOMSerializer.fromSchema(view.state.schema)
  const fragment = serializer.serializeFragment(view.state.doc.content)
  const div = document.createElement("div")
  div.appendChild(fragment)
  return div.innerHTML
}

// Or keep the native JSON representation:
const json = view.state.doc.toJSON()
const html = editor.html.get();

Writing content

import {DOMParser} from "prosemirror-model"

// Parse an HTML string and replace the whole document.
function setHTML(view, html) {
  const tmp = document.createElement("div")
  tmp.innerHTML = html
  const doc = DOMParser.fromSchema(view.state.schema).parse(tmp)
  const tr = view.state.tr.replaceWith(0, view.state.doc.content.size, doc.content)
  view.dispatch(tr)
}
editor.html.set('<p>Hello <strong>Froala</strong>!</p>');

Events

ProseMirror surfaces changes through the dispatchTransaction prop: you intercept every transaction, apply it, and react. Froala exposes named events you register in the events option (or later with editor.events.on()). The closest Froala equivalent of "the document changed" is contentChanged.

const view = new EditorView(mount, {
  state,
  dispatchTransaction(tr) {
    const next = view.state.apply(tr)
    view.updateState(next)
    if (tr.docChanged) {
      console.log("Document changed")
    }
  }
})
new FroalaEditor('#editor', {
  events: {
    initialized: function () {
      console.log('Editor ready');
    },
    contentChanged: function () {
      console.log('Document changed', this.html.get());
    }
  }
});

See every hook in the Froala events reference.

Methods

Common imperative actions (read-only mode, focus, and teardown) exist in both editors. ProseMirror expresses read-only through the editable prop, which you update with setProps; Froala has dedicated methods.

// Read-only: toggle the editable prop.
view.setProps({ editable: () => false })
view.setProps({ editable: () => true })

// Focus the editing surface.
view.focus()

// Tear the editor down.
view.destroy()
// Read-only: turn editing off and on.
editor.edit.off();
editor.edit.on();

// Focus the editing surface.
editor.events.focus();

// Tear the editor down.
editor.destroy();

The complete list lives in the Froala methods reference.

Image upload

ProseMirror has no built-in upload pipeline. You add an image node to your schema, intercept paste or drop events, upload the file yourself, then dispatch a transaction that inserts the node with the returned URL: all custom code. Froala ships an image plugin: set imageUploadURL (and any params), and the editor handles file selection, upload, and insertion, firing events you can hook into.

// You wire upload + insertion yourself, e.g. on paste.
const view = new EditorView(mount, {
  state,
  props: {
    handlePaste(view, event) {
      const file = event.clipboardData?.files?.[0]
      if (!file) return false
      uploadFile(file).then(url => {
        const node = view.state.schema.nodes.image.create({src: url})
        view.dispatch(view.state.tr.replaceSelectionWith(node))
      })
      return true
    }
  }
})

async function uploadFile(file) {
  const body = new FormData()
  body.append("file", file)
  const res = await fetch("/upload", {method: "POST", body})
  return (await res.json()).link
}
new FroalaEditor('#editor', {
  imageUploadURL: '/upload',
  imageUploadMethod: 'POST',
  imageUploadParams: { folder: 'posts' },
  events: {
    'image.uploaded': function (response) {
      console.log('Uploaded', response);
    },
    'image.error': function (error, response) {
      console.error('Upload failed', error);
    }
  }
});

Froala expects the upload endpoint to return JSON containing a link field with the stored image URL.

Framework integration: React

ProseMirror has no official React component, so the standard pattern is to create the EditorView inside a useEffect and destroy it on cleanup, holding the view in a ref. Froala provides the react-froala-wysiwyg package: a controlled component bound with model and onModelChange.

import {useEffect, useRef} from "react"
import {EditorState} from "prosemirror-state"
import {EditorView} from "prosemirror-view"
import {schema} from "prosemirror-schema-basic"
import {exampleSetup} from "prosemirror-example-setup"

function Editor() {
  const ref = useRef(null)
  useEffect(() => {
    const view = new EditorView(ref.current, {
      state: EditorState.create({
        schema, plugins: exampleSetup({schema})
      })
    })
    return () => view.destroy()
  }, [])
  return <div ref={ref} />
}
import {useState} from "react"
import FroalaEditor from "react-froala-wysiwyg"
import "froala-editor/css/froala_style.min.css"
import "froala-editor/css/froala_editor.pkgd.min.css"
import "froala-editor/js/plugins.pkgd.min.js"

function Editor() {
  const [content, setContent] = useState("")
  return (
    <FroalaEditor
      tag="textarea"
      model={content}
      onModelChange={setContent}
      config={{ key: "YOUR_LICENSE_KEY" }}
    />
  )
}

Froala also ships first-party SDKs for Angular and Vue.

You're ready to ship

You have mapped every core ProseMirror concept to its Froala equivalent. Add your license key, pick your toolbar and plugins, and point the editor at your content. Explore the references below to fine-tune the rest.

Do you think we can improve this article? Let us know.

Ready to dive in? Explore our plans