vue-quill
vue-quill copied to clipboard
v-model:content not working when changed the binding value
v-model:content not working when changed the binding value, it seems not two way binding,
@feature
@erhuabushuo
Did you try it like so:
<QuillEditor v-model:content="myContent" contentType="html"/>
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>
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.
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>
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.
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.
As can be seen with Issue https://github.com/vueup/vue-quill/issues/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!
<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
It's so trivial to fix the reactivity with an intermediate variable, I can make a PR
Please!!!
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>
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
:tada: This issue has been resolved in version 1.0.1 :tada:
The release is available on:
Your semantic-release bot :package::rocket:
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" />
@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(/*...*/)
: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"
/>
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>
@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!
@Tofandel thank you my brother!! I was about to abandon this component because the two-way binding was broken.
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?
Does the error still happens if you use ref(new Delta([]);
?
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.
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
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.
I've found the issue, it was a small one, I'll make a follow up PR
Woo that’s mega, thank you Tofandel, giving your change a try 🙏
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!
@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
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.
It is in fact master, I added deep reactivity support for delta, give it a pull and let me know