#### Is this adding or improving a _feature_ or fixing a _bug_? Improvement / debt. #### What's the new behavior? This pull request removes the `Change` object as we know it, and folds all of its behaviors into the new `Editor` controller instead, simplifying a lot of the confusion around what is a "change vs. editor" and when to use which. It makes the standard API a **lot** nicer to use I think. --- ###### NEW **The `editor.command` and `editor.query` methods can take functions.** Previously they only accepted a `type` string and would look up the command or query by type. Now, they also accept a custom function. This is helpful for plugin authors, who want to accept a "command option", since it gives users more flexibility to write one-off commands or queries. For example a plugin could be passed either: ```js Hotkey({ hotkey: 'cmd+b', command: 'addBoldMark', }) ``` Or a custom command function: ```js Hotkey({ hotkey: 'cmd+b', command: editor => editor.addBoldMark().moveToEnd() }) ``` ###### BREAKING **The `Change` object has been removed.** The `Change` object as we know it previously has been removed, and all of its behaviors have been folded into the `Editor` controller. This includes the top-level commands and queries methods, as well as methods like `applyOperation` and `normalize`. _All places that used to receive `change` now receive `editor`, which is API equivalent._ **Changes are now flushed to `onChange` asynchronously.** Previously this was done synchronously, which resulted in some strange race conditions in React environments. Now they will always be flushed asynchronously, just like `setState`. **The `render*` and `decorate*` middleware signatures have changed!** Previously the `render*` and `decorate*` middleware was passed `(props, next)`. However now, for consistency with the other middleware they are all passed `(props, editor, next)`. This way, all middleware always receive `editor` and `next` as their final two arguments. **The `normalize*` and `validate*` middleware signatures have changed!** Previously the `normalize*` and `validate*` middleware was passed `(node, next)`. However now, for consistency with the other middleware they are all passed `(node, editor, next)`. This way, all middleware always receive `editor` and `next` as their final two arguments. **The `editor.event` method has been removed.** Previously this is what you'd use when writing tests to simulate events being fired—which were slightly different to other running other middleware. With the simplification to the editor and to the newly-consistent middleware signatures, you can now use `editor.run` directly to simulate events: ```js editor.run('onKeyDown', { key: 'Tab', ... }) ``` ###### DEPRECATED **The `editor.change` method is deprecated.** With the removal of the `Change` object, there's no need anymore to create the small closures with `editor.change()`. Instead you can directly invoke commands on the editor in series, and all of the changes will be emitted asynchronously on the next tick. ```js editor .insertText('word') .moveFocusForward(10) .addMark('bold') ``` **The `applyOperations` method is deprecated.** Instead you can loop a set of operations and apply each one using `applyOperation`. This is to reduce the number of methods exposed on the `Editor` to keep it simpler. **The `change.call` method is deprecated.** Previously this was used to call a one-off function as a change method. Now this behavior is equivalent to calling `editor.command(fn)` instead. --- Fixes: https://github.com/ianstormtaylor/slate/issues/2334 Fixes: https://github.com/ianstormtaylor/slate/issues/2282
8.7 KiB
Previous:
Saving to a Database
Saving and Loading HTML Content
In the previous guide, we looked at how to serialize the Slate editor's content and save it for later. What if you want to save the content as HTML? It's a slightly more involved process, but this guide will show you how to do it.
Let's start with a basic editor:
import { Editor } from 'slate-react'
import Plain from 'slate-plain-serializer'
class App extends React.Component {
state = {
value: Plain.deserialize(''),
}
onChange = ({ value }) => {
this.setState({ value })
}
render() {
return <Editor value={this.state.value} onChange={this.onChange} />
}
}
That will render a basic Slate editor on your page.
Now... we need to add the Html
serializer. And to do that, we need to tell it a bit about the schema we plan on using. For this example, we'll work with a schema that has a few different parts:
- A
paragraph
block. - A
code
block for code samples. - A
quote
block for quotes... - And
bold
,italic
andunderline
formatting.
By default, the Html
serializer knows nothing about our schema, just like Slate itself. To fix this, we need to pass it a set of rules
. Each rule defines how to serialize and deserialize a Slate object.
To start, let's create a new rule with a deserialize
function for paragraph blocks.
const rules = [
// Add our first rule with a deserializing function.
{
deserialize(el, next) {
if (el.tagName.toLowerCase() == 'p') {
return {
object: 'block',
type: 'paragraph',
data: {
className: el.getAttribute('class'),
},
nodes: next(el.childNodes),
}
}
},
},
]
The el
argument that the deserialize
function receives is just a DOM element. And the next
argument is a function that will deserialize any element(s) we pass it, which is how you recurse through each node's children.
Okay, that's deserialize
, now let's define the serialize
property of the paragraph rule as well:
const rules = [
{
deserialize(el, next) {
if (el.tagName.toLowerCase() == 'p') {
return {
object: 'block',
type: 'paragraph',
data: {
className: el.getAttribute('class'),
},
nodes: next(el.childNodes),
}
}
},
// Add a serializing function property to our rule...
serialize(obj, children) {
if (obj.object == 'block' && obj.type == 'paragraph') {
return <p className={obj.data.get('className')}>{children}</p>
}
},
},
]
The serialize
function should also feel familiar. It's just taking Slate models and turning them into React elements, which will then be rendered to an HTML string.
The object
argument of the serialize
function will either be a Node
, a Mark
or a special immutable String
object. And the children
argument is a React element describing the nested children of the object in question, for recursing.
Okay, so now our serializer can handle paragraph
nodes.
Let's add the other types of blocks we want:
// Refactor block tags into a dictionary for cleanliness.
const BLOCK_TAGS = {
p: 'paragraph',
blockquote: 'quote',
pre: 'code',
}
const rules = [
{
// Switch deserialize to handle more blocks...
deserialize(el, next) {
const type = BLOCK_TAGS[el.tagName.toLowerCase()]
if (type) {
return {
object: 'block',
type: type,
data: {
className: el.getAttribute('class'),
},
nodes: next(el.childNodes),
}
}
},
// Switch serialize to handle more blocks...
serialize(obj, children) {
if (obj.object == 'block') {
switch (obj.type) {
case 'paragraph':
return <p className={obj.data.get('className')}>{children}</p>
case 'quote':
return <blockquote>{children}</blockquote>
case 'code':
return (
<pre>
<code>{children}</code>
</pre>
)
}
}
},
},
]
Now each of our block types is handled.
You'll notice that even though code blocks are nested in a <pre>
and a <code>
element, we don't need to specifically handle that case in our deserialize
function, because the Html
serializer will automatically recurse through el.childNodes
if no matching deserializer is found. This way, unknown tags will just be skipped over in the tree, instead of their contents omitted completely.
Okay. So now our serializer can handle blocks, but we need to add our marks to it as well. Let's do that with a new rule...
const BLOCK_TAGS = {
blockquote: 'quote',
p: 'paragraph',
pre: 'code',
}
// Add a dictionary of mark tags.
const MARK_TAGS = {
em: 'italic',
strong: 'bold',
u: 'underline',
}
const rules = [
{
deserialize(el, next) {
const type = BLOCK_TAGS[el.tagName.toLowerCase()]
if (type) {
return {
object: 'block',
type: type,
data: {
className: el.getAttribute('class'),
},
nodes: next(el.childNodes),
}
}
},
serialize(obj, children) {
if (obj.object == 'block') {
switch (obj.type) {
case 'code':
return (
<pre>
<code>{children}</code>
</pre>
)
case 'paragraph':
return <p className={obj.data.get('className')}>{children}</p>
case 'quote':
return <blockquote>{children}</blockquote>
}
}
},
},
// Add a new rule that handles marks...
{
deserialize(el, next) {
const type = MARK_TAGS[el.tagName.toLowerCase()]
if (type) {
return {
object: 'mark',
type: type,
nodes: next(el.childNodes),
}
}
},
serialize(obj, children) {
if (obj.object == 'mark') {
switch (obj.type) {
case 'bold':
return <strong>{children}</strong>
case 'italic':
return <em>{children}</em>
case 'underline':
return <u>{children}</u>
}
}
},
},
]
Great, that's all of the rules we need! Now let's create a new Html
serializer and pass in those rules:
import Html from 'slate-html-serializer'
// Create a new serializer instance with our `rules` from above.
const html = new Html({ rules })
And finally, now that we have our serializer initialized, we can update our app to use it to save and load content, like so:
// Load the initial value from Local Storage or a default.
const initialValue = localStorage.getItem('content') || '<p></p>'
class App extends React.Component {
state = {
value: html.deserialize(initialValue),
}
onChange = ({ value }) => {
// When the document changes, save the serialized HTML to Local Storage.
if (value.document != this.state.value.document) {
const string = html.serialize(value)
localStorage.setItem('content', string)
}
this.setState({ value })
}
render() {
return (
<Editor
value={this.state.value}
onChange={this.onChange}
// Add the ability to render our nodes and marks...
renderNode={this.renderNode}
renderMark={this.renderMark}
/>
)
}
renderNode = (props, editor, next) => {
switch (props.node.type) {
case 'code':
return (
<pre {...props.attributes}>
<code>{props.children}</code>
</pre>
)
case 'paragraph':
return (
<p {...props.attributes} className={node.data.get('className')}>
{props.children}
</p>
)
case 'quote':
return <blockquote {...props.attributes}>{props.children}</blockquote>
default:
return next()
}
}
// Add a `renderMark` method to render marks.
renderMark = (props, editor, next) => {
const { mark, attributes } = props
switch (mark.type) {
case 'bold':
return <strong {...attributes}>{props.children}</strong>
case 'italic':
return <em {...attributes}>{props.children}</em>
case 'underline':
return <u {...attributes}>{props.children}</u>
default:
return next()
}
}
}
And that's it! When you make any changes in your editor, you should see the updated HTML being saved to Local Storage. And when you refresh the page, those changes should be carried over.