slate: mismatch between "editor" and "value" concepts
Do you want to request a feature or report a bug?
Discussion.
What’s the current behavior?
Just opening this up for discussion, since it’s something that I think Slate hasn’t really solved well so far, and leads to it being hard to decide in either direction for the API…
Right now there are two concepts vying for the “top spot” in the architecture:
-
value
— which holds the document, the selection, the history and the schema. This can easily be created server-side (or wherever), since it’s not coupled to the view layer at all. (I’d considerchange
to be equivalent tovalue
here, since they hold all the same data.) -
editor
— which holds the plugins, the current value, and the schema to an extent. This is less easy to use server-side, since the editor is inherently tied to the view layer. (Although this need not always be the case.)
So far, it’s been unclear which is the “primary” concept. For example…
-
Event handlers are passed both a
change
and aneditor
to allow the user to decide which they want to use. You normally use thechange
, but sometimes, for example if you want to trigger an async change later, you need to use theeditor
. -
For the idea of “commands and queries” from #2066, it’s unclear whether these commands should live inside the schema (or similar) and be exposed as
change.command(...)
. Or whether, since they are tied to the plugins, they should be exposed aseditor.command(...)
. Both have tradeoffs. -
You could make the argument that
history
shouldn’t really be inside thevalue
, and instead be controlled by theeditor
which is the long-lived entity—which would seem totally reasonable to me. -
Right now there’s a confusion in that the editor’s plugins create a final
schema
object that gets set on thevalue
. But the value can also be initialized with its ownschema
, which will be overwritten… and it’s not clear how they interact.
What’s the expected behavior?
I’m not totally sure. This is something that I’ve wrestled with, but never found a nice mental model for being clear about the distinctions between the two. I’d love for anyone with ideas to comment! (Even if you aren’t sure yourself, anything is helpful!)
I think it’s helpful to have a few goals:
-
It would be good for “commands”, “history”, etc. to all be able to be modeled in server-side environments where there is no concept of the “view” layer. This doesn’t necessarily mean it has to be the
value
model, but it has to be something that isn’t tied to the DOM or rendering. -
It would be best if the top-level concept can unify the plugins, schema, history, commands, etc. in a way that makes it clear where all of this stuff stems from.
About this issue
- Original URL
- State: closed
- Created 6 years ago
- Comments: 18 (15 by maintainers)
Hmm I’ve been wrestling with this one myself. The mental model I have for the Editor is that it is simply the view layer. Given a bunch of props like value / schema etc., Render something to the page. The value is then just the pure state of the app: what is the current snapshot of the app’s state.
I think the problem is that this gives us the “view” and the “model” but it doesn’t really tell us where the controller lives (in a traditional MVC architecture). So the current “controller” is a combination of change, value and the editor that don’t always know how to talk to each other. This is why there is some confusion about whether to make changes you need to talk to the value or the editor.
I wonder if there should be a third component like
Controller
that contains all the event / state management. Then the “editor” is just a simple view layer that renders and proxies interaction events into the controller. The controller would also just be pure non-react JS, easy to work with on the server side. It would probably also be easier to test than the editor.The big downside I see to this is that in React a component is generally also the controller. This might cause us to move further away from React best practices.
I’m just going to leave a summary of my current API architecture thoughts here…
Introduce the
Editor
controller. The piece is to introduce a new “plain JS” controller exposed in the coreslate
module, calledEditor
. It will contain the core editor logic (like ensuring normalization, running through the plugins stack, etc.) that is currently partly inslate-react
. It will be used byslate-react
, or on the server-side by itself, or in the future in non-React view layers to get the basic editor behaviors. If you exposed a Slate-like API to developers you could even bundle theEditor
in your API library.Fold the
Stack
intoEditor
. Once you have the concept of this editor controller, you don’t actually need the stack. It lives in “models” right now but it’s not really a model. It’s just thin layer around some iterations methods that can easily be moved to become methods on theEditor
instance instead. This makes the public API more clear, such that plugins can invoke the stack if they have a need to (as the core plugin currently does).Add a
change.editor
reference. Changes are actually editor-specific instead of value-specific like they are currently architected. This also clears up the idea that changes are “models”, while they are in a sense, they are not immutable like the rest of Slate’s models. By making them editor-aware we can…Remove the
schema
property fromValue
. With the reference tochange.editor
and thuschange.editor.schema
, we no longer need to have thevalue.schema
coupling which is confusing because the schema is really an editor-specific object. This also eliminates another “non-model” model, because the schema isn’t really a “model” in the sense of being serializable or immutable in the way that all other Slate models are. (This is a pattern, there are a handful of things in “models” right now that don’t really belong.)Remove the
value.change()
method. Similarly, with changes now tied to theEditor
controller, changes can no longer be created with only access to avalue
, because they rely on access to the editor’sschema
to make decisions about how the change occurs (for example for void nodes).Add the “commands” concept to the editor. This introduces a new concept called “commands” which are part of the
Editor
and can be defined in plugins. This relates to #2066 and allows for editor-level commands that can be defined in a single place, but used in many. It is very similar in approach to Redux Actions, or the Elm Architecture, in terms of having a pre-defined set of “commands” that can change the state. And now that theChange
objects are editor-aware, each command can be exposed aschange[command]
, exactly like the current change methods. Which brings us to…Add “queries” support. I was originally thinking of these as two separate concepts, but I think instead commands could return values, in which case “queries” are just commands that return values but don’t add any operations to the stack. This way we don’t need to introduce an entirely new concept? Although it can be done either way, introducing a separate “queries” concept would work just as well.
Convert the current “changes” into commands. With the concept of commands introduced, all of what are currently called “changes” can now be implemented as commands in the core plugin, instead of the current custom approach. This is functionally equivalent, but it means that now even the core commands can be overridden for use cases that need it. (This is something that comes up somewhat often in Slack as a question, and isn’t easily solved right now by any other means.)
Convert
History
into commands onvalue.data
. Right now the history is implemented as “changes” but it also special-cased in a way that makes it very hard to customized. With the new pluggable commands concept though, history can change to become three commands:save
,undo
,redo
which are overridable by the user. And instead of needing to live in a customHistory
model, they can be implemented to live in thevalue.data
namespace asdata.undos
anddata.redos
. This way users can implement their own history, using a totally different data structure if they’d like.At the end you end up with a clearer delineations:
<Editor>
,<Island>
,render*
The only overlap remaining that I can see still is that “plugins” end up defining logic that lives in both the Controller and the View layers. But I’m not sure if there’s a way around this. (If anyone has ideas I’m open!)
How does that sound?
I don’t really have anything concrete to add, other than that I think of Editor purely as the view/controller layer, and that I really really like being able to work with a Value outside of the context of showing an editor. It’s one of my favorite things about Slate – with just a Value, I can create and apply operations, I can apply schemas, I can serialize / deserialize, etc. I get all the transformations I want, and just get a new Value at the end.
A few things people have mentioned here that I’d appreciate:
.setValue
, and it’d be nice to open this up. Having it be a property of Editor makes some sense to me, since I think of Value changes as automated, and I can’t imagine a program wanting undo / redo that it couldn’t handle itself.@bryanph thank you for taking the time to write that up! I really appreciate it. Those are great questions, I’ll try to answer each one…
Good question. This would unlock a few different things:
Plugins would be able to fully override the history logic for saving, undoing, and redoing operations. This way one could implement a “non-operational” history as a plugin, or a different logic for when to join operations into groups, etc. (Right now this is only possible if you define your own immutable
History
record that copies the mirrors API exactly, and are sure to always initialize your value with it. Not something a plugin can do, and even then certain new behaviors are probably out of reach.)Plugins would be able to fully override any of the commands (which are now changes). This allows for defining custom behaviors for even the built-in commands like
insertNodeByPath
. I think this would allow for “attribution” where you want to attach marks to any text/nodes each user does. (Although this may be possible currently at the operations level, I haven’t fully investigated.) It would also be possible for users to define a custom behavior for internal editor commands likesplitNodeByPath
, instead of having to override the behavior from the top by overriding the <kbd>enter</kbd> functionality.With the addition of commands, plugins would be able to define their own first-class commands that can be used throughout the editor. And, more importantly, plugins can take a command as a configuration option to be performed when some logic is achieved. (This is currently possible by being diligent about factoring all of your editor logic into small helper functions, and instead designing plugins to accept those functions.)
I think that is it. But it also is about cleaning up the model distinctions that currently make the codebase confusing, and that limit it from achieving things in the future.
Written up above, but it comes down to adding “commands” and “queries” for this.
Essentially, yes. Except I’m thinking about trying to make it more transparent, such that all of the commands are exposed as first-class methods on the
Change
object. This keeps the API native feeling and terse, instead of having to result to a secondary data structure, orBut I think it would be good to expose
editor.commands
with a map of them so that people can check to ensure that their plugins are registering properly. I haven’t really fleshed this out yet.This one is tricky. It defines what shape the document (and later ideally the selection too) can take. But it isn’t necessarily part of the model so much as a constraint that is applied to the model. It leads to confusion if a value can have a
value.schema
when inserted into the editor, while at the same time the editor has aneditor.schema
. And then there’s a question of which one is the “true” schema, or if they are somehow combined, etc.Instead, if the
editor
is allowed to control the schema, those questions disappear. This matches more closely with the ideas being developed likeisVoid
orisAtomic
not being inherent to the model, but inherent to the editor. One editor could treat images as voids in the traditional sense, whilst another could decide that it edits their textual content by leaving them non-void with the image as abackground-image
. (Or something 😄 else crazy!)These are good points. I think we’re not quite close enough to seeing what React Native, or what Vue support looks like to make a decision. But it could very well be that we end up separating the fields out by environment if we need to. Same for the plugin naming. Until we get closer though I think it’s safe to keep them as-is.
How does all that sound?
@ericedem great questions! Thanks for reading and digging in.
Sorry, this is from #2066. Basically once you add “commands” you start to realize that many things that work with commands need to ask the editors questions to know whether to perform certain logic, and you get a parallel “queries” feature for doing that. If “commands” are schema-specific functions that perform changes. Then “queries” are schema-specific functions that return information.
I’m sketching this out right now. I’d like to keep the component interface the same, since it feels pretty nice to me, and feels React-ish. Such that the
<Editor>
component is actually just going to create its ownEditor
controller under the covers, and rely on the controller to handle some of the logic for it.However, that underlying controller is never publicly exposed to the user. So for example, the
<Editor>
will wrap theEditor
controller, and when creating aChange
it will have achange.editor
pointing to the component instance. (The component will be in charge of mimicking theEditor
interface so that they are interchangeable.)By doing that, the
<Editor>
remains a “ViewController” in the React sense. I think the pattern that will end up shaking out is that each view layer can adopt their own patterns. For React, it merges the “view” and “controller”, and thus it makes sense to do that here. If some other view layer had two separate concepts, they could separate them.However, on the server-side you’d just use the
Editor
directly, with the same API (since<Editor>
mimics it).What this means is that some plugins, if they happen to rely on React-specific properties of the
change.editor
will be React specific. But this is already the case withevent
being synthetic events. And I think this is inevitable so we should embrace it.How does that sound?
@ianstormtaylor Yeah. In the OT case, though, you could have valid simultaneous operations put the Value into an invalid state.
Imagine you have two empty paragraphs, client 1 deletes paragraph 1, and client 2 simultaneously deletes paragraph 2. Those are both valid operations on the document, but you’d end up with a Document with no nodes.
So you’d probably want the Schema / Controller on that side, too, you just wouldn’t need an Editor.