ueberdosis / tiptap

The headless rich text editor framework for web artisans.
https://tiptap.dev
MIT License
27.58k stars 2.29k forks source link

AlpineJS v3 - Range Error: Applying a mismatched transaction #1515

Closed reefki closed 1 year ago

reefki commented 3 years ago

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

jelib3an commented 3 years ago

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 :smile:

olivsinz commented 3 years ago

Thanks but getUnobservedData() is no longer available.

olivsinz commented 3 years ago

@reefki did you find a way?

jelib3an commented 3 years ago

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.

olivsinz commented 3 years ago

@jelib3an Thanks a lot.

KevinBatdorf commented 3 years ago

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

robertdrakedennis commented 3 years ago

@KevinBatdorf apologies with pinging you, can you explain your solution more? I'm a bit of a novice when it comes down further into the technical stuff and would really like to understand how to alleviate this issue with your solution. Thanks!

jelib3an commented 3 years ago

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

hanspagel commented 3 years ago

Just wanted to say that I have no experience with Alpine, but I’d love to update the docs when we found a solution. ✌️ Thanks for everyone helping out.

KevinBatdorf commented 3 years ago

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

EasterPeanut commented 3 years ago

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

haubie commented 3 years ago

Thanks @EasterPeanut your codesandbox and also phoenix project were very helpful in migrating to Alpine 3!

Mushr0000m commented 3 years ago

Yes thanks @EasterPeanut it helped ! Would be nice to have a solution without changing the variable scope like in VueJs and other though…

reefki commented 3 years ago

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.

aspyropoulos commented 3 years ago

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()
  },
francois-soapbox commented 3 years ago

@aspyropoulos Thanks!

Atem18 commented 2 years ago

Maybe docs should be edited no ?

galaczi commented 2 years ago

Wow I didn't expect to go down this rabbit hole when I opened the Tiptap setup page. Honestly it looks much easier to just use vanilla JS. It's like a five liner.

hanspagel commented 2 years ago

Maybe we can build an alpine plugin?

As I said, I have nearly zero experience with Alpine. If someone could send a PR with improvements to the doc page, that would be amazing!

iksaku commented 2 years ago

@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

RobertCordes commented 2 years ago

Did anybody find a solution yet? I’m in the middle of upgrading a huge app to Alpine v3 and this is the only thing that’s holding me back. I've been working on this for days but I just can't figure out how to solve this issue. Maybe @calebporzio can give us a hint? 😇 Using Tiptap with Alpine v2 and Livewire is working flawlessly.

gmlewis commented 2 years ago

I'm using my own custom EmitterProvider (based loosely on y-websocket.js) to talk to emitter.io and am not using AlpineJS at all, but also get this error when upgrading from @tiptap/vue3@2.0.0-beta.2 to @tiptap/vue3@2.0.0-beta.91:

Uncaught RangeError: Applying a mismatched transaction
    at EditorState.applyInner (index.es.js:863:33)
    at EditorState.applyTransaction (index.es.js:831:1)
    at EditorState.apply (index.es.js:807:1)
    at Editor.dispatchTransaction (tiptap-core.esm.js:3457:1)
    at EditorView.dispatch (index.es.js:5285:29)
    at sync-plugin.js:401:1
    at ProsemirrorBinding.mux (mutex.js:35:1)
    at ProsemirrorBinding._typeChanged (sync-plugin.js:384:1)
    at Module.callAll (function.js:19:1)
    at callEventHandlerListeners (yjs.mjs:1863:12)

I just wanted to add another data point here while I keep trying to debug it in case others run into the same issue.

azarai commented 2 years ago

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.
francoism90 commented 2 years ago

@azarai Could you please share your (full) solution? :)

bdbch commented 2 years ago

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.

samwillis commented 2 years ago

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)

leo-petrucci commented 2 years ago

I'm not 100% this is a solution but it seems to work in my case? (I need to do more testing).

So you define your editor outside Alpine:

window.editor = null;

Then, within the main x-data function we initialise the editor:

const data= (content: Content = '') => {
    async init(element: Element) {
      window.editor = new Editor({
            // stuff in here obviously
      });
}

Then we create a new function so we can access the editor from within our data function:

const data= (content: Content = '') => {
    async init(element: Element) {
      window.editor = new Editor({
            // stuff in here obviously
      });
    },
    editor: () => {
      return window.editor;
    },
}

Then from your html you can call this:

              <button
                data-text="Bold"
                @click="editor().chain().toggleBold().focus().run()"
                :class="{ 'is-active': editor().isActive('bold') }"
              >
                Bold
              </button>

The difference with the above is that instead of calling editor.chain() we call editor().chain().

As far as I can tell almost everything here works except for :class="{ 'is-active': editor().isActive('bold') }".

EDIT:

I thought this may have been a better solution, but the error is still there :/

    get editor() {
      return window.editor;
    },
reefki commented 2 years ago

@creativiii yes, exactly what I did https://github.com/ueberdosis/tiptap/issues/1515#issuecomment-937327322 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.

leo-petrucci commented 2 years ago

@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?

EDIT: On second thought this works just as well?

  <button
    x-data="{ active: false }"
    data-text="Bold"
    @click="() => {
      editor().chain().toggleBold().focus().run();
      active = editor().isActive('bold');
    }"
    :class="{ 'is-active': active }"
  >
    Bold
  </button>
reefki commented 2 years ago

@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>
leo-petrucci commented 2 years ago

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;
          });
        },
github-actions[bot] commented 2 years ago

This issue is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 7 days

bdbch commented 1 year ago

Since @peterfox added a good note about this to our PHP documentation for Alpine I'd close this issue for now. Hope everyone is happy with that decision. Feel free to reopen if there is still something open to discuss.

dipeshmurmu2005 commented 5 months ago

You can get the easiest integration of tip with laravel livewire you can view blog: https://medium.com/@dipeshmurmu/effortless-integration-harnessing-tiptap-editor-with-laravel-livewire-b0fc9fe4d5f1