Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow v-model #133

Closed
1dnmr opened this issue Dec 13, 2018 · 27 comments
Closed

Allow v-model #133

1dnmr opened this issue Dec 13, 2018 · 27 comments
Labels
Type: Feature The issue or pullrequest is a new feature

Comments

@1dnmr
Copy link

1dnmr commented Dec 13, 2018

Please allow using of v-model in editor

@holtwick
Copy link

holtwick commented Dec 15, 2018

This is probably better done one level higher. Lets say you have a component setting up the TipTap editor. There in the Editor setup do:

editor = new Editor({   
    // ...
    onUpdate: ({getJSON}) => {
      const state = getJSON()
      this.$emit('input', state)  
// ...
editor.setContent(this.value)

More at https://vuejs.org/v2/guide/components.html#Using-v-model-on-Components

@1dnmr
Copy link
Author

1dnmr commented Dec 16, 2018

Hi Dirk,

Thank you for your answer. Anyway my idea was make the editor more user-friendly and following the best Vue component building practice. v-model is a one of these practices. Making component that not supported this out of the box is quite weird.

@holtwick
Copy link

I see. This is certainly a good idea following the best practice.

While working with TipTap I came to the point to decide in which format the editor state should be stored. Instead of serializing to HTML in my case it was better to wrap it into an object like this;

{ api: 1, state: {... } }

api would should be incremented in case I need to change the schema or other related stuff. Using a top level object is also good in case I also need to store selections or additional info like comments. All this is then serialized to JSON.

Therefore I would love to see a dual implementation: 1. The current Editor approach and 2. the v-model one.

BTW, keep up the great work. This is an awesome project!

@philippkuehn philippkuehn added the Type: Feature The issue or pullrequest is a new feature label Dec 20, 2018
@motatoes
Copy link

@holtwick's solution worked well for me. Ofcourse I had to create a prop called value inside the component. But I needed to add a watcher for the value prop in order to account for data changes in the parent component (for example, I was fetching data with Ajax and setting the parent value):

  props: ['value'],
  data: {
  },
  watch: {
    value (newValue) {
      this.editor.setContent(this.value)
    }
  }


@laurensiusadi
Copy link

laurensiusadi commented Apr 13, 2019

EDIT: There's a better solution #133 (comment)

Here's how I did it.
This is my Editor component.

props: [ 'value' ],
data() {
  return {
    editor: null,
  }
},
mounted() {
  this.editor = new Editor({
    extensions: [ ],
    content: this.value,
    onUpdate: ({ getHTML }) => {
      this.$emit('input', getHTML())
    },
  })
  this.editor.setContent(this.value)
},
beforeDestroy() {
  if (this.editor) {
    this.editor.destroy()
  }
},
watch: {
  value (val) {
    // so cursor doesn't jump to start on typing
   if (this.editor && val !== this.value) {
      this.editor.setContent(val, true)
    }
  }
}

I use it on its parent like this

<editor v-model="note.content"/>

Works fine for me.

PS: might have some issue with performance (on 100,000 characters or more)

@afwn90cj93201nixr2e1re

This comment was marked as spam.

@laurensiusadi
Copy link

laurensiusadi commented Jul 25, 2019

Bad solution. Double call when you gonna type something, watch gonna be triggered, your check's is terrible. if (val !== this.editor.getHTML()) { try to itterate on 1 million symbols.

Any suggestions how I can improve that?
How about if (this.editor && val !== this.value)
Since I already emit input, it will get new value

@afwn90cj93201nixr2e1re

This comment was marked as spam.

@estani
Copy link

estani commented Aug 26, 2019

@laurensiusadi
That didn't work for me an makes little sense imo. The problem is that the editor changes the value too, via $emit, so you have just tell apart what was the source of the change, and not the change itself.
what I did was to mark the editor change:

  props: ["value"],
  data() {
    return {
      editor: null,
      editorChange: false
    };
  },
  mounted() {
    this.editor = new Editor({
      onUpdate: ({ getHTML }) => {
        this.editorChange = true;
        this.$emit("input", getHTML());
      },
      content: this.value,
      extensions: [/* ... */]
    });
  },
  beforeDestroy() {
    if (this.editor) this.editor.destroy();
  },
  watch: {
    value(val) {
      if (this.editor && !this.editorChange) {
        this.editor.setContent(val, true);
      }
      this.editorChange = false;
    }
  }

Not sure if there's a better way, but this works with vuex

@BrianHung
Copy link
Contributor

Is there a way to use v-model on a custom Node instead of entire editor? For example, with title.js, can I bind the content within ['h1', {id: 'title'}, 0] with document.title? I'm a bit new to Vue and TipTap.

@Ravikc
Copy link

Ravikc commented Oct 28, 2019

I just came across this library and I must say that it's amazing. I was just going through all the issues to see if I can use it in my project and this issues seems very critical to me. I just wanted to confirm if there is anyone working on this issue. I could start looking into it if no one else is already working on it.

Once again, thanks for this awesome editor.

@laurensiusadi
Copy link

@Ravikc I'm not sure if this is a big issue, since it works okay for me now. But if you want to start looking into it, that would be awesome!

@rameezcubet
Copy link

https://www.npmjs.com/package/tiptap-vuetify worked this out

@laurensiusadi
Copy link

laurensiusadi commented Nov 27, 2019

I'm gonna rewrite the code from tiptap-vuetify here in JS (they're in TS)

  props: ["value"],
  data() {
    return {
      editor: null,
      emitAfterOnUpdate: false
    };
  },
  mounted() {
    this.editor = new Editor({
      onUpdate: ({ getHTML }) => {
        this.emitAfterOnUpdate = true;
        this.$emit("input", getHTML());
      },
      content: this.value,
      extensions: [/* ... */]
    });
  },
  beforeDestroy() {
    if (this.editor) this.editor.destroy();
  },
  watch: {
    value(val) {
      if (this.emitAfterOnUpdate) {
        this.emitAfterOnUpdate = false
        return
      }
      if (this.editor) this.editor.setContent(val)
    }
  }

Pretty similar to @estani code
So I just copied that code above and replace the variables from https://github.com/iliyaZelenko/tiptap-vuetify/blob/master/src/components/TiptapVuetify.vue

Haven't test this yet

EDIT: This works best for me

@A-d-o-n-i-s
Copy link

this.editor.setContent(val, true);

You must set true to false to avoid re-emitting editor.onUpdate()... and burning CPU cycles needlessly, just like the tiptap-vuteify code does. And thanks for posting that one! It works well.

@alloself
Copy link

With that i get problem with carret position lag,anyone get same?

or how to resolve it better?

@hanspagel
Copy link
Contributor

Thanks for sharing the solutions! ❤️

I’m closing this here. I think we’ll provide support for it in tiptap 2 (or provide a wrapper component).

@arifikhsan
Copy link

to use custom editor wrapper like this, write this in the parent component

 <custom-editor :value="detail" v-on:input="detail = $event" />

detail as props in value

@laurensiusadi
Copy link

laurensiusadi commented Nov 23, 2020

@alloself
Here's my final editor for now. By far the best implementation for my use case.

Note: This works with v-model but needs an id to be watched instead of value itself.
We need to add watch on id for setContent, or it won't change the editor's value when you load a different content without destroying the component, since the editor gets its content on mounted.

How to use:

<editor
  :key="id"
  v-model="noteContent"
/>

Editor component:

  props: ["value"],
  data() {
    return {
      editor: null
    };
  },
  mounted() {
    this.editor = new Editor({
      content: this.value,
      onUpdate: ({ getHTML }) => {
        this.$emit("input", getHTML());
      },
      extensions: [/* ... */]
    });
  },
  beforeDestroy() {
    if (this.editor) this.editor.destroy();
  }

I removed setContent on watch value, so cursor doesn't jump when we're typing because of content change. There's no need to get update from value, since you already updating the editor content while typing. The onUpdate function will keep firing while typing, then the parent component will keep saving the input to API with 500ms debounce.

Note: This won't work 2 ways (real-time) since you only emit data and not watching to receive and update editor content.

@bcorcoran
Copy link

@laurensiusadi

You can also just use the key attribute on your component, that way when the editor opens it will detect a different key and force an update.

@laurensiusadi
Copy link

@laurensiusadi

You can also just use the key attribute on your component, that way when the editor opens it will detect a different key and force an update.

I should've done that! Thanks!

@Niaz-estefaie
Copy link

I'm getting getHTML is not a function error constantly by this way

@hanspagel
Copy link
Contributor

Are you using tiptap 1 or 2? Here is an example for tiptap v2: https://www.tiptap.dev/installation/vue2#5-use-v-model-optional

@Niaz-estefaie
Copy link

@hanspagel I'm using tiptap 2
Thank you

@jloganolson
Copy link

jloganolson commented May 20, 2021

Realized editor is a ref re "getHTML is not a function error" - here is the code that works for me:

export default {
  name: "Editor",
  components: {
    EditorContent,
  },
  setup() {
    const store = useStore();
    const editor = useEditor({
      content: store.state.testString,
      extensions: [Document, Action, Character, Text, Commands, Keymap],
      onUpdate: ({ editor }) => store.commit("setTestString", editor.getHTML()),
    });
    const modelValue = computed(() => {
      return store.state.testString;
    });
    watch(modelValue, (value) => {
      const isSame = editor.value.getHTML() === value;

      if (isSame) {
        return;
      }

      editor.value.commands.setContent(value, false);
    });

    return { editor };
  },
};

(deleted original post of code that didn't work)

@Niaz-estefaie
Copy link

Niaz-estefaie commented May 26, 2021

That worked for me like this

export default {
  name: "Editor",
 
components: {
   EditoerContent    
 },

mounted() {
    this.editor = new Editor({
        extensions: [
            Document,
            CustomParagraph,
            Text,
            Mention.configure({
                HTMLAttributes: {
                    class: "mention"
                },
                    render: () => {
                        let component;
                        let popup;
  
                        return {
                            onStart: props => {
                                component = new VueRenderer(MentionList, {
                                    parent: this,
                                    propsData: props
                                });
  
                                popup = tippy("body", {
                                    getReferenceClientRect:
                                        props.clientRect,
                                    appendTo: () => document.body,
                                    content: component.element,
                                    showOnCreate: true,
                                    interactive: true,
                                    trigger: "manual",
                                    placement: "bottom-start"
                                });
                            },
                            onUpdate(props) {
                                component.updateProps(props);
  
                                popup[0].setProps({
                                    getReferenceClientRect: props.clientRect
                                });
                            },
                            onKeyDown(props) {
                                return component.ref?.onKeyDown(props);
                            },
                            onExit() {
                                popup[0].destroy();
                                component.destroy();
                            }
                        };
                    }
                }
            })
        ],
        onUpdate() {
            // send the content to an API here
            self.text = this.getJSON();
        }
    });
 }
}

@JahnoelRondon
Copy link

JahnoelRondon commented Jun 26, 2023

ok now how do you do this in react...

Edit: Never mind I found out how this solution works for react and if the code is the same should work for vue. The white spaces are set to false by default in the setContent options so you just need to turn them on by doing either true or "full" for all white spacing.

   onUpdate: ({editor}) => {
       const cleanText = profanity.clean(editor.getHTML());
       editor.commands.setContent(cleanText, false, {preserveWhitespace: "full"});
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature The issue or pullrequest is a new feature
Projects
None yet
Development

No branches or pull requests