slate v0.42.0 Release Notes

Release Date: 2018-10-09 // over 5 years ago
  • NEW

    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.

    ๐Ÿ—„ ###### DEPRECATED

    ๐Ÿ—„ 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.

    BREAKING

    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.