Tiptap 2 beta is here

After 9 months of work it’s finally here: The first public beta version of tiptap v2.

Here are some of the biggest changes:

Here are some of the nicest changes:

  • Commands are chainable. With that you can combine multiple steps into one single transaction.
    editor
      .chain()
      .focus()
      .setMark('bold')
      .insertContent('some text')
      .command(({ tr }) => {
        // do something with the current transaction
        return true
      })
      .run()
    
  • Support for text align. This is not an easy topic but we found a nice way to integrate it into tiptap. With a feature called global attributes it’s possible to inject attributes to existing nodes. We also had to create a custom enter handler and custom splitBlock and splitListItem commands for it. You can see a live demo here.
  • Render nodes as react components or vue components
  • Show suggestions for things like mentions or slash commands

Would really love to hear your thoughts! :raised_hands:

7 Likes

Nice, looks great :slight_smile: ! I’ve been taking a sneak peak at your new API and decided to use a similar approach myself. Basically just turning PM plugins into React components that have their own props instead of one massive EditorProps object.

So having dabbled myself mostly with React+PM integration, I’ll ask the basic ones that are on my mind - how do components subscribe to the global editor state? Observables? Also, do you intend to allow the use of some central event message bus, a la Redux?

And by looking into your ReactNodeView - it seems you have avoided the use of ReactDOM completely? No portals nothing? I myself found out that you could do a hook based approach with portals that might the most optimal, instead of looping through them. But haven’t really tinkered with it much lately.

One thing I’ve been thinking about is how to manage the state fetched from the API. For example collaboration participants - what’s your take on that? I guess you could just wrap it around an extension but then you’d also need to pass that data around in a context of sorts. With appropriate event dispatching/listening logic to allow components to do and react to changes.

Also, I have a tricky question that’s been on my mind. What’s your approach for rolling out breaking schema changes? I’ve started to wonder do you actually need to add in a migration logic or just support two approaches or forcefully migrate all the docs in the database to the new schema. Of course making breaking changes is probably something you should avoid to the end but when that happens, you want to have some way of doing it without breaking everything for the users.

One thing I’d like to see, for Tiptap and PM in general, is good testing support with jsdom. Seems pretty important considering how complicated the editors can get.

期待中…

Glad to see you reached beta. Congratulations! Looking great.

how do components subscribe to the global editor state? Observables? Also, do you intend to allow the use of some central event message bus, a la Redux?

What do you mean by global editor state? State from the editor or from your react app?

And by looking into your ReactNodeView - it seems you have avoided the use of ReactDOM completely? No portals nothing?

I’m using portals! You can see it here.

One thing I’ve been thinking about is how to manage the state fetched from the API. For example collaboration participants - what’s your take on that?

Have you seen our collab example? We provide first class support with Y.js collab. There is also a guide for it.

Also, I have a tricky question that’s been on my mind. What’s your approach for rolling out breaking schema changes?

We do not currently provide anything for this. However, I find it best to migrate the JSON data manually on the server side.

One thing I’d like to see, for Tiptap and PM in general, is good testing support with jsdom. Seems pretty important considering how complicated the editors can get.

I haven’t had good experiences with JSDOM, so we run all tests with Cypress. Works great! Example

Hey, thanks for the detailed reply. Yeah I meant the whole app: editor plugin state, extension state, UI state, API state and so forth. It seems that to manage the whole thing you need message passing of some sorts, which you seem to provide as well with editor pub/sub events, but I was wondering if you had some interesting new angle to it.

Oh, I guess I looked into the wrong files, whops. But cool! There is a hook-way of doing that too but haven’t checked if it’s performance-wise any better.

Yeah I read your collab docs - very impressive. It’s definitely a PITA to implement with the bare collab module. Yet I’m still a bit ambivalent about Yjs, like is it easy to switch between single and multiplayer editing and how much memory it actually consumes in the server.

But what I was getting at that do you implement the API providers as extensions or as separate services. Not a huge difference but something I myself am pondering. I guess having one entity for everything makes things simple.

Hmm okay, good to know. Not an easy task that either.

Oh Cypress, neat huh. Yeah it’s a bit awkward to polyfill stuff with jsdom. Only upside I assume is that it’s faster to run.

Are there plans to implement server-side rendering (SSR) given the ability to render nodes in Vue and React? I noticed there’s https://github.com/ueberdosis/tiptap/blob/main/packages/html which uses GitHub - holtwick/hostic-dom: Lightweight virtual DOM. Generates HTML and XML. Supports CSS selectors. Supports JSX. Parses HTML. Web Scraping. Post process page content. to render HTML server side, but it uses ProseMirror’s DOMSerializer which uses the NodeSpec toDOM method and not the CustomNodeView.

Hey, thanks for the detailed reply. Yeah I meant the whole app: editor plugin state, extension state, UI state, API state and so forth. It seems that to manage the whole thing you need message passing of some sorts, which you seem to provide as well with editor pub/sub events, but I was wondering if you had some interesting new angle to it.

I have to admit that this was my first React project. But each component gets some props about the editor. If you need more data from outside, you can do it with useContext, right?

Yeah I read your collab docs - very impressive. It’s definitely a PITA to implement with the bare collab module. Yet I’m still a bit ambivalent about Yjs, like is it easy to switch between single and multiplayer editing and how much memory it actually consumes in the server.

We didn’t find any memory issues at the server side. It’s pretty stable. Someone posted tiptap at HackerNews a few days ago and we had about 100+ users in a single collab document.

We’ll also provide a monitor package (WIP) for hocuspocus.

Oh Cypress, neat huh. Yeah it’s a bit awkward to polyfill stuff with jsdom. Only upside I assume is that it’s faster to run.

With cypress you can write end-to-end tests, integration tests and unit tests so this is not an issue for us. It’s just more powerful and flexible.

I have that in mind but no exact schedule yet. So far, it has not been asked for, which is why I have considered it as not urgent.

Congratulations on the beta! It looks very impressive. My biggest issue that I see with it is that there are no stylesheets that I can find. Sure the developer has full design freedom, but let’s face it, that is always the case.

The issue is that most developers suck at making the GUI pretty. When you have a great tool that is ugly, you just lost a large part of potential developers that do not have access to designers.

It would be great to provide great stylesheets to make TipTap also look great. You would be hands down the de facto standard in this category.

This is not planned now. If you want a drop-in solution you better check Quill, TinyMCE, CKEditor, Redactor etc.

I am not looking for a drop-in solution, but I am not excited to spend weeks reinventing the wheel just to get started with something that looks professional. That is why I am not using TipTap for our global homeschool co-op infrastructure. We will keep using http://imperavi.com until your solution is more polished.

Keep up the great work.

Looks really cool! The chain-able commands are a really nice touch.

Have you all considered how difficult it would be to show plugin decorations (like in mention example) in a collaborative environment? For example, being able to show someone exactly how mentions work without need to screen share. I would guess that you’d have to make all interactions a “command” which can be broadcasted to other users through Yjs-like primitives. WDYT?

Thanks! Yeah chainable commands are a like a superpower in ProseMirror! There is also a great way to check if you can chain something at all:

// check single command
editor.can().toggleList()
// check chain
editor.can().chain().toggleList().lift().run()

Have you all considered how difficult it would be to show plugin decorations (like in mention example) in a collaborative environment? For example, being able to show someone exactly how mentions work without need to screen share.

Do you mean to show other users the mention popup? This is not a decoration in our solution.

But decorations are indeed a bit of a challenge with Y.js. You don’t see a difference if you render your decorations on every document change. But if you want to cache/map your decorations you have to write some more logic because Y.js replaces to whole document for other users changes. This sounds dramatic but actually works quite well because most things can be re-used (like node views) :+1:

1 Like

Whoa! This is amazing and right on time and exactly what I was starting to kinda build myself. Thanks for saving me a ton of work and enable me to get to the fun stuff way sooner. Probably. Will give it a thorough evaluation on the weekend but on a first glance it seems to tick a lot of the boxes I need. I heard of tiptap before, but I thought it was still bound to vue, which I don’t use. It makes me genuinely happy that it’s now framework agnostic, good job!

One quick question though: Does it have some kind of markdown hybrid view? Eg Headlines, links etc styled as such, but keeping the markdown syntax visible?

Well in theory that’s all you need and useContext does work for providing state but. React doesn’t have as nice built-in state management as Vue and useContext can get quite tedious to work with. Moreover, the nature of PM’s event loop combined with the handling collaboration events and whatnot does seem the steer the whole architecture towards more event-based state management.

As one example let’s consider updating a component based on a plugin state change. So the simplest way is to make one context that has the editorState which you can replace in the dispatchTransaction call. Yet the component would update on every transaction and you’d have to check inside the component to see if the plugin state has changed. So you want to slice the state to make the updates as minimal as possible but it’s not very convenient with pure useContext. I myself made some hacky pub/sub pattern where I trigger a provider in the plugin’s apply function whenever plugin state changes but I definitely have to refactor it when I have the time. Curvenote put everything into Redux which seems nice but I don’t want to deal with its boilerplate.

To be frank it may be even impossible to add in a similar state management into an editor framework versus making your custom editor. So it’s a bit tricky.

That sounds very positive. Yet I still want to benchmark what’s the difference between using normal transactions versus Yjs, especially for single-player editing. For heavy collaboration I believe Yjs is indeed very good fit but in my own use case, I expect collaboration to be somewhat of an extra feature and I would like to keep its complexity to minimum. Having more control over the payloads could also allow some optimizations on the transferred data to reduce its size (like done by CKEditor).

1 Like

Why would the component update on every transaction. I was able to create a very simple hook for https://bangle.dev that reads a plugin state (src code) , it is working great for me and doesn’t utilize the event pub sub pattern.

Because EditorState is recreated on every transaction thus it would always update if the component was subscribed to changes to it. From what I can tell from your code, you are doing precisely what I said you have to do which is to slice the state somehow to the components. And sure, you’re not using pub/sub per say although one could think of editor transactions that change the plugin state as published events. But any state that would not be inside plugin state and still be relevant, eg API/UI stuff, you would have to engineer your own method of triggering changes somehow and relaying them forward. Using React context for that purpose sucks.