-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add controller #2221
Add controller #2221
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haven't looked through everything yet, just some questions around rendering and which values correspond to what within the editor.
if ( | ||
normalize === false || | ||
(this.plugins === this.tmp.lastPlugins && | ||
this.value === this.tmp.lastValue) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm not understanding, but shouldn't this be checking against the value being passed in? Like: value === this.tmp.lastValue
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, you're right! Good catch.
// Update the props on the controller before rendering. | ||
const { options, readOnly } = props | ||
const plugins = this.resolvePlugins(props.plugins, props.schema) | ||
const value = tmp.change ? tmp.change.value : props.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The editor will only rerender if there is a props change, should we only render prop.value here instead? Or maybe tmp.change
should be state.change
if we want a render cycle to happen.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ericedem not sure I understand what you mean. I believe here tmp.change
should only be present if we needed to normalize the value
before the component was mounted and ready to receive changes. So it's just saying that if there's an interim value, use it, since it will soon be flushed via onChange
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Op! Sorry, I just got what you mean now. You're right, since we're already rendering via editor.value
we're already achieving this, and the tmp.change
logic here isn't doing anything!
@ericedem thanks, that is super helpful! |
Damn that's quick! Looking forward to a writeup |
Codecov Report
@@ Coverage Diff @@
## master #2221 +/- ##
=========================================
+ Coverage 82.65% 83.5% +0.85%
=========================================
Files 40 42 +2
Lines 4035 4092 +57
=========================================
+ Hits 3335 3417 +82
+ Misses 700 675 -25
Continue to review full report at Codecov.
|
88e2613
to
7e0e2af
Compare
This is really great, looking forward to this! On the logistics side, is there anything you'd like from the community in terms of testing before this gets rolled out? Given how major this change is, it seems likely there will be a lot of bugs in the semver this lands in (though this will should fix a bunch too). |
@ericedem thanks! Nothing special compared to normal releases with breaking changes I think. |
Would it be possible or worthwhile to be able to generate a new
|
@justinweiss I'm not sure what you mean. Could you give me a code sample or use case? |
@ianstormtaylor Never mind! I thought I was using |
Look awesome @ianstormtaylor! What is the best approach to handle it with the new version now that |
@amitm02 yup, I'd do it exactly as you said. Create another non-React |
@ianstormtaylor - Man! After having refactored our code during the weekend for the new release, I just want to say that this change is really awesome! There were a lot to refactor though (we relied on |
@skogsmaskin thank you, that means a lot to me. I get a bit nervous sometimes to make breaking changes, but it’s really nice to hear that they laid off like we thought! |
* fold Stack into Editor * switch Change objects to be tied to editors, not values * introduce controller * add the "commands" concept * convert history into commands on `value.data` * add the ability to not normalize on editor creation/setting * convert schema to a mutable constructor * add editor.command method * convert plugin handlers to receive `next` * switch commands to use the onCommand middleware * add queries support, convert schema to queries * split out browser plugin * remove noop util * fixes * fixes * start fixing tests, refactor hyperscript to be more literal * fix slate-html-serializer tests * fix schema tests with hyperscript * fix text model tests with hyperscript * fix more tests * get all tests passing * fix lint * undo decorations example update * update examples * small changes to the api to make it nicer * update docs * update commands/queries plugin logic * change normalizeNode and validateNode to be middleware * fix decoration removal * rename commands tests * add useful errors to existing APIs * update changelogs * cleanup * fixes * update docs * add editor docs
* fold Stack into Editor * switch Change objects to be tied to editors, not values * introduce controller * add the "commands" concept * convert history into commands on `value.data` * add the ability to not normalize on editor creation/setting * convert schema to a mutable constructor * add editor.command method * convert plugin handlers to receive `next` * switch commands to use the onCommand middleware * add queries support, convert schema to queries * split out browser plugin * remove noop util * fixes * fixes * start fixing tests, refactor hyperscript to be more literal * fix slate-html-serializer tests * fix schema tests with hyperscript * fix text model tests with hyperscript * fix more tests * get all tests passing * fix lint * undo decorations example update * update examples * small changes to the api to make it nicer * update docs * update commands/queries plugin logic * change normalizeNode and validateNode to be middleware * fix decoration removal * rename commands tests * add useful errors to existing APIs * update changelogs * cleanup * fixes * update docs * add editor docs
This is a structural change to the architecture of some of the core parts of Slate. Specifically it replaces the old "pseudo-models", in the
Stack
,Schema
andHistory
with a newEditor
controller. In doing so it upgrades the plugin middleware stack to be more flexible. All with the goal of simplifying the core architecture, making Slate easier to use server-side, in tests and in other view layers, and making more of the core behaviors of Slate more customizable by plugins.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:However it also introduces imperative methods to make testing easier:
I'm very excited about it, so I hope you like it!
The
<Editor>
can now choose to not normalize on mount. A nice side effect of splitting out theEditor
logic into a reusable place is that it's easier to implement customizable behaviors for normalization. You can now pass anoptions={{ normalize: false }}
prop to the React<Editor>
which will disable the default normalization that takes place when the editor is constructed. This is helpful in cases where you are guaranteed to have an already normalized value, and don't want to incur the performance cost of normalizing it again.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.Those commands are then available directly on the
change
objects, which are now editor-specific: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: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:
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.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:
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: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.
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.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 theEditor
. 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 theEditor
. 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: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.The
editor
object is no longer passed to event handlers. Previously, the third argument to event handlers would be the Reacteditor
instance. However, now thatChange
objects contain a direct reference to the editor, you can access this onchange.editor
instead.In its place is the new
next
argument, which allows you to choose to defer to the plugins further on the stack before handling the event yourself.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 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.slate-hyperscript
no longer normalizes values. This behavior was very problematic because it meant that you could not determine exactly what output you'd receive from any given hyperscript creation. The logic for creating child nodes was inconsistent, relying on the built-in normalization to help keep it "normal". While this is sometimes helpful, it makes writing tests for invalid states very tricky, if not impossible.Now,
slate-hyperscript
does not do any normalization, meaning that you can create any document structure with it. For example, you can create a block node inside an inline node, even though a Slate editor wouldn't allow it. Or, if you don't create leaf text nodes, they won't exist in the output.For example these are no longer equivalent:
Similarly, these are no longer equivalent either:
This allows you to much more easily test invalid states and transition states. However, it means that you need to be more explicit in the "normal" states than previously.
The
<text>
and<mark>
creators now return useful objects. This is a related change that makes the library more useful. Previously you could expect to receive avalue
from the<value>
creator, but the others were less consistent. For example, the<text>
creator would actually return an array, instead of theText
node that you expect.Similarly, the
mark
creator used to return aText
node. Now it returns a list ofLeaf
objects, which can be passed directly as children to the<text>
creator.The
findRange
,findPoint
,cloneFragment
, andgetEventRange
utils now take aneditor
. Previously these utility functions took aschema
argument, but this has been replaced with the neweditor
controller instead now that theSchema
model has been removed.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.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.Fixes: #2045
Fixes: #2152
Fixes: #2066
Fixes: #2214
Fixes: #837
Fixes: #2199
Fixes: #2176
Fixes: #2206
Addresses: #1013
Addresses: #1411
Addresses: #2098 (comment)
Addresses: #1456
Addresses: #1730
Addresses: #1311
Addresses: #2190