The past month has seen a grand total of three patches on the master branch, which is notably slower than things have been moving before that. I’m still working on ProseMirror, but I did fall into a rabbit hole that went a bit deeper than anticipated (and I’m still not sure I’ve found the bottom).
One of the promises for 0.9.0 was that I’d split the library into smaller modules. The main challenge there is finding good splitting points in the big-blob-of-state that is the ProseMirror
class. I created some experiments that didn’t really work well, and then looked toward Draft for inspiration. If you haven’t seen Draft, it’s an editor component similar to ProseMirror, with less emphasis on complex document structure and schemas, written for React. It defines a state object that’s a persistent (immutable) data structure, and a React-style component that A) takes a state and updates the view (DOM) to reflect that state and B) converts DOM events into state changes.
The idea in these kinds of systems (and React is a pretty weak example – it gets progressively cleaner in Flux, Redux, Om, and Elm) is that a web application’s data flow is constrained to a simple, predictable flow. So instead of everything being stateful, they move towards a system where there’s a single canonical value that defines the state of a component or even the whole app. And instead of mutating that state, the more rigid styles create a new state and leave the old one intact. Instead of event handlers with random effects being wired up all over the place, view components produce either new states (React) or messages that are used to compute a new state (the other approaches), and send those to the code that created them through a single, programmable channel.
Compared to ProseMirror’s current design, this stuck me as undeniably superior technology. But of course I was not going to take any of the current competing frameworks (there’s a lot) as a dependency and lock the library into one ecosystem. So I’ve tried to come up with an approach that has the advantages of a linear data flow and persistent state values, but which can be used both on its own and in the context of one of these frameworks.
I’ve pushed the code that I have so far to the ‘linear’ branch. It’s not done yet, and it might take a while before I feel comfortable releasing it, but here is how it works:
The state
module defines a data structure that keeps the state of the editor. At its core, this is just the current document, selection, and transient marks added at the current cursor position. Those were already immutable data structures, so that part is easy. A state object has an applyAction
method, which you give an action (an object with a type
field and optional other data in it) in order to produce a new state.
But in order to be able to define things like history and collaborative editing as plugins, the state can be extended with extra fields, which can define the way they are recomputed for different types of actions.
The view
module defines a really bare-bones version of the editor interface — it shows the document, and handles events on it in the most basic way. It is exposed as an object that takes an editor state and a set of ‘props’ when created, and has an update
method that can be used to give it a new state and set of props, causing its DOM structure to update to show the new state. ‘Props’ are pretty much what they are in React, a grab-bag of named parameters to configure the behavior of the component. They are a clever way to avoid having things like event handlers and keymaps in the editor state, by treating them as input parameters instead. The most important prop is onAction
, which the view calls every time something happens that should update the state. It is up to the code that created the view to define the exact way such actions are handled, but a minimal implementation looks like action => view.update(view.state.applyAction(action))
.
Other props are things like handleKeyDown
, handleSingleClick
, handleTextInput
(event handlers that allow you to customize the way the editor responds to certain events), spellcheck
, label
(configuration), onFocus
, onBlur
(event notification), and plugins
, an array of plugin objects that can expose their own props (the same set) to influence the way the view works.
This works well, on the whole, and I like how it’s allowed me to move even more things, such as keymap support and history tracking, out of the core. But I haven’t finished integrating asynchronous prompts yet, support for what used to be called marked ranges (decorating content) is gone entirely because I’m working on a different approach to that feature, and the docs are a mess right now. So it’ll be a while before all this is release-ready, especially since I’m leaving on a two-week holiday later this week.
I’m going to try to do an intermediate release tomorrow, based on 0.8.3. It’ll definitely include the change where DOM events are passed to click event handlers, as well as a rough version of table support. If there’s anything else you’d really like to see in there, let me know (if it’s big, I might not be able to get to it, though).
Feedback on this new design is also welcome (though please refrain from drawing panicked conclusions without first making sure you understand what’s going on).