slate v0.42.0 Release Notes
Release Date: 2018-10-09 // over 5 years ago-
NEW
Introducing the
Editor
controller. Previously there was a vagueeditor
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 JavaScriptEditor
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
orinsertText
. 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 anext
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
andqueries
concept are all implemented as plugins that attach varying middleware as well. For example, commands are processed using theonCommand
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.
๐ ###### DEPRECATED
๐ The
slate-simulator
is deprecated. Previously this was used as a pseudo-controller for testing purposes. However, now with the newEditor
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.BREAKING
The
Value
object is no longer tied to changes. Previously, you could create a newChange
by callingvalue.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 anEditor
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 theeditor
to be able to access itseditor.change
method in your React components.โ Remove the
Stack
"model", in favor of the newEditor
. Previously there was a pseudo-model called theStack
that was very low level, and not really a model. This concept has now been rolled into the newEditor
controller, which can be used in any environment because it's just plain JavaScript. There was almost no need to directly use aStack
instance previously, so this change shouldn't affect almost anyone.โ Remove the
Schema
"model", in favor of the newEditor
. Previously there was another pseudo-model called theSchema
, that was used to contain validation logic. All of the same validation features are still available, but the oldSchema
model is now rolled into theEditor
controller as well, in the form of an internalSchemaPlugin
that isn't exposed.โ Remove the
schema.isVoid
andschema.isAtomic
in favor of queries. Previously these two methods were used to query the schema about the behavior of a specificnode
ordecoration
. Now these same queries as possible using the "queries" concept, and are available directly on thechange
object:if (change.isVoid(node)) { ... }
The middleware stack must now be explicitly continued, using
next
. Previously returningundefined
from a middleware would (usually) continue the stack onto the next middleware. Now, with middleware taking anext
function argument you must explicitly decide to continue the stack by callnext()
yourself.โ Remove the
History
model, in favor of commands. Previously there was aHistory
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 invalue.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 theEditor
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 theChange
class no longer makes sense. Instead, you can use theeditor.change()
API to receive a new change object with the commands and queries specific to your editor's plugins.The
getClosestVoid
,getDecorations
andhasVoidParent
method now take aneditor
. Previously theseNode
methods took aschema
argument, but this has been replaced with the neweditor
controller instead now that theSchema
model has been removed.