Vue WYSIWYG Editor v-model, Events, and Two-Way Binding Explained
Posted on By Shamal Jayawardhana | Last updated on | In Editor,
Table of contents
- Key Takeaways
- Understanding Two-Way Binding in Vue
- Why WYSIWYG Editors Need Special Binding
- Setting Up Froala as a Vue WYSIWYG Editor
- Install the required packages
- Import the editor assets
- Register the Vue plugin
- Create your first editor
- Binding the Editor with v-model
- Understanding the Data Flow
- Listening to Editor Events
- Combining v-model with Editor Events
- Working with Reactive Data
- Resetting and Updating Editor Content
- Load an existing article
- Switch to another document
- Clear the editor
- Load a template
- Using the Editor Inside Vue Forms
- Common Mistakes Developers Make
- Manipulating the DOM directly
- Not using v-model
- Mutating HTML incorrectly
- Ignoring the editor lifecycle
- Updating state incorrectly
- Recreating the editor unnecessarily
- Binding large objects instead of HTML
- Performance Best Practices
- Avoid unnecessary re-renders
- Lazy load the editor
- Debounce autosave
- Keep event handlers lightweight
- Keep reactive state lightweight
- Consider large documents
- Why Froala Works Well with Vue Reactivity
- Conclusion
- FAQ
- How do I bind a Vue WYSIWYG editor using v-model?
- Why doesn't a rich text editor behave like a normal input?
- Can I use editor events together with v-model?
- How do I update editor content programmatically?
- Is Froala compatible with Vue 3?
Integrating a standard form field into Vue is simple: add v-model, connect it to reactive state, and Vue keeps the interface and data synchronized. A WYSIWYG editor is more complex because it manages formatted HTML, toolbar actions, pasted content, selection state, and other editing behavior within its own interface.
This guide explains how to keep a Vue WYSIWYG editor synchronized with application state using v-model, component events, and Vue 3’s Composition API. Using the Froala Vue Rich Text Editor as the implementation example, you will learn how two-way binding works, how to respond to editor events, and how to update content without relying on manual DOM manipulation.
Key Takeaways
- Use v-model to establish a single source of truth between your Vue application and the editor, ensuring changes stay synchronized in both directions.
- Combine v-model with editor events such as contentChanged, focus, and blur to implement features like autosave, validation, live previews, and draft management.
- Leverage Vue’s Composition API with ref() and reactive() to manage editor content alongside the rest of your application’s reactive state.
- Update the bound reactive variable instead of manipulating the DOM, allowing Vue and the editor to handle synchronization automatically.
- Follow performance best practices by debouncing expensive operations, keeping reactive state lightweight, and avoiding unnecessary re-renders.
- Choose a Vue-native editor that integrates seamlessly with Vue’s reactivity system, provides a comprehensive event API, and supports the customization needed for production-ready applications.
Understanding Two-Way Binding in Vue
Two-way binding keeps your application’s state and UI synchronized. When the user updates an input, the underlying data changes automatically. Likewise, when the data changes programmatically, the UI reflects the new value.
With standard form elements, Vue makes this easy using the v-model directive.
<script setup>
import { ref } from 'vue'
const title = ref('')
</script>
<template>
<input v-model="title" placeholder="Enter a title" />
<p>Current value: {{ title }}</p>
</template>
When the user types into the input:
Input ↓ v-model ↓ title (ref) ↓ Vue re-renders the UI
This works because v-model combines two operations:
- It passes the current value to the component.
- It listens for updates and writes them back to the reactive variable.
Without v-model, you would need to handle both steps yourself.
<input :value="title" @input="title = $event.target.value" />
Although both examples produce the same result, v-model is cleaner and easier to maintain.
Rich text editors are different from native inputs. Instead of editing plain text, they manage formatted HTML, toolbar interactions, and editing state internally. As a result, they require an integration that synchronizes the editor with Vue’s reactive data instead of relying on a standard <input> element.
Why WYSIWYG Editors Need Special Binding
Unlike a textarea, a WYSIWYG editor is a complete application running inside your Vue component. It maintains its own editing environment while Vue manages your application’s state.
For example, when a user clicks Bold, the editor doesn’t simply append text. It updates the HTML, preserves the cursor position, records an undo history entry, refreshes the toolbar, and re-renders the content.
User Action
↓
Editor Internal State
↓
Generated HTML
↓
Vue Reactive State
A rich text editor typically manages:
- The editable DOM
- HTML content
- Text formatting
- Toolbar state
- Cursor and selection
- Clipboard operations
- Undo/redo history
If the editor and Vue state become unsynchronized, problems quickly appear. For example, loading an existing article by updating a Vue variable won’t update the editor unless the integration propagates the new value. Likewise, changes made inside the editor won’t be available when the form is submitted unless they are synchronized back to Vue.
A Vue-compatible editor solves this by keeping both sides in sync automatically, allowing the editor to handle editing while Vue manages the application’s reactive state.
Setting Up Froala as a Vue WYSIWYG Editor
Froala provides an official Vue package that makes it straightforward to integrate a rich text editor into Vue 3 applications.
Install the required packages
Install the Froala editor and its Vue wrapper using your preferred package manager.
npm install froala-editor vue-froala-wysiwyg
Import the editor assets
Import the Froala JavaScript bundle to register the built-in editor plugins, then import the required styles.
import 'froala-editor/js/plugins.pkgd.min.js' import 'froala-editor/css/froala_editor.pkgd.min.css' import 'froala-editor/css/froala_style.min.css'
Register the Vue plugin
Register the Froala plugin in your application’s entry file.
import { createApp } from 'vue'
import App from './App.vue'
import VueFroala from 'vue-froala-wysiwyg'
createApp(App)
.use(VueFroala)
.mount('#app')
Once registered, the <froala> component is available throughout your application.
Create your first editor
Create a reactive variable to hold the editor content and bind it to the editor component.
<script setup>
import { ref } from 'vue'
const content = ref('<p>Hello, Vue!</p>')
</script>
<template>
<froala v-model:value="content" />
</template>
Running the application displays a fully functional rich text editor with the initial HTML stored in content. At this point, the editor is installed and ready to use. In the next section, you’ll learn how the v-model directive keeps the editor and Vue’s reactive state synchronized automatically.
Binding the Editor with v-model
The v-model directive connects the Froala editor to a reactive variable, creating a single source of truth for your editor content.
<froala v-model:value="content" />
Whenever the user edits the document, Vue automatically updates the content variable. Likewise, if you update content in your application, the editor immediately reflects the new value.
content (ref)
⇅
v-model
⇅
Froala Editor
For example, after loading an existing article from an API, simply assign the returned HTML to the reactive variable.
content.value = response.data.body
The editor immediately displays the updated content without requiring any additional synchronization code.
Because content is reactive, other parts of your application always receive the latest editor content.
<p>Content length: {{ content.length }}</p>
<button :disabled="!content">
Publish
</button>
As the user edits the document, the character count and button state update automatically because they depend on the same reactive variable.
This is the primary benefit of using v-model. Instead of manually reading HTML from the editor or writing content back into it, you work with a single reactive variable while Vue and Froala handle the synchronization for you. The next section explores how these updates propagate through the rest of your Vue application.
Understanding the Data Flow
When you bind Froala using v-model, the editor becomes part of Vue’s reactive data flow.
User Types
↓
Froala Editor
↓
v-model
↓
Vue State
↓
Other Components
↓
Editor Updates
Here’s what happens at each step:
- User Types
The user edits content inside the Froala editor. - Froala Editor
Froala updates its internal document model, toolbar state, cursor position, and generated HTML. - v-model
The updated HTML is automatically assigned to your reactive variable. - Vue State
Your ref() or reactive() object now contains the latest editor content. - Other Components
Any component using that reactive data is updated automatically. - Editor Updates
If the reactive value changes elsewhere (for example, after loading a draft), Vue updates the editor to display the new content.
The following example demonstrates this flow.
<script setup>
import { ref } from 'vue'
const content = ref('<p>Hello World!</p>')
</script>
<template>
<froala v-model:value="content" />
<h3>Preview</h3>
<div v-html="content"></div>
</template>
As the user types, the preview updates automatically because both the editor and preview share the same reactive variable.
Listening to Editor Events
While v-model synchronizes content, editor events let you react to user actions.
Some commonly used Froala events include:
| Event | Common Use |
|---|---|
| initialized | Run setup logic after the editor loads |
| contentChanged | Autosave, validation, live preview |
| focus | Highlight the active editor |
| blur | Validate or save content |
| keyup | Character or word counter |
| keydown | Keyboard shortcuts |
| paste.after | Sanitize pasted content |
| image.inserted | Track uploaded images |
| image.removed | Clean up deleted images |
For example, you can listen for content changes to trigger an autosave.
<template>
<froala
v-model:value="content"
:config="config"
/>
</template>
<script setup>
import { ref } from 'vue'
const content = ref('')
const config = {
events: {
contentChanged() {
console.log('Saving draft...')
}
}
}
</script>
You can also monitor when the editor gains or loses focus.
const config = {
events: {
focus() {
console.log('Editor focused')
},
blur() {
console.log('Editor lost focus')
}
}
}
Character counting is another common use case.
const config = {
events: {
keyup() {
console.log('Content updated')
}
}
}
Instead of polling for changes, your application responds immediately whenever an event occurs.
Combining v-model with Editor Events
v-model and editor events solve different problems.
- v-model stores the current editor content.
- Events notify your application when something happens.
In most real-world applications, you’ll use both together.
<script setup>
import { ref } from 'vue'
const content = ref('')
const hasUnsavedChanges = ref(false)
const config = {
events: {
contentChanged() {
hasUnsavedChanges.value = true
}
}
}
</script>
<template>
<froala
v-model:value="content"
:config="config"
/>
<button :disabled="!hasUnsavedChanges">
Save Draft
</button>
</template>
Here:
- v-model keeps content synchronized.
- contentChanged enables the Save Draft button.
- After saving successfully, you can simply reset the flag.
hasUnsavedChanges.value = false
The same approach works for many common features:
- Enable or disable the Publish button.
- Display a live preview.
- Show an “Unsaved changes” warning.
- Trigger autosave every few seconds.
- Enforce character or word limits.
A typical workflow looks like this:
User edits content
↓
contentChanged event
↓
Application logic runs
↓
v-model updates content
↓
UI refreshes automatically
Working with Reactive Data
Vue 3’s Composition API uses ref() and reactive() to create reactive state.
For editor content, ref() is usually the simplest choice.
<script setup>
import { ref } from 'vue'
const content = ref('')
</script>
Bind it directly to the editor.
<froala v-model:value="content" />
Whenever the user edits the document:
User types
↓
content.value updates
↓
Vue reactivity runs
↓
Dependent components update
If your editor belongs to a larger form, reactive() can group related fields together.
<script setup>
import { reactive } from 'vue'
const article = reactive({
title: '',
content: '',
category: ''
})
</script>
<template>
<input v-model="article.title" />
<froala v-model:value="article.content" />
</template>
Now the title, editor content, and category all belong to the same reactive object.
Resetting and Updating Editor Content
Since the editor is bound with v-model, updating the reactive variable automatically updates the editor.
Load an existing article
content.value = article.body
Switch to another document
content.value = selectedPost.content
Clear the editor
content.value = ''
Load a template
content.value = ` <h2>Meeting Notes</h2> <ul> <li></li> </ul>
There’s no need to call DOM APIs or manually replace the editor’s HTML. Simply update the reactive value, and Vue synchronizes the editor.
For best results:
- Always update the bound reactive variable.
- Avoid manually manipulating the editor’s DOM.
- Keep a single source of truth for editor content.
- Let v-model handle synchronization.
Using the Editor Inside Vue Forms
Rich text editors are often used alongside other form fields.
<script setup>
import { reactive } from 'vue'
const form = reactive({
title: '',
content: ''
})
</script>
<template>
<form>
<input
v-model="form.title"
placeholder="Article title"
/>
<froala v-model:value="form.content" />
</form>
</template>
Your form data now contains both the title and rich text content.
A typical submission looks like this:
async function submit() {
if (!form.content.trim()) {
alert('Content is required.')
return
}
await api.post('/articles', form)
}
The workflow is straightforward:
Editor
↓
Form Model
↓
Validation
↓
Submit
Because the editor is part of the same reactive form model, validation, submission, and API requests work exactly like any other Vue form field. This keeps your forms simpler while ensuring the editor content always stays synchronized with the rest of your application.
Get the complete Vue 3 and Froala example from GitHub to explore `v-model`, editor events, live preview, autosave, programmatic content updates, and form validation in a runnable project.
Common Mistakes Developers Make
Integrating a Vue WYSIWYG editor is straightforward once you understand Vue’s reactivity model. The following mistakes are common and can lead to synchronization issues or unnecessary complexity.
Manipulating the DOM directly
Avoid updating the editor by manually changing its HTML.
❌ Avoid
document.querySelector('.fr-element').innerHTML = html
✅ Use
content.value = html
Always update the reactive variable and let v-model synchronize the editor.
Not using v-model
Some developers manually read the editor content before submitting a form.
❌ Avoid
const html = editor.html.get()
✅ Use
<froala v-model:value="content" />
The latest HTML is always available in content.value.
Mutating HTML incorrectly
Avoid modifying the generated HTML using string operations.
❌ Avoid
content.value += '<p>New text</p>'
Instead, update the editor naturally through user interaction or replace the content with a complete HTML string when appropriate.
Ignoring the editor lifecycle
Some logic should only run after the editor has finished initializing.
const config = {
events: {
initialized() {
console.log('Editor is ready')
}
}
}
Waiting for the initialized event helps avoid timing issues.
Updating state incorrectly
Always modify the reactive variable instead of trying to synchronize multiple copies of the same data.
❌ Avoid
let editorContent = '' let html = ''
✅ Use
const content = ref('')
Maintain a single source of truth.
Recreating the editor unnecessarily
Avoid destroying and recreating the editor whenever content changes.
❌ Avoid
editor.destroy() editor.initialize()
Instead, simply update the bound value.
content.value = article.body
Binding large objects instead of HTML
The editor should be bound to the HTML content, not the entire article object.
❌ Avoid
const article = reactive({
title: '',
author: '',
content: '',
tags: []
})
<froala v-model:value="article" />
✅ Use
<froala v-model:value="article.content" />
This keeps updates focused and avoids unnecessary reactive work.
Performance Best Practices
Most applications won’t experience performance issues, but following a few best practices helps keep the editor responsive as projects grow.
Avoid unnecessary re-renders
Keep the editor mounted whenever possible. If only the content changes, update the reactive variable instead of recreating the component.
content.value = article.body
Lazy load the editor
If the editor appears only on certain pages, consider loading it only when needed.
const Editor = defineAsyncComponent(() =>
import('./Editor.vue')
)
This reduces your application’s initial bundle size.
Debounce autosave
Saving on every keystroke can generate unnecessary API requests.
import { debounce } from 'lodash-es'
const saveDraft = debounce(() => {
console.log('Saving...')
}, 1000)
Use the debounced function inside the contentChanged event.
Keep event handlers lightweight
Event handlers fire frequently, especially keyup and contentChanged.
Prefer:
contentChanged() {
saveDraft()
}
Avoid expensive calculations or multiple API calls inside every event.
Keep reactive state lightweight
Store only the editor’s HTML in the reactive variable.
const content = ref('')
Avoid storing editor instances or unrelated application data inside the same reactive value.
Consider large documents
Very large articles naturally require more processing.
For better performance:
- Update only the bound HTML.
- Debounce expensive operations.
- Avoid unnecessary watchers.
- Use editor events only where needed.
These small optimizations help maintain a smooth editing experience.
Why Froala Works Well with Vue Reactivity
Vue’s reactive data model works best with components that follow the same principles. Froala’s official Vue integration is designed to fit naturally into that workflow.
Some of its key capabilities include:
- Native Vue component integration
- Simple v-model support for two-way binding
- Comprehensive event system for responding to user actions
- Automatic synchronization with Vue’s reactive state
- Fast rendering for responsive editing
- Extensive configuration and toolbar customization
- A plugin ecosystem for adding features such as tables, images, markdown, file management, and more
Because the editor integrates directly with Vue’s reactivity system, developers can work with familiar Composition API patterns instead of manually synchronizing editor content. This results in cleaner code and makes it easier to build features such as autosave, live previews, form validation, and collaborative editing while keeping the application’s state consistent.
Conclusion
Vue’s v-model makes two-way binding simple by keeping application state and the user interface synchronized automatically. Rich text editors introduce additional complexity because they manage their own editing environment, making proper synchronization essential for a reliable editing experience.
By combining v-model with editor events, you can build responsive features such as autosave, live previews, validation, and draft management while keeping your code clean and maintainable. Using a Vue-native editor like Froala allows you to integrate rich text editing into modern Vue 3 applications without sacrificing the benefits of Vue’s reactive data model.
Ready to build a seamless editing experience? Try the Froala Vue Rich Text Editor and start integrating rich text editing into your Vue 3 applications today.
FAQ
How do I bind a Vue WYSIWYG editor using v-model?
Bind the editor component to a reactive variable using Vue’s v-model directive. As users edit the content, the editor automatically updates the reactive variable. Likewise, any programmatic changes to that variable are reflected in the editor, keeping both synchronized.
Why doesn’t a rich text editor behave like a normal input?
Unlike native inputs, a WYSIWYG editor manages its own editable DOM, formatting, toolbar interactions, cursor position, and undo/redo history. Because of this additional functionality, it requires an integration layer to synchronize its content with Vue’s reactive data model.
Can I use editor events together with v-model?
Yes. v-model keeps the editor’s content synchronized with your application state, while editor events let you respond to user actions such as content changes, focus, blur, keyboard input, image uploads, and paste operations. Using both together makes it easy to implement features like autosave, validation, and live previews.
How do I update editor content programmatically?
Simply update the reactive variable bound to the editor with v-model. Vue automatically propagates the new value to the editor, making it easy to load existing articles, switch between documents, clear the editor, or insert predefined templates.
Is Froala compatible with Vue 3?
Yes. Froala provides an official Vue integration that supports Vue 3, including seamless v-model binding, reactive state synchronization, and a comprehensive event API for building modern rich text editing experiences.
Shamal Jayawardhana
Shamal Jayawardhana is a seasoned web development expert and technical content strategist with a proven track record of helping developers and digital creators thrive. With over five years of hands-on experience, he has worked with leading SaaS brands to produce high-impact tutorials, WordPress guides, and developer-focused resources.



No comment yet, add your voice below!