What I'm working on

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).

2 Likes

That sounds great, with this linear branch. One planned thing at my ProseMirror Integration is to use the menubar from the main app, which is in my case done with react / redux and has a materiel styled UI. I guess (or hope, at least) it might get easier to do such UI integrations with the upcoming linear ProseMirror (just from looking at the sources I think it got already easier or even possible at all without patching, with the 0.8 release). @marijn, have a nice holiday.

Thanks for the update! Moving to an architecture with a simpler data flow sounds really good. I’ve had good experiences with Reat and Redux, but I’ll have to look at the code to get a better idea of how this will look in ProseMirror.

Thanks for doing an intermediate relase. I’m happy that there will be a rough version of tables as this is currently our highest priority. Besides that it would be nice to have a solution for some of the unexpected copy/paste behaviors, but I’m not sure how big this is: Pasting removes list item · Issue #397 · ProseMirror/prosemirror · GitHub.

Enjoy your vacation! :wink:

HA! Was sorta hoping the quiet was because you were on vacation :slight_smile: glad to hear you will be soon!

I’m currently using VueJS and may go down the path of using its implementation of redux, so I think this in general is a good move to align with many of the new age flux apps. Having a coherent place for state and events and allowing us to tap in to certain events to do something special or delegate to PM should allow PM to be very extendable while not boosting complexity to get up and running.

I too have this integration pain point. I want to completely manage my own divs and menu. I’d like to be able to tell PM to attach to an existing div as appose to PM creating a new div and a wrapper. Currently I have a modified PM constructor to this affect.

I am very confused about why this would be necessary. The menus are already outside of the core, and to build your own menu, you can either do it from scratch, or use the primitives in menu.js to do it in a few lines of code.

Probably not needed for menus, but for allowing me to setup the divs and have PM attach to an existing div.