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

v-model:content not working when changed the binding value #188

Closed
erhuabushuo opened this issue Mar 18, 2022 · 39 comments · Fixed by #317
Closed

v-model:content not working when changed the binding value #188

erhuabushuo opened this issue Mar 18, 2022 · 39 comments · Fixed by #317
Labels

Comments

@erhuabushuo
Copy link

erhuabushuo commented Mar 18, 2022

v-model:content not working when changed the binding value, it seems not two way binding,

@feature

@aydot
Copy link

aydot commented Mar 19, 2022

@erhuabushuo
Did you try it like so:
<QuillEditor v-model:content="myContent" contentType="html"/>

@thedomeffm
Copy link

thedomeffm commented Mar 19, 2022

Solved this problem like this (vue 3 with TS):

Updated 2022-11-12

<template>
    <div class="app-editor">
        <QuillEditor ref="editor"
                     v-model:content="value"
                     :options="options"
                     content-type="html" />
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';

export default defineComponent({
    components: {
        QuillEditor,
    },

    props: {
        modelValue: {
            type: String,
            required: false,
            default: null,
        },
    },

    emits: ['update:modelValue'],

    data() {
        return {
            // https://vueup.github.io/vue-quill/guide/options.html#option-attributes
            options: {
                // debug: 'info',
                toolbar: 'essential',
                placeholder: '...',
                theme: 'snow',
            },
            internalValue: null as string|null,
        };
    },

    computed: {
        value: {
            get(): string|null|undefined {
                return this.modelValue;
            },
            set(newValue: string | null) {
                this.internalValue = newValue;
                this.$emit('update:modelValue', newValue);
            },
        },
    },

    watch: {
        modelValue(newValue) {
            if (newValue === this.internalValue) {
                return;
            }

            this.$refs.editor.setHTML(this.modelValue);
        },
    },
});
</script>

@stale
Copy link

stale bot commented Jul 16, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix This will not be worked on label Jul 16, 2022
@vz-spr
Copy link

vz-spr commented Jul 21, 2022

Didn't work for me @thedomeffm , because cursor moved to the beginning for some reason when i started typing
The main problem was that after initialised with null or undefined I needed to update after fetch data from server.
So i found another robust solution which main idea is something like:

<script lang="ts" setup>
import Quill from 'quill'
import {onMounted, ref, watch} from 'vue'


const props = defineProps<{
    modelValue: string|null
    syncValue?: string|null
}>()
const emits = defineEmits(['update:modelValue'])
const div = ref(null)
let quill = null
onMounted(() => {
    quill = new Quill(div.value, {
        theme: 'snow'
    })

    quill.on('text-change', () => {
        console.log('--- text change', quill.root.innerHTML)
        emits('update:modelValue', quill.root.innerHTML)
    })
})
let _syncValue = props.syncValue
watch(props,  (newP) => {
    console.log('--- sync value, new and old', newP.syncValue, _syncValue)
    if (newP.syncValue === _syncValue) {
        return
    }
    let delta = quill.clipboard.convert(newP.syncValue)
    quill.setContents(delta, 'api')
    _syncValue = newP.syncValue
})
</script>

<template>
    <div class="quill-wrapper" ref="div"></div>
</template>

@stale stale bot removed the wontfix This will not be worked on label Jul 21, 2022
@carlos-mora
Copy link

This one is a significant issue, as it's spected to work as usual with v-models.
Changing underlying data is a regular practice. Please pay attention to this.

@stale
Copy link

stale bot commented Nov 11, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added wontfix This will not be worked on and removed wontfix This will not be worked on labels Nov 11, 2022
@thedomeffm
Copy link

As can be seen with Issue #275, people still don't understand the behaviour of the editor!
As a solution one should either explain the behaviour in the documentation or solve the reactivity in vue-quill!

@Jasonchang6435
Copy link

Jasonchang6435 commented Nov 16, 2022

<template>
  <div class="rich-text-wrap">
    <QuillEditor
      ref="edit"
      :content="html"
      content-type="html"
      @text-change="handleChange"
      :modules="modules"
      :toolbar="[
        [
          { header: 1 },
          {
            header: 2,
          },
          'bold',
          'italic',
          'underline',
          'strike',
          'blockquote',
          'code-block',
        ],
        [{ list: 'ordered' }, { list: 'bullet' }],
        ['image', 'link'],
      ]"
    />
  </div>
  <div class="text-len">{{ textLen }}</div>
</template>

<script setup lang="ts">
import { ref, watch } from "vue";
import { Delta, QuillEditor } from "@vueup/vue-quill";
import ImageUploader from "quill-image-uploader";
import useImageUpload from "@/compositions/useImageUpload";

const props = defineProps<{
  len: number;
  html: string;
}>();

const emit = defineEmits<{
  (e: "update:len", value: number): void;
  (e: "update:html", value: string): void;
}>();

const { uploadHandle, uploadUrl } = useImageUpload();

const textLen = ref<number>(0);
const edit = ref();
const modules = ref({
  name: "imageUploader",
  module: ImageUploader,
  options: {
    upload: (file: File) => {
      return new Promise((resolve, reject) => {
        const formData = new FormData();
        formData.append("image", file);
        uploadHandle(file, "text")
          .then(() => {
            return resolve(uploadUrl.value);
          })
          .catch(() => {
            return reject(new Error("upload image failed"));
          });
      });
    },
  },
});

const handleChange = () => {
    textLen.value = edit.value.getText().trim().length;
    emit("update:len", textLen.value);
    emit("update:html", edit.value.getHTML());
};
</script>

when use v-model and contentType is html , when props value changes and the editor did not show corerctly, and I try to use pasteHtml to set. the cursor act weirdly

@Tofandel
Copy link
Contributor

It's so trivial to fix the reactivity with an intermediate variable, I can make a PR

@cmora-pe
Copy link

Please!!!

@PDSAPWCV
Copy link

As common practice, changes on v-model are not reflected in the editor.

You can fix this problem by calling quilljs's internal API directly. (Use refs)
https://quilljs.com/docs/api/#setcontents

This solution is based on the assumption that contents are provided via props.

<template>
      <QuillEditor ref="quill" theme="snow" toolbar="essential" v-model:content="data.contents" content-type="delta"/>
</template>

<script>
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'

export default {
  name: 'editor-component',
  components: { QuillEditor },
  props: {
    data: {
      type: Object,
      default: null,
    },
  },
  data() {
    return {
      data: {
        id: null,
        contents: '',
      },
    }
  },
  watch: {
    data: {
      deep: true,
      handler(newData, prevData) {
        this.data = {
          id: newData.id,
          // contents: JSON.parse(newData.contents),
        }
        this.updateEditor(JSON.parse(newData.contents)) // delta json string from server
      },
    },
  },
  computed: {
  },
  methods: {
    updateEditor(delta) {
      this.$refs.quill.setContents(delta)
    },
  },
}
</script>

<style scoped lang="less">
</style>

@Tofandel
Copy link
Contributor

Tofandel commented Nov 29, 2022

As common practice, changes on v-model are not reflected in the editor.

Not common and not intended, props should be reactive, the common behavior of a v-model is to have 2 way binding, using refs is just a workaround (which btw did you try it? You're going to have infinite update issues like this with a watcher, because the watcher will trigger on v-model update and reupdate the content etc, you're also gonna have the cursor jumping to the begining of the editor everytime you type)

I made the PR to fix the bug already, it works very well and addresses all this, hopefully will be merged soon enough

@luthfimasruri
Copy link
Collaborator

🎉 This issue has been resolved in version 1.0.1 🎉

The release is available on:

Your semantic-release bot 📦🚀

@nathanchase
Copy link

There's still seemingly no way to wrap <QuillEditor> inside of your own custom component. I want to do something like the following, where you set a v-model on your custom component, and it bubbles up from QuillEditor when its value changes:

// RichTextEditor.vue
<template>
  <ClientOnly>
    <QuillEditor
      class="editor"
      :placeholder="placeholder"
      theme="bubble"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </ClientOnly>
</template>

<script setup lang="ts">
import '@vueup/vue-quill/dist/vue-quill.bubble.css';

defineProps({
  modelValue: {
    type: String,
    default: '',
    required: true,
  },

  placeholder: {
    type: String,
    default: '',
    required: false,
  },
});

defineEmits(['update:modelValue']);

if (!process.server) {
  const { QuillEditor } = await import('@vueup/vue-quill');
  const { vueApp } = useNuxtApp();
  if (!vueApp._context.components.QuillEditor) {
    vueApp.component('QuillEditor', QuillEditor);
  }
}
</script>
// OtherComponent.vue
<RichTextEditor v-model="myvalue" />

@Tofandel
Copy link
Contributor

Tofandel commented Jan 6, 2023

@nathanchase Yes there is, you just have the wrong prop name

Replace

:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"

with

:content="props.modelValue"
@update:content="$emit('update:modelValue', $event)"

You may also need to do const $emit = defineEmits(/*...*/)

@nathanchase
Copy link

nathanchase commented Jan 6, 2023

:content="modelValue" doesn't work for me, either. This was the way I had to do it to get it working (in Nuxt 3):

// RichTextEditor.client.vue
<template>
  <div>
    <div class="editor">
      <QuillEditor
        v-model:content="value"
        content-type="html"
        :options="defaultOptions"
        :placeholder="placeholder"
        theme="bubble"
        @update:content="handleChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import '@vueup/vue-quill/dist/vue-quill.bubble.css';
import 'quill-paste-smart';

const props = defineProps({
  modelValue: {
    type: String,
    default: '',
    required: false,
  },

  placeholder: {
    type: String,
    default: '',
    required: false,
  },
});

const emit = defineEmits(['update:modelValue']);

const defaultOptions = ref({
  theme: 'bubble',
  modules: {
    clipboard: {
      allowed: {
        tags: ['em', 'strong', 's', 'p', 'br', 'ul', 'ol', 'li', 'span'],
      },
      keepSelection: true,
    },
    toolbar: [
      ['bold', 'italic', 'strike'],
      ['blockquote'],
      [{ list: 'ordered' }, { list: 'bullet' }],
      ['clean'],
    ],
  },
});

const internalValue = ref();

const value = computed({
  get(): string | null | undefined {
    return props.modelValue;
  },

  set(newValue) {
    internalValue.value = newValue;
    emit('update:modelValue', newValue);
  },
});

if (!process.server) {
  const { QuillEditor } = await import('@vueup/vue-quill');
  const { vueApp } = useNuxtApp();
  if (!vueApp._context.components.QuillEditor) {
    vueApp.component('QuillEditor', QuillEditor);
  }
}

const handleChange = (value: any) => {
  emit('update:modelValue', value);
};
</script>
<LazyRichTextEditor
  v-model:modelValue="htmlValue"
  placeholder="Placeholder text"
/>

image

@Tofandel
Copy link
Contributor

Tofandel commented Jan 6, 2023

There is no reason you need to introduce an intermediate reactive variable, that's a lot of redundant code

@update:content is called when your content update so there is no need to introduce v-model in a wrapped component or you'll be causing double updates

Your mistake was not using $event instead of $event.target.value and props.modelValue because you're in a setup component

<template>
  <div>
    <div class="editor">
      <QuillEditor
        :content="props.modelValue"
        content-type="html"
        :options="defaultOptions"
        :placeholder="placeholder"
        theme="bubble"
        @update:content="handleChange"
      />
    </div>
  </div>
</template>
      
<script setup lang="ts">
import '@vueup/vue-quill/dist/vue-quill.bubble.css';
import 'quill-paste-smart';

const props = defineProps({
  modelValue: {
    type: String,
    default: '',
    required: false,
  },

  placeholder: {
    type: String,
    default: '',
    required: false,
  },
});

const emit = defineEmits(['update:modelValue']);

const defaultOptions = ref({
  theme: 'bubble',
  modules: {
    clipboard: {
      allowed: {
        tags: ['em', 'strong', 's', 'p', 'br', 'ul', 'ol', 'li', 'span'],
      },
      keepSelection: true,
    },
    toolbar: [
      ['bold', 'italic', 'strike'],
      ['blockquote'],
      [{ list: 'ordered' }, { list: 'bullet' }],
      ['clean'],
    ],
  },
});

if (!process.server) {
  const { QuillEditor } = await import('@vueup/vue-quill');
  const { vueApp } = useNuxtApp();
  if (!vueApp._context.components.QuillEditor) {
    vueApp.component('QuillEditor', QuillEditor);
  }
}

const handleChange = (value: any) => {
  emit('update:modelValue', value);
};
</script>

@nathanchase
Copy link

nathanchase commented Jan 6, 2023

@Tofandel Ok, this works and allows me to remove the computed property:

<QuillEditor
  :content="modelValue" // <- not 'props.modelValue', but just 'modelValue'
  content-type="html" // <- this HAS to be html... if it's not, it throws a recursion error
  :options="defaultOptions"
  :placeholder="placeholder"
  theme="bubble"
  @update:content="handleChange"
/>

Thanks for the help!

@cortnum
Copy link

cortnum commented Jan 25, 2023

@Tofandel thank you my brother!! I was about to abandon this component because the two-way binding was broken.

@adrianhand
Copy link

Would thank anyone for a little help here as I try to get two-way binding working with vue-quill. I understand from the docs that v-model:content is the prop I need to use for two way binding. Here is my vue3 composition template in its simplest form:

<template>
    <quill-editor theme="snow" toolbar="full" v-model:content="selectedNote" content-type="delta" />
</template>

<script>
import { ref } from 'vue'

import { Delta, QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'

export default {
    components: {
        QuillEditor,
    },
    setup() {
        let selectedNote = ref(new Delta())
        return { selectedNote }
    },
}
</script>

So my intent is to bind the editor contents 2-way with selectedNote - the above appears to work but generates the following error in console "[Vue warn]: Maximum recursive updates exceeded."

However - everything works as expected if I change my content-type to text or html. Is there a way to have error-free two-way binding whilst also using delta as my content type?

@Tofandel
Copy link
Contributor

Does the error still happens if you use ref(new Delta([]); ?

@adrianhand
Copy link

Good shout mate worth a try - have just done so though and the behaviour is the same. Have also noticed that every keypress moves the cursor back to the start of the editor window again.

Its not the end of the world if I have to use HTML, it just feels like using delta is a lot more elegant.

@Tofandel
Copy link
Contributor

As a workaround, try to create a delta with some content in it, there might be a bug in the previous PR for deltas as I'm not using it and the tests only cover a non empty delta, I'll investigate

@adrianhand
Copy link

adrianhand commented Jan 31, 2023

Okay Tofandel, thank you again - have just tried that, I took a snippet from the demo folder on the repo, so the delta I made looks like this:

new Delta([{ insert: 'Gandalf', attributes: { bold: true } }, { insert: ' the ' }, { insert: 'Grey', attributes: { color: '#ccc' } }])

And the result was the same - but it brought me to trying to run the demos from the repo - and indeed when I do that, they produce the same error result; they too are two-way binding a delta object with v-model:content. I checked a different browser just to be sure and the behaviour was the same on Chrome and on Safari.

@Tofandel
Copy link
Contributor

Tofandel commented Jan 31, 2023

I've found the issue, it was a small one, I'll make a follow up PR

@adrianhand
Copy link

Woo that’s mega, thank you Tofandel, giving your change a try 🙏

@adrianhand
Copy link

adrianhand commented Feb 1, 2023

Have pulled your repo + built mate, with mixed results.

Firstly, the errors are gone.

When you change the content of the quilleditor, changes are pushed back to the property as I'd expect.

However, though the quilleditor component always reflects the initial value of its bound property, if I subsequently change the value of the bound property with delta manipulation. the editor doesn't reflect the change, for example:

selectedNote.value.insert('Text', { bold: true, color: '#ccc' });

or indeed straight up replacing the delta entirely:

selectedNote.value = new Delta([])

The editor doesn't pick these up - its only two way binding at moment of initial population, after that its 1-way only.

Here is a minimal example to demonstrate, which changes the bound property to no effect:

<template>
    {{ selectedNote }}
    <button @click="changeModelValue">Change Model Value</button>
    <quill-editor theme="snow" toolbar="full" v-model:content="selectedNote" content-type="delta" />
</template>

<script>
import { ref } from 'vue'

import { Delta, QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'

export default {
    components: {
        QuillEditor,
    },
    setup() {
        let selectedNote = ref(new Delta([{ insert: 'Gandalf', attributes: { bold: true } }, { insert: ' the ' }, { insert: 'Grey', attributes: { color: '#ccc' } }]))

        function changeModelValue() {
            selectedNote.value = new Delta([{ insert: 'not reflected in editor', attributes: { bold: true } }])
        }

        return { selectedNote, changeModelValue }
    },
}
</script>

Would again appreciate your thoughts, thank you for bearing with!

@Tofandel
Copy link
Contributor

Tofandel commented Feb 1, 2023

@adrianhand Are you sure you're on the correct branch if you pulled the fork? Because the pen is exactly as in the demo, which works fine, but for the selectedNote.value.insert('Text', { bold: true, color: '#ccc' }); yes that's true it doesn't handle nested changes

@adrianhand
Copy link

Ahhhhh mate I am so so sorry, when I pulled I assumed I would default to tofandel/master - I am sure switching to the right branch will do the trick, I am ashamed. Will follow up with a result.

@Tofandel
Copy link
Contributor

Tofandel commented Feb 1, 2023

It is in fact master, I added deep reactivity support for delta, give it a pull and let me know

@adrianhand
Copy link

Okay bud, you were absolutely right - sorry again(!) - I was not using your master branch. When checkout correctly everything works as expected with error-free two-way binding between v-model:content and my objects.

However (!) whilst every text change is reflected in my object, some operations like setting text style aren't pushed through to the model object. If I subsequently make a text change, reactivity forces the binding to refresh and my text styles are there in the delta.

Its a really little thing and I don't know what I can reasonably expect here.

@Tofandel
Copy link
Contributor

Tofandel commented Feb 2, 2023

Indeed, gave it a try and also pushed a fix for that as well

@luthfimasruri
Copy link
Collaborator

🎉 This issue has been resolved in version 1.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@umneeq2
Copy link

umneeq2 commented Feb 16, 2023

Why use such a plugin that complicates the work? He just needs to link v-model and that's it.

@cortnum
Copy link

cortnum commented Mar 7, 2023

This still doesn't work as it should.

I have a case, where I have multiple text editors spawned in a for loop. And when I insert one in the middle with an empty model value (''), and the component which renders the editor updates, the texteditor still has the text from the previous model value inside. If I insert one in the middle, which HAS a model value (eg. '.'), it works as expected.

It seems there is a problem when the model value was set to an empty string, that doesn't update the text you see on the screen. But the content value of the editor updates just fine.

@Tofandel
Copy link
Contributor

Tofandel commented Mar 8, 2023

@cortnum Are you using 1.1.0? 1.0.1 had the issue you describe, which has been fixed

Please provide a reproduction using codepen or codesandbox

@cortnum
Copy link

cortnum commented Mar 8, 2023

@Tofandel yes I am. I will try to make one later today

@qiyongjin
Copy link

`<script setup name="QuillEditor">
import { defineProps, ref, watch, reactive, onMounted, toRaw } from 'vue';
import { QuillEditor, Quill } from "@vueup/vue-quill";
import BlotFormatter from "quill-blot-formatter";
import "@vueup/vue-quill/dist/vue-quill.snow.css";

if (!Quill.imports["modules/BlotFormatter"]) {
Quill.register("modules/BlotFormatter", BlotFormatter);
}

const props = defineProps({
textContent: {
type: String,
default: '',
}
});

// 定义content为响应式对象
const contents = ref(`
打上来看看

还是十六客服的

杀骷髅反对吧打死了几分

杀骷髅反对吧打死了几分

`);

const toolbar = ref([
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
["bold", "italic", "underline", "strike"],
[{ color: [] }, { background: [] }],
["clean"],
["image"],
["link"],
["video"],
]);
watch(() => props.textContent, (newVal) => {
contents.value = newVal;
}, {
immediate: true
});
const options = reactive({
modules: {
BlotFormatter,
toolbar: toolbar.value
},
placeholder: '請輸入內容...',
theme: 'snow',
theme:"bubble",
default:contents.value,
contentType: "delta" | "html" | "text",
enable: true,
readOnly: false,
});

const myQuillEditor = ref(null)

</script> ` The data cannot be rendered, and when the component passes in the data, it seems to have been rendered to the editor, but the editor automatically replaces all of them with empty labels

@Tofandel
Copy link
Contributor

contentType: "delta" | "html" | "text",

You need to choose one type, you can't pass multiple like this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.