Prosemirror-Svelte

Hello everyone,

first of all, I want to emphasize what a delight Prosemirror has turned out to be. With previous experience in DraftJS, it took a moment to switch gears to Prosemirror, but once that initial period was over, I was blown away. Big thanks to Marijn for creating this!

Having this out of the way, I wanted to give something back.

I recently (well, a few months back) migrated a major project of mine from React to Svelte (which I also fell in love with). So there I was, in need of a new text-editor and decided to create Prosemirror bindings for Svelte.

While it’s not really hard to do that, it took a few rounds to make it work just right (mainly because I wanted to build something that embraces both the reactive nature of Svelte as well as the transaction based philosophy of Prosemirror).

The result is now available on Github and npm (MIT). A demo is available on surge.sh.

Hope it helps someone. I would love to get your feedback. :smiley:

6 Likes

:+1:

I checked the repo and tried the demo. IMO, the helper methods are not convenient enough. It relies on editing/managing EditorState directly if one wants to create a Node and insert it somewhere, while people would like to do by UI in a WYSIWYG editor.

Maybe you can take a look at tiptap. It is vue-based, but the design is worth of reference.

Hi,

first of all many thanks for the feedback!

I actually thought about writing something more high-level at first (also for our internal needs at Faden). Especially when I was still new to Prosemirror. But it always felt like imposing another layer of abstraction on the existing way Prosemirror does things. The result here is really more like a gentle marriage of concepts from Svelte and Prosemirror. Especially using editor state felt important to me since this provides full access to both the document as well as selection. If you use the on:transaction event, you even get access to the view. The idea is that you could create your UI fully in Svelte but also be able to use plugins (one application - which we use internally - is to dispatch custom events from within a prosemirror plugin - e.g. when the selection enters or leaves a certain text - and listen to them on the Svelte component).

But I fully understand that it can be a bit daunting at the beginning to create a custom schema and all. I already thought about adding additional helper methods.

I added two convenience wrappers to serialize/deserialize state and included them in the rich-text editing example. I’ll also have a look at TipTap.

Thanks for publishing this!

Maybe you could add such a demo. It would help illustrate the usage. And it could guide people to understand your design better.

P.S. I am new to prosemirror, too. Prosemirror is really powerful and has so many concepts. It is quite difficult to understand and master it. Tiptap reorganize some concepts into one place at code level, which helps in some extent. But

So I am looking forward to a more-svelte way. And I will keep an eye on your work. :grinning:

I had a closer look at TipTap. It’s certainly an interesting approach.

Nevertheless, I prefer to keep the current direction of using editorState directly - and also using the existing plugin and transaction system. This way, updates and bugfixes to Prosemirror should mostly be automatically inherited by the Svelte bindings. Also, it encourages newcomers to get more familiar with the underlying architecture (and access to the full potential of its flexibility).

At the same time, I fully understand that not everybody wants to learn the whole thing just to get started or just to implement a basic editor. So, since I had some spare time today, I wrote up several new helpers (and moved several of them into a new sub-namespace). Also added more examples at http://prosemirror-svelte.surge.sh/ (and a basic navigation to keep things neat and tidy).

One additional thing: I created a Svelte REPL at https://svelte.dev/repl/db7359c0bd1e443e8ebdc2d4bdc1f571?version=3 where you can play with some of the concepts. Happy hacking!

And Happy New Year! :smiley:

I agree with your point of view. And I am glad to see more examples. If you could show how to implement all the examples on ProseMirror examples, it would even better.

@christianheine: There is a tiptap-svelte project BTW. Might be worth looking at.

@justinmoon Thanks. I didn’t know that one.

The feedback in this thread made me curious. In which way is Tip-Tap more approachable than Prosemirror itself?

I personally like the way how Prosemirror handles things - with the one notable exception that I needed a a prop-driven interface (so ui=f(state), i.e. how Svelte and other frameworks like React see the world). Well, and to support this concept, some helpers to quickly whip up an editor state or create a new one from modifications (like setting a node type or toggling mark type), since the existing prosemirror-commands work with a dispatch transaction driven API. I was probably influenced by DraftJS which I used before (before ditching it - along with React - for Svelte and Prosemirror. Never regretted this for a second by the way).

But I digress. Also @jiewuza: I would love to pick your brains here: What are the major challenges you faced with Prosemirror and how do you feel that TipTap solved this “better” (for the lack of a better word) than the existing Prosemirror API?

@christianheine Yea it took me a while to find that one, too.

My situation: I need to write an editor which can do the following:

  • Apply bold, italic, and link formatting (ideally inline markdown syntax + wsywyg someday)
  • Insert images (ideally with button, drag-and-drop, and paste functionality someday)

But I need a basic MVP before I start on that more advanced stuff.

Slate.js or TipTap look perfect but I’m using Svelte so those aren’t an options.

Prosemirror seems like next best option, but it’s a little low level for me. It being so low-level is a positive long term because I know that I’ll be able to implement almost any feature I can imagine given enough time. But my immediate problem is building an MVP and ProseMirror seems a little intimidating so far on that front – it doesn’t really seem to work out-of-the-box. But if you continued adding examples to your project I think that would give me more confidence.

Honestly I don’t know which approach is better, the only advice I can give you is that your current direction really needs the examples to demonstrate the the integration is manageable.

@justinmoon Got it. Thanks for the constructive feedback.

Prosemirror is rather low-level, that much I have to admit. But I can assure you that once you start to dig into how the Schema, EditorState and Transactions work, the rest is really downhill.

Adding images is not that hard either.

  1. Make sure they are defined in the schema (given in the rich-text schema of prosemirror-svelte, since it’s currently the basic-schema provided by Prosemirror)
  2. Create an image node (available as a method on the schema)
  3. Create a transaction (from the current editor state)
  4. Apply the transaction to the current editor state to get a new editor state

I added a helper to do just that and published this as a minor version.

const insertImage = (editorState, attrs, from = null, to = null) => {

  if (from === null) from = editorState.selection.anchor;
  if (to === null) to = editorState.selection.head;

  const imageNode = editorState.schema.nodes.image.create(attrs);

  const transaction = editorState.tr;
  transaction.replaceWith(from, to, imageNode);

  return editorState.apply(transaction);
};

Also added this as an example (#6). Here as an interactive REPL: https://svelte.dev/repl/caaed73f2cc74de39de94ba3b05eba25?version=3.16.7

You might also want to check out example #3 to see how to apply marks (bold/italic) or node types (paragraph/ heading).

And, feel free to open an issue in the repo for anything else. I cannot promise everything will be added, but then we can keep track.

I think @justinmoon just said my mind.

IMO, Tiptap has a clear design of linking toolbar/command/node together. And it provides many demos, which is eloquent and really helps.

@christianheine yep, @jiewuza is right - what is love in tiptap from the first point - is that it was pretty straightforward fot me to get started - Replaced toolbar from tiptap with my visuals and then just reused commands it has to make a basic editor. When i started digging to smth custom - yep it gets the same complexity if you have to think about schema, etc. But having more examples helps with this. Its difficult to learn new paradigms when you dont have time. So the less the better. Best is using svelte/vue type of coding everywhere possible and only think about schema sometimes.

@christianheine Nice! It looks neat. I have dabbled myself with integrating React to PM nodeviews which is quite the same as this. Recently I have had some trouble with figuring out how to inject the state to the nodeview components: should I use separate stores? Mobx or Redux? Only plugin state? If so, how to slice it to the components?

In the end I came to the same conclusion as you, that the state should reside inside the plugin-states and manipulated using Redux-type action/dispatcher type messaging to not to fight against the internal PM logic (and to avoid duplicating the state). However, it’s a bit problematic to then serve the plugin states to the components as you can’t just use the regular connect(mapStateToProps, mapDispatchToProps, Component) Redux approach. I got stuck trying to customize this HOC with an efficient way of doing something along the lines of:


const pluginKey = pluginStateSlice[key]

pluginStateSlice.getState(editorView.state)

Didn’t even get to the point of thinking about debouncing the updates to the components which you have apparently added. I thought about using a plugin which at the start of every plugin transactions adds a meta-flag to the transactions eg tr.setMeta('_update', 'UPDATE') which every transactions that happens in response to this transactions delays with eg tr.setMeta('_update', 'DELAY'). When the value goes unmodified till the end, another plugin then adds a final meta-flag eg tr.setMeta('_update', 'FLUSH') that flushes the changes to the React components. It’s a bit iffy still but that was one approach I thought of.

It would be easy of course if you can assume that all your transactions were independent, thus no such hacks would be needed. But since I’m not sure of the way how the updating of the React-based nodeViews (or the plugin-state in general) happens, a similar user-defined flushing would be the only way to ensure that updates were to happen only once. This way if you by some reason had a plugin that appended something to other plugin’s transaction and those plugin’s state were showed in same React nodeView, there would be no intermediate updates between them. But yep, this is for sure boilerplatish as hell.

Anyway, it’s nice to see something else figuring out the same problems as I have =). Yet I have to mention that your example doesn’t really solve the main issues with creating a true PM-Svelte integrated editor. Mainly what you need is a robust API for describing the custom PM-Svelte plugins with the boilerplate to combine the plugins with the state, event-dispatcher and whatnot. So far I have gathered those custom plugins should at least include:

  • nodeview(s)

  • schema (or more specifically SchemaSpec as Schema is already instantiated and it can’t be combined with other Schemas from other plugins)

  • PM plugins (eg keymaps & the plugin itself)

  • possible styles/features to PM as a whole eg a gutter with line-numbers or a toolbar icon

If you have taken a look into Atlassian’s atlaskit you might get the idea what I mean. It is a bit complicated but well, what can you do. TipTap would be a lot simpler and probably easier to use, but if you want to leverage as much as PM API as possible without inventing too many weird abstractions, I guess this is a valid approach.

I myself did the switch from Slate to PM partly because I felt its API was kinda bad (at least now they have removed ImmutableJS) and for what I wanted to do, which is quite low-level manipulation, it simply couldn’t do it fast enough. And I’m quite interested in Svelte as well, but yeah there’s enough stuff on my plate with this React-PM integration already :slight_smile: .

Thanks for the feedback :slight_smile:

Yeah, bridging frameworks (especially those like React with a virtual DOM) is usually a PITA.

Regarding some of the points you mentioned: I am still contemplating a decent API for the library to keep as much as possible out of the way of Prosemirror, but also helping newcomers to its API to get things done quickly. Someone just opened an issue on the repo asking about hashtags - and despite really trying to give a simple answer there are at least 4-5 moving pieces (parsing the editor state around the current selection/ rendering a popup / handling key events/ modifying the editor state on select - potentially with a dedicated entity) that need to be coordinated.

So probably the best way would be to provide guidance how to create “svelte-enhanced” plugins - and maybe offer a few default plugins by default.

Recently I have been toying around a lot around the edges with Svelte, essentially how far you can go to do things the way Svelte suggests before things start to get unstable. Svelte is delightfully robust and predictable here (ok, it required a few hours debugging the compiled bundle, but nonetheless). Also, I found that it’s relatively straightforward to create Svelte components from “non-Svelte” code (here’s a very basic example with prosemirror-svelte).

Will try to allocate some time on this later this week.

P.S.: I tried both Slate and Draft.js while I was still in React-land. Initially decided to go with Draft.js (because Slate was still in beta and also because I got some really weird performance issues during development. Not to mention that it rendered what looked like the Himalaya mountains when inspecting it in the Chrome performance tools). But (despite being on the agenda for ages) Draft.js still does not properly support mobile entry (same for Slate - try IME like Chinese PinYin…). So Prosemirror certainly has been a delight in terms of stability and browser support.

Hi @christianheine !

I’m integrating ProseMirror with Svelte on a project I’m working on.

I’m not sure if I missed it in this thread… but did you find a way to use Svelte components as nodes in a ProseMirror document?

I’m thinking it could be done by using SSR component modules and using the render() method to generate the DOM string for ProseMirror in the toDOM method.

See the SSR docs for Svelte.

With Rollup you can generate multiple configs and then export some .svelte files two times. Once for the SSR module, and then once for the client-side interactive module to be used when rendering the rich text ProseMirror document (when not editing).

See this Rollup config for a Svelte SSR demo I did.

Does this make sense?

Hey there,

Well, eventually I never really needed the full flexibility of having Svelte components as nodes in the document. I did however want to use the interactivity of Svelte components for things like popups which are triggered from Prosemirror. Mostly because I had a Svelte store which I wanted to use as a data source for the component. The exact use case was hashtag and mention support. So I wrote a plugin which identified whether the cursor was inside a hashtag and then conditionally rendered the Svelte component (in fact, I wrote a whole library on top of what I previously published as ProsemirrorSvelte, but it’s not straightforward to use yet and I don’t have the time to polish the API and publish the whole thing as open source right now :slightly_frowning_face:. But it’s certainly on my agenda for Q3/2020).

Anyways. This should give you an idea about the component rendering:

view(editorView: EditorView) {
  const state = pluginKey.getState(editorView.state);

  // ... more code

  const instance = new options.Component({ // </-- Here the Svelte component instance is created
    target: editorView.dom.parentNode,
    props: getProps(editorView, state)
  });

  return {
    update(editorView) {
      const state = pluginKey.getState(editorView.state);
      instance.$set(getProps(editorView, state)); // </-- Here the Svelte component instance is updated
    },
    destroy() {
      const state = pluginKey.getState(editorView.state);
      state.destroy(); // cleanup (especially subscriptions)
    }
  };
}

If you’re wondering: TagState is simply a dedicated class to handle the plugin state.

state: {
  init(_, editorState: EditorState) {
    return new TagState(options, editorState, pluginKey);
  },
  apply(tr, tagState: TagState, oldState: EditorState, editorState: EditorState) {
    return tagState.updateState(editorState);
  }
},

To keep it flexible, I am providing the Svelte component when the plugin state is initialized:

import TagSelector from "./TagSelector.svelte"; // normal import. Will be handled by Rollup later

// ...more code

const options: TagSelectorPluginsOptions = {
  // ...more options
  // @ts-ignore
  Component: TagSelector,
}

So essentially, the concept is to use the Svelte client component API (which you usually only use once to render the Svelte root component to also render components elsewhere). What I realized in the meantime is that, in the end, Svelte just issues “createElement”, etc. calls. There’s nothing “magical” like Reacts virtual DOM. If you want a deeper dive into Svelte’s inner working and how components are rendered: https://stackoverflow.com/questions/58902385/sveltejs-vs-reactjs-rendering-difference-repaint-reflow/59485936#59485936

I know this is not exactly what you were asking, but I hope it provides some food for thought. Let me know if something is unclear.

1 Like

Thanks a lot @christianheine I will digest this and let you know if I have some questions!