ueberdosis / tiptap

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

[Bug]: Svelte component doesn't accept content changes #4918

Open benbucksch opened 9 months ago

benbucksch commented 9 months ago

Which packages did you experience the bug in?

tiptap/core in svelte

What Tiptap version are you using?

2.2.2

What’s the bug you are facing?

There is no official Svelte component for TipTap.

The documentation has only sample code for a Svelte component. However, that sample code has the HTML content hardcoded. Obviously, you'd make this a component property html. You'd also need to get the content that the user wrote out of it - that's the whole point of the editor. That's the bug here: This should be part of the sample code.

If we try to add that functionality, that gives us:

/** The editor content that the user writes.
 * in/out */
export let html: string;
...
      content: html,
      onTransaction: () => {
        // force re-render so `editor.isActive` works as expected
        editor = editor
      },
      onUpdate: () => {
        html = editor.getHTML();
      },

However, this code has a bug: If the html property is changed by the parent component, then the changes do not apply. This is because the code content: html, runs onMount() only.

This is needed not only when parents wants to specifically change the content, but mor importantly also when we simply change records / data. When we merely change record/data and only the property value changes, Svelte re-uses existing components and does not re-create them, so onMount doesn't run, so html is not sent to the editor, so the changes are ignored, so the editor shows the wrong data of the previous record.

Straight-forward fixes in standard Svelte way, like:

$: editor && editor.commands.setContent(html);

do not work, due to TipTap having its own mind. When I do this, I can no longer type spaces, or rather they are stripped, because onUpdate sets html, which then triggers setContent(html) again, which for some unknown reason strips the spaces.

Given how Svelte re-uses components with new property data, this is the most basic usage of the TipTap editor in Svelte, and every developer will need to solve this. This should be part of the sample code Svelte component.

What browser are you using?

Firefox

Code example

https://tiptap.dev/docs/editor/installation/svelte

What did you expect to happen?

When html changes, the editor content reflects that new data.

Anything to add? (optional)

No response

Did you update your dependencies?

Are you sponsoring us?

mhanbali commented 5 months ago

Hey Ben,

I ran into something similar last night and created a solution for my specific need. I know it's not a complete solution, but in case it fits anyone else's problem.

Summary: I created two exported functions - one to insert text, used by a button. Another to bind the editor contents to another text field to mirror input. The latter I don't need/use, but just wrote real quick as a test.

In your TipTap editor component, create/export two functions outside of onMount:

// this will be called by the parent component in a button
export function insertText(text: string) {
    editor?.commands.insertContent(text);
}

// this will be used by a textarea in the parent with on:input
// note: this will not account for formatting such as new lines
export function changeContent(text: string) {
    editor?.commands.setContent(text);
}

Next, in your parent component, likely where you are calling the <Editor /> (or whatever you name it) component, create two variables:

$: changeText = ''; this is to bind/update the text

let component: Editor; this is to bind to the actual component. Replace "Editor" with whatever your import name is. My import of the editor looks like this: import Editor from '$lib/components/Editor.svelte';

If you aren't using TypeScript, you can probably just do let component;

Now, bind your component to use the exported functions. <Editor bind:this={component} />

Create a textarea/input. I'll use it to get the text I want to insert in this example:

<textarea
    rows="10"
    bind:value={changeText}
></textarea>

In a button you can call insertText: <button on:click={() => component.insertText(changeText)}>Insert Text</button>

And that's it.

If you want to bind the textarea to the Editor component, you can add this to your <textarea> on:input={() => component.insertText(changeText)}

Note: as said before, it won't keep formatting like new lines the way it's currently written.

benbucksch commented 5 months ago

it won't keep formatting like new lines the way it's currently written.

Right. My first solution had the exact same problem, see initial description: "setContent(html) again, which for some unknown reason strips the spaces."

I need the formatting and new lines, so this solution didn't work. Obviously, an HTML editor where I cannot get the content out nor not the formatting, is kind-of pointless. That's why I filed this bug here.

mhanbali commented 5 months ago

Try updating the function to the below. It looks to keep the new lines:

export function changeContent(text: string) {
  editor?.commands.clearContent();
  editor?.commands.insertContent(text);
}

update: further testing it even accepts html like <b>Bold</b> in the plain textarea and reflects correctly in the editor. Still not a complete 2-way solution. Though if you need to bind 1-way it's a start.