Changelog
This is a list of changes to Slate with each new release. Until
1.0
is released, breaking changes will be added as minor version bumps, and smaller, patch-level changes won't be noted since the library is moving quickly while in beta.⚠️ Until https://github.com/atlassian/changesets/issues/264 is solved, each package will maintain its own individual changelog, which you can find here:
New CustomTypes for Editor, Element, Text and other implementation specific objects. Improved typing with TypeScript lets you add CustomTypes for the Slate Editor. This change requires you to set up your types at the start. It's a new concept so please read the new TypeScript documentation here: https://docs.slatejs.org/concepts/11-typescript
Introduced new customizable TypeScript typings. You can override the built-in types to extend them for your own editor's domain model. However the changes to make this possible likely resulted in some changes to the existing type contracts.
The
useEditor
hook was renamed to useSlateStatic
. This was done to better differentiate between the useSlate
hook and to make it clear that the static version will not re-render when changes occur.There were no breaking changes or new additions in this release.
User properties on Elements and Texts now have an unknown type instead of any. Previously, the arbitrary user defined keys on the
Text
and Element
interface had a type of any
which effectively removed any potential type checking on those properties. Now these have a type of unknown
so that type checking can be done by consumers of the API when they are applying their own custom properties to the Text
s and Element
s.Overridable commands now live directly on the editor object. Previously the
Command
concept was implemented as an interface that was passed into the editor.exec
function, allowing the "core" commands to be overridden in one place. But this introduced a lot of Redux-like indirection when implementing custom commands that wasn't necessary because they are never overridden. Instead, now the core actions that can be overridden are implemented as individual functions on the editor (eg. editor.insertText
) and they can be overridden just like any other function (eg. isVoid
).Previously to override a command you'd do:
const withPlugin = editor => {
const { exec } = editor
editor.exec = command => {
if (command.type === 'insert_text') {
const { text } = command
if (myCustomLogic) {
// ...
return
}
}
exec(command)
}
return editor
}
Now, you'd override the specific function directly:
const withPlugin = editor => {
const { insertText } = editor
editor.insertText = text => {
if (myCustomLogic) {
// ...
return
}
insertText(text)
}
return editor
}
You shouldn't ever need to call these functions directly! They are there for plugins to tap into, but there are higher level helpers for you to call whenever you actually need to invoke them. Read on…
Transforms now live in a separate namespace of helpers. Previously the document and selection transformation helpers were available directly on the
Editor
interface as Editor.*
. But these helpers are fairly low level, and not something that you'd use in your own codebase all over the place, usually only inside specific custom helpers of your own. To make room for custom userland commands, these helpers have been moved to a new Transforms
namespace.Previously you'd write:
Editor.unwrapNodes(editor, ...)
Now you'd write:
Transforms.unwrapNodes(editor, ...)
The
Command
interfaces were removed. As part of those changes, the existing Command
, CoreCommand
, HistoryCommand
, and ReactCommand
interfaces were all removed. You no longer need to define these "command objects", because you can just call the functions directly. Plugins can still define their own overridable commands by extending the Editor
interface with new functions. The slate-react
plugin does this with insertData
and the slate-history
plugin does this with undo
and redo
.User action helpers now live directly on the
Editor.*
interface. These are taking the place of the existing Transforms.*
helpers that were moved. These helpers are equivalent to user actions, and they always operate on the existing selection. There are some defined by core, but you are likely to define your own custom helpers that are specific to your domain as well.For example, here are some of the built-in actions:
Editor.insertText(editor, 'a string of text')
Editor.deleteForward(editor)
Editor.deleteBackward(editor, { unit: 'word' })
Editor.addMark(editor, 'bold', true)
Editor.insertBreak(editor)
...
Every one of the old "core commands" has an equivalent
Editor.*
helper exposed now. However, you can easily define your own custom helpers and place them in a namespace as well:const MyEditor = {
...Editor,
insertParagraph(editor) { ... },
toggleBoldMark(editor) { ... },
formatLink(editor, url) { ... },
...
}
Whatever makes sense for your specific use case!
The
format_text
command is split into add_mark
and remove_mark
. Although the goal is to keep the number of commands in core to a minimum, having this as a combined command made it very hard to write logic that wanted to guarantee to only ever add or remove a mark from a text node. Now you can be guaranteed that the add_mark
command will only ever add custom properties to text nodes, and the remove_mark
command will only ever remove them.Previously you would write:
editor.exec({
type: 'format_text',
properties: { bold: true },
})
Now you would write:
if (isActive) {
editor.exec({ type: 'remove_mark', key: 'bold' })
} else {
editor.exec({ type: 'add_mark', key: 'bold', value: true })
}
🤖 Note that the "mark" term does not mean what it meant in0.47
and earlier. It simply means formatting that is applied at the text level—bold, italic, etc. We need a term for it because it's such a common pattern in richtext editors, and "mark" is often the term that is used. For example the<mark>
tag in HTML.
The
Node.text
helper was renamed to Node.string
. This was simply to reduce the confusion between "the text string" and "text nodes". The helper still just returns the concatenated string content of a node.The
match
option must now be a function. Previously there were a few shorthands, like passing in a plain object. This behavior was removed because it made it harder to reason about exactly what was being matched, it made debugging harder, and it made it hard to type well. Now the match
option must be a function that receives the Node
object to match. If you're using TypeScript, and the function you pass in is a type guard, that will be taken into account in the return value!Previously you might write:
Editor.nodes(editor, {
at: range,
match: 'text',
})
Editor.nodes(editor, {
at: range,
match: { type: 'paragraph' },
})
Now you'd write:
Editor.nodes(editor, {
at: range,
match: Text.isText,
})
Editor.nodes(editor, {
at: range,
match: node => node.type === 'paragraph',
})
The
mode
option now defaults to 'lowest'
. Previously the default varied depending on where in the codebase it was used. Now it defaults to 'lowest'
everywhere, and you can always pass in 'highest'
to change the behavior. The one exception is the Editor.nodes
helper which defaults to 'all'
since that's the expected behavior most of the time.The
Editor.match
helper was renamed to Editor.above
. This was just to make it clear how it searched in the tree—it looks through all of the nodes directly above a location in the document.The
Editor.above/previous/next
helpers now take all options in a dictionary. Previously their APIs did not exactly match the Editor.nodes
helper which they are shorthand for, but now this is no longer the case. The at
, match
and mode
options are all passed in the options
argument.Previously you would use:
Editor.previous(editor, path, n => Text.isText(n), {
mode: 'lowest',
})
Now you'd use:
Editor.previous(editor, {
at: path,
match: n => Text.isText(n),
mode: 'lowest',
...
})
The
Editor.elements
and Editor.texts
helpers were removed. These were simple convenience helpers that were rarely used. You can now achieve the same thing by using the Editor.nodes
helper directly along with the match
option. For example:Editor.nodes(editor, {
at: range,
match: Element.isElement,
})
The
<Slate>
onChange
handler no longer receives the selection
argument. Previously it received (value, selection)
, now it receives simply (value)
. Instead, you can access any property of the editor directly (including the value as editor.children
). The value/onChange
convention is provided purely for form-related use cases that expect it. This is along with the change to how extra props are "controlled". By default they are uncontrolled, but you can pass in any of the other top-level editor properties to take control of them.The
Command
and CoreCommand
interfaces have been split apart. Previously you could access Command.isCoreCommand
, however now this helper lives directly on the core command interface as CoreCommand.isCoreCommand
. This makes it more symmetrical with userland commands.Command checkers have been simplified. Previously Slate exposed command-checking helpers like
Command.isInsertTextCommand
. However these were verbose and not useful most of the time. Instead, you can now check for CoreCommand.isCoreCommand
and then use the command.type
property to narrow further. This keeps core more symmetrical with how userland will implement custom commands.The
<Slate>
component is now pseudo-controlled. It requires a value=
prop to be passed in which is controlled. However, the selection
, marks
, history
, or any other props are not required to be controlled. They default to being uncontrolled. If your use case requires controlling these extra props you can pass them in and they will start being controlled again. This change was made to make using Slate easier, while still allowing for more complex state to be controlled by core or plugins going forward—state that users don't need to concern themselves with most of time.The
Editor
now has a marks
property. This property represents text-level formatting that will be applied to the next character that is inserted. This is a common richtext editor behavior, where pressing a Bold button with a collapsed selection turns on "bold" formatting mode, and then typing a character becomes bold. This state isn't stored in the document, and is instead stored as an extra property on the editor itself.The
slate-schema
package has been removed! This decision was made because with the new helpers on the Editor.*
interface, and with the changes to normalizeNode
in the latest version of Slate, adding constraints using normalizeNode
actually leads to more maintainable code than using slate-schema
. Previously it was required to keep things from getting too unreadable, but that always came at a large cost of indirection and learning additional APIs. Everything you could do with slate-schema
you can do with normalizeNode
, and more.Node matching functions now receive just a
Node
. Previously they received a NodeEntry
tuple, which consisted of [node, path]
. However now they receive only a node
argument, which makes it easier to write one-off node-checking helpers and pass them in directly as arguments. If you need to ensure a path, lookup the node first.A few unnecessary helpers were removed. There were a handful of leftovers helpers that were not used anywhere in Slate's core logic, and were very unlikely to be used in userland, so they've been removed to reduce bundle size. You are always free to re-implement them if you truly need them. The list of helpers removed is:
Editor.ancestor
Node.closest
Node.furthest
Range.exists
Range.isRangeList
Range.isRangeMap
The
slate-schema
package now exports a factory. Previously you imported the withSchema
function directly from the package, and passed in your schema rules when you called it. However, now you import the defineSchema
factory instead which takes your schema rules and returns a custom withSchema
plugin function. This way you can still use helpers like compose
with the plugin, while pre-defining your custom rules.The
properties
validation in the schema is now exhaustive. Previously a properties
validation would check any properties you defined, and leave any unknown ones as is. This made it hard to be certain about which properties would end up on a node. Now any non-defined properties are considered invalid. And using an empty {}
validation would ensure that there are no custom properties at all.The
leaves
schema validation ensures text-level formatting. You can use it from any higher up element node in the tree, to guarantee that it only contains certain types of text-level formatting on its inner text nodes. For example you could use it to ensure that a code
block doesn't allow any of its text to be bolded or italicized.The
Mark
interface has been removed! Previously text-level formatting was stored in an array of unique marks. Now that same formatting is stored directly on the Text
nodes themselves. For example instead of:{
text: 'A line of text.',
marks: [{ type: 'bold' }],
}
You now have:
{
text: 'A line of text.',
bold: true,
}
And the marks are added and removed from the text nodes using the same
Editor.setNodes
transform that you use for toggling formatting on block and inline nodes. This greatly simplifies things and makes Slate's core even smaller.The
<Slate>
component is now a "controlled" component. This makes things a bit more React-ish, and makes it easier to update the editor's value when new data is received after the initial render. To arrive at the previous "uncontrolled" behavior you'll need to implement it in userland using React's built-in hooks.Whereas previously you would do:
<Slate defaultValue={initialValue}>...</Slate>
Now you must manage the value and selection yourself, like:
const [value, setValue] = useState(initialValue)
const [selection, setSelection] = useState(null)
<Slate
initialValue={initialValue}
selection={selection}
onChange={(value, selection) => {
setValue(value)
setSelection(selection)
}}
>
...
</Slate>
A complete overhaul. The Slate codebase has had a complete overhaul and many pieces of its core architecture have been reconsidered from the ground up. There are lots of changes. We recommend re-reading the Walkthroughs and Concepts documentation and the Examples to get a sense for everything that has changed. As well as the Migration writeup for what the major changes are.
⚠ Warning: Changes past this point refer to the older Slate architecture, based on Immutable.js and without TypeScript. Many things are different in the older architecture and may not apply to the newer one.
Introducing the
Annotation
model. This is very similar to what used to be stored in value.decorations
, except they also contain a unique "key" to be identified by. They can be used for things like comments, suggestions, collaborative cursors, etc.{
object: 'annotation',
key: String,
type: String,
data: Map,
anchor: Point,
focus: Point,
}
There are three new
*_annotation
operations. The set of operations now includes add_annotation
, remove_annotation
and set_annotation
. They are similar to the existing *_mark
operations.Introducing "iterable" model methods. This introduces several iteratable-producing methods on the
Element
interface, which Document
, Block
and Inline
all implement. There are iterables for traversing the entire tree:element.blocks(options)
element.descendants(options)
element.inlines(options)
element.texts(options)
element.ancestors(path, options)
element.siblings(path, options)
You can use them just like the native JavaScript iterables. For example, you can loop through the text nodes after a specific node:
for (const next of document.texts({ path: start.path })) {
const [node, path] = next
// do something with the text node or its path
}
Or you can traverse all of the "leaf" blocks:
for (const [block] of document.blocks({ onlyLeaves: true })) {
// ...
}
And because these iterations use native
for/of
loops, you can easily break
or return
out of the loops directly—a much nicer DX than remembering to return false
.The
value.decorations
property is now value.annotations
. Following with the split of decorations into annotations, this property was also renamed. They must now contain unique key
properties, as they are stored as a Map
instead of a List
. This allows for much more performant updates.The
Decoration
model no longer has a nested mark
property. Previously a real Mark
object was used as a property on decorations, but now the type
and data
properties are first class properties instead.{
object: 'decoration',
type: String,
data: Map,
anchor: Point,
focus: Point,
}
Mark operations no longer have
offset
or length
properties. Since text nodes now contain a unique set of marks, it wouldn't make sense for a single mark-related operation to result in a splitting of nodes. Instead, when a mark is added to only part of a text node, it will result in a split_node
operation as well as an add_mark
operation.Text operations no longer have a
marks
property. Previously it was used to add text with a specific set of marks. However this is no longer necessary, and when text is added with marks it will result in an insert_text
operation as well as an add_mark
operation.Using
Text.create
or Text.createList
with a leaves
property will error. Now that text nodes no longer have leaves, you will need to pass in the text
string and marks
directly when creating a new text node. (However, you can still create entire values using Value.create
in a backwards compatible way for convenience while migrating.)// This works, although deprecated, which is the common case...
Value.create(oldValueJson)
// ...but this will error!
Text.create(oldTextJson)
Value.toJSON
returns the new data model format, without leaves. Although Value.fromJSON
and Value.create
allow the old format in deprecated mode, calling Value.toJSON
will return the new data format. If you still need the old one you'll need to iterate the document tree converting text nodes yourself.The low-level
Value.*
and Node.*
mutation methods have changed. These changes follow the operation signature changes, since the methods take the same arguments as the operations themselves. For example:// Previously...
value.addMark(path, offset, length, mark)
// ...is now:
value.addMark(path, mark)
These are low-level methods, so this change shouldn't affect the majority of use cases.
Initializing editors with
Text
nodes with a leaves
property is deprecated. In this new version of Slate, creating a new value with Value.create
with the old leaf data model is still allowed for convenience in migration, but it will be removed in a coming version. (However, using the low-level Text.create
will throw an error!)// This works, although deprecated, which is the common case...
Value.create(oldValueJson)
// ...but this will error!
Text.create(oldTextJson)
A few properties of
Operation
objects have changed. In an effort to standardize and streamline operations, their properties have changed. This won't affect 90% of use cases, since operations are usually low-level concerns. However, if you are using operational transform or some other low-level parts of Slate, this may affect you. The value
, selection
, node
, and mark
properties—which contained references to Immutable.js objects—have all been removed. In their place, we have standardized a properties
and newProperties
pair. This will greatly reduce the size of operations stored in memory, and makes dealing with them easier when serialized as well.Introducing the
child_min_invalid
and child_max_invalid
schema errors. These new schema errors map directly to the mix
and max
schema rule definitions, and make it easier to determine exactly what your normalization logic needs to do to fix the document.Added new node retrieval methods. There are three new methdos for node retrieval. The first is
getNodesAtRange
which will retrieve all of the nodes in the tree in a given range. And the second two are getRootBlocksAtRange
and getRootInlinesAtRange
for retrieving the top-most blocks or inlines in a given range. These should be helpful in defining your own command logic.Schema errors for
min
and max
rules have changed. Previously they would result in errors of child_required
, child_object_invalid
, child_type_invalid
and child_unknown
. Now that we have the new child_min_invalid
and child_max_invalid
errors, these schema rules will return them instead, making it much easier to determine exactly which rule is causing a schema error.The
getBlocksAtRange
and getInlinesAtRange
methods have been renamed. To clear up confusion about which blocks and inlines are retrieve in the case of nesting, these two methods have been renamed to getLeafBlocksAtRange
and getLeafInlinesAtRange
to clarify that they retrieve the bottom-most nodes. And now there are two additional methods called getRootBlocksAtRange
and getRootInlinesAtRange
for cases where you want the top-most nodes instead.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:Hotkey({
hotkey: 'cmd+b',
command: 'addBoldMark',
})
Or a custom command function:
Hotkey({
hotkey: 'cmd+b',
command: editor => editor.addBoldMark().moveToEnd(),
})
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
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:editor.run('onKeyDown', { key: 'Tab', ... })
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.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.Introducing the
Editor
controller. Previously there was a vague editor
concept, that was the React component itself. This was helpful, but because it was tightly coupled to React and the browser, it didn't lend itself to non-browser use cases well. This meant that the line between "model" and "controller/view" was blurred, and some concepts lived in both places at once, in inconsistent ways.A new
Editor
controller now makes this relationship clear. It borrows many of its behaviors from the React <Editor>
component. And the component actually just instantiates its own plain JavaScript Editor
under the covers to delegate the work to.This new concept powers a lot of the thinking in this new version, unlocking a lot of changes that bring a clearer separation of responsibilities to Slate. It allows us to create editors in any environment, which makes server-side use cases easier, brings parity to testing, and even opens us up to supporting other view layers like React Native or Vue.js in the future.
It has a familiar API, based on the existing
editor
concept:const editor = new Editor({ plugins, value, onChange })
editor.change(change => {
...
})
However it also introduces imperative methods to make testing easier:
editor.run('renderNode', props)
editor.event('onKeyDown', event)
editor.command('addMark', 'bold')
editor.query('isVoid', node)
I'm very excited about it, so I hope you like it!
Introducing the "commands" concept. Previously, "change methods" were treated in a first-class way, but plugins had no easy way to add their own change methods that were reusable elsewhere. And they had no way to override the built-in logic for certain commands, for example
splitBlock
or insertText
. However, now this is all customizable by plugins, with the core Slate plugin providing all of the previous default commands.const plugin = {
commands: {
wrapQuote(change) {
change.wrapBlock('quote')
},
},
}
Those commands are then available directly on the
change
objects, which are now editor-specific:change.wrapQuote()
This allows you to define all of your commands in a single, easily-testable place. And then "behavioral" plugins can simply take command names as options, so that you have full control over the logic they trigger.
Introducing the "queries" concept. Similarly to the commands, queries allow plugins to define specific behaviors that the editor can be queried for in a reusable way, to be used when rendering buttons, or deciding on command behaviors, etc.
For example, you might define an
getActiveList
query:const plugin = {
queries: {
getActiveList(editor) {},
},
}
And then be able to re-use that logic easily in different places in your codebase, or pass in the query name to a plugin that can use your custom logic itself:
const list = change.getActiveList()
if (list) {
...
} else {
...
}
Taken together, commands and queries offer a better way for plugins to manage their inter-dependencies. They can take in command or query names as options to change their behaviors, or they can export new commands and queries that you can reuse in your codebase.
The middleware stack is now deferrable. With the introduction of the
Editor
controller, the middleware stack in Slate has also been upgraded. Each middleware now receives a next
function (similar to Express or Koa) that allows you to choose whether to iterating the stack or not.// Previously, you'd return `undefined` to continue.
function onKeyDown(event, editor, next) {
if (event.key !== 'Enter') return
...
}
// Now, you call `next()` to continue...
function onKeyDown(event, editor, next) {
if (event.key !== 'Enter') return next()
...
}
While that may seem inconvenient, it opens up an entire new behavior, which is deferring to the plugins later in the stack to see if they "handle" a specific case, and if not, handling it yourself:
function onKeyDown(event, editor, next) {
if (event.key === 'Enter') {
const handled = next()
if (handled) return handled
// Otherwise, handle `Enter` yourself...
}
}
This is how all of the core logic in
slate-react
is now implemented, eliminating the need for a "before" and an "after" plugin that duplicate logic.Under the covers, the
schema
, commands
and queries
concept are all implemented as plugins that attach varying middleware as well. For example, commands are processed using the onCommand
middleware under the covers:const plugin = {
onCommand(command, editor, next) {
...
}
}
This allows you to actually listen in to all commands, and override individual behaviors if you choose to do so, without having to override the command itself. This is a very advanced feature, which most people won't need, but it shows the flexibility provided by migrating all of the previously custom internal logic to be based on the new middleware stack.
Plugins can now be defined in nested arrays. This is a small addition, but it means that you no longer need to differentiate between individual plugins and multiple plugins in an array. This allows plugins to be more easily composed up from multiple other plugins themselves, without the end user having to change how they use them. Small, but encourages reuse just a little bit more.
The
slate-simulator
is deprecated. Previously this was used as a pseudo-controller for testing purposes. However, now with the new Editor
controller as a first-class concept, everything the simulator could do can now be done directly in the library. This should make testing in non-browser environments much easier to do.The
Value
object is no longer tied to changes. Previously, you could create a new Change
by calling value.change()
and retrieve a new value. With the re-architecture to properly decouple the schema, commands, queries and plugins from the core Slate data models, this is no longer possible. Instead, changes are always created via an Editor
instance, where those concepts live.// Instead of...
const { value } = this.state
const change = value.change()
...
this.onChange(change)
// You now would do...
this.editor.change(change => {
const { value } = change
...
})
Sometimes this means you will need to store the React
ref
of the editor
to be able to access its editor.change
method in your React components.Remove the
Stack
"model", in favor of the new Editor
. Previously there was a pseudo-model called the Stack
that was very low level, and not really a model. This concept has now been rolled into the new Editor
controller, which can be used in any environment because it's just plain JavaScript. There was almost no need to directly use a Stack
instance previously, so this change shouldn't affect almost anyone.Remove the
Schema
"model", in favor of the new Editor
. Previously there was another pseudo-model called the Schema
, that was used to contain validation logic. All of the same validation features are still available, but the old Schema
model is now rolled into the Editor
controller as well, in the form of an internal SchemaPlugin
that isn't exposed.Remove the
schema.isVoid
and schema.isAtomic
in favor of queries. Previously these two methods were used to query the schema about the behavior of a specific node
or decoration
. Now these same queries as possible using the "queries" concept, and are available directly on the change
object:if (change.isVoid(node)) {
...
}
The middleware stack must now be explicitly continued, using
next
. Previously returning undefined
from a middleware would (usually) continue the stack onto the next middleware. Now, with middleware taking a next
function argument you must explicitly decide to continue the stack by call next()
yourself.Remove the
History
model, in favor of commands. Previously there was a History
model that stored the undo/redo stacks, and managing saving new operations to those stacks. All of this logic has been folded into the new "commands" concept, and the undo/redo stacks now live in value.data
. This has the benefit of allowing the history behavior to be completely overridable by userland plugins, which was not an easy feat to manage before.Values can no longer be normalized on creation. With the decoupling of the data model and the plugin layer, the schema rules are no longer available inside the
Value
model. This means that you can no longer receive a "normalized" value without having access to the Editor
and its plugins.// While previously you could attach a `schema` to a value...
const normalized = Value.create({ ..., schema })
// Now you'd need to do that with the `editor`...
const value = Value.create({ ... })
const editor = new Editor({ value, plugins: [{ schema }] })
const normalized = editor.value
While this seems inconvenient, it makes the boundaries in the API much more clear, and keeps the immutable and mutable concepts separated. This specific code sample gets longer, but the complexities elsewhere in the library are removed.
The
Change
class is no longer exported. Changes are now editor-specific, so exporting the Change
class no longer makes sense. Instead, you can use the editor.change()
API to receive a new change object with the commands and queries specific to your editor's plugins.The
getClosestVoid
, getDecorations
and hasVoidParent
method now take an editor
. Previously these Node
methods took a schema
argument, but this has been replaced with the new editor
controller instead now that the Schema
model has been removed.The
withoutNormalization
helper has been renamed to withoutNormalizing
. This is to stay consistent with the new helpers for withoutSaving
and withoutMerging
.The the "operation flags" concept was removed. This was a confusing concept that was implemented in multiple different ways and led to the logic around normalizing, saving, and merging operations being more complex than it needed to be. These flags have been replaced with three simpler helper functions:
withoutNormalizing
, withoutSaving
and withoutMerging
.change.withoutNormalizing(() => {
nodes.forEach(node => change.removeNodeByKey(node.key))
})
change.withoutSaving(() => {
change.setValue({ decorations })
})
This means that you no longer use the
{ normalize: false }
or { save: false }
options as arguments to individual change methods, and instead use these new helper methods to apply these behaviors to groups of changes at once.The "normalize" change methods have been removed. Previously there were a handful of different normalization change methods like
normalizeNodeByPath
, normalizeParentByKey
, etc. These were confusing because it put the onus on the implemented to know exact which nodes needed to be normalized. They have been removed, and implementers no longer ever need to worry about which specific nodes to normalize, as Slate will handle that for them.The internal
refindNode
and refindPath
methods were removed. These should never have been exposed in the first place, and are now no longer present on the Element
interface. These were only used internally during the normalization process.Remove all previously deprecated code paths. This helps to reduce some of the complexity in Slate by not having to handle these code paths anymore. And it helps to reduce file size. When upgrading, it's highly recommended that you upgrade to the previous version first and ensure there are no deprecation warnings being logged, then upgrade to this version.
Introducing the
Range
model and interface. Previously the "range" concept was used in multiple different places, for the selection, for decorations, and for acting on ranges of the document. This worked okay, but it was hiding the underlying system which is that Range
is really an interface that other models can choose to implement. Now, we still use the Range
model for referencing parts of the document, but it can also be implemented by other models that need to attach more semantic meaning...Introducing the
Decoration
and Selection
models. These two new models both implement the new Range
interface. Where previously they had to mis-use the Range
model itself with added semantics. This just cleans up some of the confusion around overlapping properties, and allows us to add even more domain-specific methods and properties in the future without trouble.Decorations have changed! Previously, decorations piggybacked on the
Range
model, using the existing marks
property, and introducing their own isAtomic
property. However, they have now been split out into their own Decoration
model with a single mark
and with the isAtomic
property controlled by the schema. What previously would have looked like:Range.create({
anchor: { ... },
focus: { ... },
marks: [{ type: 'highlight' }],
isAtomic: true,
})
Is now:
Decoration.create({
anchor: { ... },
focus: { ... },
mark: { type: 'highlight' },
})
Each decoration maps to a single
mark
object. And the atomicity of the mark controlled in the schema instead, for example:const schema = {
marks: {
highlight: {
isAtomic: true,
},
},
}
The
Range
model has reduced semantics. Previously, since all decorations and selections were ranges, you could create ranges with an isFocused
, isAtomic
or marks
properties. Now Range
objects are much simpler, offering only an anchor
and a focus
, and can be extended by other models implementing the range interface. However, this means that using Range.create
or document.createRange
might not be what you want anymore. For example, for creating a new selection, you used to use:const selection = document.createRange({
isFocused: true,
anchor: { ... },
focus: { ... },
})
But now, you'll need to use
document.createSelection
instead:const selection = document.createSelection({
isFocused: true,
anchor: { ... },
focus: { ... },
})
The
value.decorations
property is no longer nullable. Previously when no decorations were applied to the value, the decorations
property would be set to null
. Now it will be an empty List
object, so that the interface is more consistent.The
Node.createChildren
static method is deprecated. This was just an alias for Node.createList
and wasn't necessary. You can use Node.createList
going forward for the same effect.The
renderPortal
property of plugins is deprecated. This allows slate-react
to be slightly slimmer, since this behavior can be handled in React 16 with the new <React.Fragment>
using the renderEditor
property instead, in a way that offers more control over the portal behavior.The
data
property of plugins is deprecated. This property wasn't well designed and circumvented the core tenet that all changes to the value
object will flow through operations inside Change
objects. It was mostly used for view-layer state which should be handled with React-specific conventions for state management instead.Node.isVoid
access is deprecated. Previously the "voidness" of a node was hardcoded in the data model. Soon it will be determined at runtime based on your editor's schema. This deprecation just ensures that you aren't using the node.isVoid
property which will not work in future verisons. What previously would have been:if (node.isVoid) {
...
}
Now becomes:
if (schema.isVoid(node)) {
...
}
This requires you to have a reference to the
schema
object, which can be access as value.schema
.Value.isFocused/isBlurred
and Value.hasUndos/hasRedos
are deprecated. These properties are easily available via value.selection
and value.history
instead, and are now deprecated to reduce the complexity and number of different ways of doing things.Introducing the
Point
model. Ranges are now built up of two Point
models—an anchor
and a focus
—instead of having the properties set directly on the range itself. This makes the "point" concept first-class in Slate and better API's can be built around point objects.Point.create({
key: 'a',
path: [0, 0],
offset: 31,
})
These points are exposed on
Range
objects via the anchor
, focus
, start
and end
properties:const { anchor, focus } = range
change.removeNodeByKey(anchor.key)
These replace the earlier
anchorKey
, anchorOffset
, etc. properties.Document.createRange
creates a relative range. Previously you'd have to use Range.create
and make sure that you passed valid arguments, and ensure that you "normalized" the range to sync its keys and paths. This is no longer the case, since the createRange
method will do it for you.const range = document.createRange({
anchor: {
key: 'a',
offset: 1,
},
focus: {
key: 'a',
offset: 4,
},
})
This will automatically ensure that the range references leaf text nodes, and that its
anchor
and focus
paths are set.Document.createPoint
creates a relative point. Just like the createRange
method, createPoint
will create a point that is guaranteed to be relative to the document itself. This is often a lot easier than using Point.create
directly.const anchor = document.createPoint({
key: 'a',
offset: 1,
})
The
Range.focus
method was removed. (Not Change.focus
!) This was necessary to make way for the new range.focus
point property. Usually this would have been done in a migration-friendly way like the rest of the method changes in this release, but this was an exception. However the change.focus()
method is still available and works as expected.Range.set
and Range.merge
are dangerous. If you were previously using the super low-level Immutable.js methods range.set
or range.merge
with any of the now-removed properties of ranges, these invocations will fail. Instead, you should use the range.set*
helpers going forward which can be migrated with deprecations warnings instead of failing outright.The
offset
property of points defaults to null
. Previously it would default to 0
but that could be confusing because it made no distinction from a "set" or "unset" offset. Now they default to null
instead. This shouldn't really affect any real-world usage of Slate.The
Range.toJSON()
structure has changed. With the introduction of points, the range now returns its anchor
and focus
properties as nested point JSON objects instead of directly as properties. For example:{
"object": "range",
"anchor": {
"object": "point",
"key": "a",