tiptap: AlpineJS v3 - Range Error: Applying a mismatched transaction

Description I got Range Error: Applying a mismatched transaction when clicking on a menu button with AlpineJS v3.

CodeSandbox I created a CodeSandbox to help you debug the issue: https://codesandbox.io/s/tiptap-v2-alpinejs-v3-s5x2o

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 5
  • Comments: 33 (5 by maintainers)

Commits related to this issue

Most upvoted comments

Wanted to share my solution for having the editor not reactive, but still have a reactive menu for Tiptap/Alpine v3: https://codesandbox.io/s/tiptap-with-alpine-js-v3-q4qbp

Thanks @EasterPeanut your solution is very helpful. But since I need to have multiple editor instances, so I ended up with this implementation:

import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'

export default (content) => {
    let editors = window.tiptapEditors || {}

    return {
        id: null,
        content: content,
        updatedAt: Date.now(),

        init() {
            this.id = this.$el.getAttribute('id')

            editors[this.id] = new Editor({
                element: this.$refs.element,
                extensions: [
                    StarterKit,
                ],
                content: this.content,
                onUpdate: ({ editor }) => {
                    this.content = editor.getHTML()
                },
                onSelectionUpdate: () => {
                    this.updatedAt = Date.now()
                },
            })

            window.tiptapEditors = editors
        },

        editor() {
            return editors[this.id]
        },

        toggleHeading(level) {
            this.editor().chain().toggleHeading({ level }).focus().run()
        },

        toggleBold() {
            this.editor().chain().toggleBold().focus().run()
        },

        toggleItalic() {
            this.editor().chain().toggleItalic().focus().run()
        },
    }
}

The editor instances are saved to window as an object, the key is using the element id. Whenever I need the editor instance I only need to call this.editor() method.

If you add Alpine to the window scope you can use Alpine.raw(editor) to unwrap the proxy and it should work.

Just spotted this, and can hopefully point in the right direction, Alpine.js uses the reactive engine from Vue.js 3. Fixing it will be a very similar to what I did to get Vue3 working way back last year… https://github.com/ueberdosis/tiptap/issues/1166

From memory the trick with Vue was to use a ShallowRef() to hold the Editor object, this stops the reactive engine from trying to track all properties and methods within TipTap. (we then did a bunch of stuff to make the toolbars reactive but my memory gives up at that point)

It doesn’t look like Alpine exposes the ShallowRef() api, but that will defiantly be the starting point to getting it working while having the Editor be in your x-data.

(as others have mentioned having your editor object outside of x-data is a good workaround, but obviously not ideal)

Using the demos above as a reference, instead of using this.editor, just use window.editor = new Editor (or name it however you like) and access it the same way. There’s no need to make it a reactive property, and that seems to be what’s breaking things here.

That’s probably the more logical approach than my previous suggestion to unwrap the reactive parts on demand (with Alpine.raw()).

As some people described already this is a Proxy issue with Alpine. Alpine is using Proxy to have reactive objects - which is why this error is occuring.

image

Here you can see that a Proxy is sent to applyTransaction - since this is not the correct way to apply a transaction prosemirror is throwing this error.

Don’t know a solution for now, just so people know more about why and where this is happening.

Related to this error, I’m also experiencing a “Alpine Expression Error: editor is not defined” caused by a “Uncaught ReferenceError: editor is not defined”.

Test project: I followed the instructions in the doc and set up the example with Alpine 3. The basic editor worked BUT as soon as I added BubbleMenu or FloatingMenu it breaks again and Alpine can’t find the reference to the “editor” field anymore.

The placeholder extension worked though.

2 workarounds I found:

  1. Downgraden Alpine to v2 (2.8.2) and it worked again. Code stayed the same.
  2. Initialize the Editor outside of Alpine and set it as window.editor and never add a reference as an Alpine field/data. Then in an Alpine triggered method, using window.editor and calling methods on the editor worked just fine.

Thank you @EasterPeanut for the solution. Does anybody knows why in @EasterPeanut implementation buttons are not activated when are pressed but only when you press and type your first character (similarly when you deactivate)?

Updated:

Implementing the onTransaction function solved my issue:

 onTransaction: () => {
      this.updatedAt = Date.now()
  },

Sorry! I must’ve completely skipped over your comment 😂 Did you figure out a solution to checking if a button isActive?

I have buttons attribute in my Alpine component. With this attribute, not only I can give a different set of buttons for each editors but also I can use this to hold the buttons state and update the state by listening to onUpdate and onTransaction events.

In the html you can just loop the button collection and access the active state like:

<template x-for="button in buttons" :key="button.name" hidden>
    <button type="button" :class="{ 'bg-gray-50': button.active }" x-on:click.prevent="clickButton(button.name)">
        <span x-text="button.label"></span>
    </button>
</template>

That’s really smart! I’ll give it a shot.

Not sure if there’s a correct way to listen for a value changing with Alpine but this is how my buttons look:


    buttons: [
      {
        active: false,
        isActive: () => window.editor?.isActive('bold'),
        name: 'bold',
        onClick: () => {
          window.editor!.chain().toggleBold().focus().run();
        },,
      },
    ],

Then in the editor:

        onTransaction: ({ transaction }) => {
          this.buttons.forEach(button => {
            button.active = button.isActive() as boolean;
          });
        },

@creativiii yes, exactly what I did #1515 (comment) but you might want to put the editor in a collection instead so you access multiple editor instances in case you have multiple editors in a single page.

Sorry! I must’ve completely skipped over your comment 😂 Did you figure out a solution to checking if a button isActive?

I have buttons attribute in my Alpine component. With this attribute, not only I can give a different set of buttons for each editors but also I can use this to hold the buttons state and update the state by listening to onUpdate and onTransaction events.

In the html you can just loop the button collection and access the active state like:

<template x-for="button in buttons" :key="button.name" hidden>
    <button type="button" :class="{ 'bg-gray-50': button.active }" x-on:click.prevent="clickButton(button.name)">
        <span x-text="button.label"></span>
    </button>
</template>

@hanspagel the reactivity system in Alpine v3 is extracted from Vue’s reactivity system, so basically a trimmed down version only exposing the reactive function, will need to take a look at Vue’s integration to see if I can be of any help

@KevinBatdorf Thanks for the suggestion! I never knew about Alpine.raw()

@robertdrakedennis Here’s an example with the mentioned solution. https://codesandbox.io/s/tiptap-v2-alpinejs-v3-forked-2bbw8?file=/index.js

However, Alpine.raw() doesn’t appear to be bindable for reactivity. I also am Alpine novice. Would be interested to know if anyone has any good suggestions as it would be kind of a hassle to create a separate attribute for every button we want to check activeness on.

It appears to be definitely a proxy issue. I sort of got something working here by storing the editor object in a closure. https://codesandbox.io/s/tiptap-v2-alpinejs-v3-forked-6qxfs?file=/index.js

I don’t know why reactivity is not working but it’s a start and maybe this can point you in the right direction.

Hi, I think this is an Alpine issue. Downgrading to Alpine 2.8.2 works. It might have something to do with the fact that this.editor is a Proxy object - though it’s just a theory.

I don’t know if the getUnobservedData() still works, but you could try to use it to get access to the actual editor object. https://codewithhugo.com/alpinejs-inspect-component-data-from-js/

Hope this info helps 😄