The documentation is a Work in Progress and will be continuously improved and extended. We'd love to hear your feedback in our documentation repository.
Getting Started
We create our own Edtr.io-powered WYSIWYG editor during this tutorial. It is divided into several sections:
Prerequisites
We'll assume that you are familiar with React and the npm ecosystem. If you need to review your knowledge, we recommend the official React Tutorial.
Setup
First, we install Edtr.io and its peer dependencies:
# Using yarnyarn add @edtr-io/core@^2.0.0 react@^17.0.0 react-dom@^17.0.0 react-is@^17.0.0 react-dnd@^14.0.0 react-dnd-html5-backend@^14.0.0 redux@^4.0.0 styled-components@^5.0.0# OR Using npmnpm i --save @edtr-io/core@^2.0.0 react@^17.0.0 react-dom@^17.0.0 react-is@^17.0.0 react-dnd@^14.0.0 react-dnd-html5-backend@^14.0.0 redux@^4.0.0 styled-components@^5.0.0
Furthermore, we need at least one plugin. There are some "official" plugins provided by Edtr.io (though none of these are required). Let's start with the text plugin:
# Using yarnyarn add @edtr-io/plugin-text@^2.0.0 @fortawesome/fontawesome-svg-core@^1.0.0 @fortawesome/free-solid-svg-icons@^5.0.0 @fortawesome/react-fontawesome@^0.1.0 immutable@^4.0.0-0# OR Using npmnpm i --save @edtr-io/plugin-text@^2.0.0 @fortawesome/fontawesome-svg-core@^1.0.0 @fortawesome/free-solid-svg-icons@^5.0.0 @fortawesome/react-fontawesome@^0.1.0 immutable@^4.0.0-0
Creating the editor
We now have all the building blocks to create a basic Edtr.io editor:
import { Editor } from '@edtr-io/core'import { createTextPlugin } from '@edtr-io/plugin-text'import * as React from 'react'const textPlugin = createTextPlugin({placeholder: 'Hello world',registry: [],})const plugins = {text: textPlugin,}const initialState = { plugin: 'text' }export default function App() {return <Editor plugins={plugins} initialState={initialState} />}
Note
Depending on your bundler, you might get the error ReferenceError: regeneratorRuntime is not defined
. If so, add the line import 'regenerator-runtime/runtime'
to the top of the file.
At this point, we should have a glorified rich-text editor. Let's go over the things we did in detail:
Firstly, we initialized an Edtr.io plugin using the createTextPlugin
factory
provided by @edtr-io/plugin-text
. Like every "official" Edtr.io plugin, this
factory accepts a config specific to the plugin. Some options are required (e.g.
registry
), while others are optional (e.g. placeholder
).
Note
The factory pattern used by the "official" Edtr.io plugins is only a convention. Feel free to write your own plugins the way you like.
After that, we specified the plugins that our editor should know about. This is
a mapping of (arbitrary) keys to Edtr.io plugins. You can name the plugins how
you prefer (e.g. rich-text
instead of text
), and they will appear in the
serialized Edtr.io state.
The last thing we need is the state our editor should start with. The state
describes an Edtr.io document and will be used to initialize the store. A
document consists of the plugin used (i.e. with the same name as declared in
plugins
) and an optional state. If no state is provided (like in our example),
the plugin creates its own initial state (e.g. empty text in our case).
Lastly, we render the Editor
component provided by @edtr-io/core
with the
allowed plugins and the initial state.
Nesting Documents
The whole thing becomes much more interesting when we nest documents: Plugins may contain other Edtr.io documents and therefore create a tree of documents. The rows plugin provided by Edtr.io, for example, renders a list of documents.
First, we install the rows plugin
# Using yarnyarn add @edtr-io/plugin-rows@^2.0.0# OR using npmnpm i --save @edtr-io/plugin-rows@^2.0.0
and add it to our editor:
import { Editor } from '@edtr-io/core'import { createRowsPlugin } from '@edtr-io/plugin-rows'import { createTextPlugin } from '@edtr-io/plugin-text'import * as React from 'react'const registry = [{name: 'text',title: 'Text',description: 'A rich-text editor',},]const rowsPlugin = createRowsPlugin({content: { plugin: 'text' },plugins: registry,})const textPlugin = createTextPlugin({placeholder: 'Hello world',registry: [],})const plugins = {rows: rowsPlugin,text: textPlugin,}const initialState = { plugin: 'rows' }export default function App() {return <Editor plugins={plugins} initialState={initialState} />}
We created the rows plugin via createRowsPlugin
similarly to our text plugin.
The rows plugin requires content
which specifies which plugin should be used
by default, and plugins
which defines the plugins that may be added, including
some metadata (e.g. a human-readable title
). After that, we added the rows
plugin to our known plugins
, and replaced the initial state to start with the
rows plugin.
Note
The plugins
passed to the Editor
component are not necessarily the same as the plugins
passed to the rows plugin. The former describes any plugin the editor knows about and is therefore able to render. The latter may be a subset of that and defines the plugins that the user is actually able to add. For example, you might specify plugins that are only used internally or should only be used in a specific context. Furthermore, you can also override a plugin's configuration (e.g. a plugin may override the configuration of any child plugin it renders) and for example limit what users may do.
Persisting Documents
So far, we haven't talked about how you'd persist the documents created by the editor. The editor keeps track of its own state (and additional things like what document is currently focused, etc.), but does not actually persist the state. One way to tie the editor to your actual backend is to add a change listener:
import { Editor } from '@edtr-io/core'import { createRowsPlugin } from '@edtr-io/plugin-rows'import { createTextPlugin } from '@edtr-io/plugin-text'import * as React from 'react'const registry = [{name: 'text',title: 'Text',description: 'A rich-text editor',},]const rowsPlugin = createRowsPlugin({content: { plugin: 'text' },plugins: registry,})const textPlugin = createTextPlugin({placeholder: 'Hello world',registry: [],})const plugins = {rows: rowsPlugin,text: textPlugin,}const initialState = { plugin: 'rows' }export default function App() {return (<div style={{ padding: '20px 40px' }}><Editorplugins={plugins}initialState={initialState}onChange={({ changed, getDocument }) => {console.log('Changed', changed)console.log('Serialized state', getDocument())}}/></div>)}
The change listener receives an object containing changed
and getDocument
:
changed
is a boolean that tells you whether there are any pending changes (compared to the initial state you provided). You may use this to disable a save button, for example.getDocument
is a function that returns the serialized document (e.g. to persist in a database, file, local storage, etc.). Since this operation gets more expensive the longer the document is, you should decide yourself when you really need the serialized document (e.g. when the user actually presses a save button or by some time-based autosave).
Note
If your backend is only able to handle strings, you still need to stringify the JSON returned by the editor.
Note
A serialized document is easy to parse by design. For example, you could write your own custom renderer that is completely independent of Edtr.io (and possibly even from React). Or create a single Edtr.io document of multiple entities in your domain. The possibilities are endless.
Writing Plugins
Until now, we only used "official" Edtr.io plugins. While these plugins are often heavily customizable, it is often helpful to create domain-specific plugins that are tied to your own use case.
An Edtr.io plugin consists of the following parts:
- A React Component that defines how the plugin is rendered in the editor
- A description of the state (including how the state should be serialized if needed)
- A plugin configuration that allows to change the behavior of the plugin in different contexts
- Optionally, some overrides of the Edtr.io default behavior (e.g. disabling default Edtr.io hotkey bindings)
Example: a Counter Plugin
For educational purposes, let's define a simple plugin that represents a counter.
Firstly, we need to install another package intended for plugin developers:
# Using yarnyarn add @edtr-io/plugin# OR using npmnpm i --save @edtr-io/plugin
Then, we define the state of the plugin:
import { number } from '@edtr-io/plugin'const counterState = number(0)
Edtr.io has the concept of a so-called "state type". A state type encapsulates:
- The structure of the state used at runtime
- How the state is going to be serialized
- What the initial state should look like
- State type specific helpers intended to make development easier (e.g. handling file uploads)
The goals are similar to React hooks. And similarly to React hooks, Edtr.io already provides state types for the most typical use cases.
In our counter plugin, we can just use the number
state type that represents -
surprise - a number, and accepts the initial value (0 in our case).
Next, we define how the counter plugin is rendered:
function CounterEditor({ editable, focused, state }) {return (<React.Fragment>Current counter value:{editable ? (<buttononClick={() => {state.set((value) => value - 1)}}>-</button>) : null}{state.value}{editable ? (<buttononClick={() => {state.set((value) => value + 1)}}>+</button>) : null}</React.Fragment>)}
The component of an Edtr.io plugin receives - among other things - the following props:
editable
is a boolean that tells you whether your plugin is editable. You can use this to hide editor-only elements when the document is not editable.focused
is a boolean that tells you whether your plugin is currently focused. You may use this to hide some elements when the user is editing a different document at the moment (to get a better WYSIWYG feeling). There are also ways to check whether any of your child documents are focused (that isn't in the scope of this tutorial, though).state
contains the helpers defined by the state type used by your plugin. In our example,state.value
contains the current counter value andstate.set
can be used to update the state (similarly to React'ssetState
, you may also provide an update callback to make sure that you actually use the current state).
In our counter example, we render the current value (surrounded by two buttons to decrement or increment the state respectively, if the document is editable).
Tying these things together, we have an Edtr.io plugin definition:
const counterPlugin = {Component: CounterEditor,config: {}state: counterState}
Example: a Card Plugin
Let's explore a more complicated example: a card. A card consists of a title
and its content
(which should be just some nested Edtr.io document). When you
want to combine multiple values in your state, you can use the object
state
type. To render child documents, there is the child
state type:
import { child, string, object } from '@edtr-io/plugin'const cardState = object({title: string(''),content: child({ plugin: 'rows' }),})
State types are composable by design. For example, the object
state type
expects a mapping of arbitrary keys to state types (and exposes those as an
object to the plugin developer). The string
state type pretty much works the
same as the number
state type. The child
state type expects an object with
plugin
that describes the plugin that should be used by the child (as defined inplugins
),- optional
initialState
to override the initial state used by the plugin, - optional
config
to (deeply) override the plugin configuration. For example, we could override theplugins
of the rows plugin or override theplaceholder
used by the text plugin.
We can use the state type as follows:
function CardEditor({ editable, state }) {// `state.title` contains all the `string` state type helpersconsole.log(state.title.value)// `state.content` contains all the `child` state type helpers, e.g. `render`const children = state.content.render()return (<div><strong>{editable ? (<inputtype="text"onChange={(e) => {state.title.set(e.target.value)}}value={state.title.value}/>) : (state.title.value)}</strong>{children}</div>)}
Where to go from here
- Look at the official core plugins. You can check their source code to see how things work.
- Check the available state types.
- Explore the API Reference.