Get Started for FREE

Vue WYSIWYG Editor v-model, Events, and Two-Way Binding Explained

Vue WYSIWYG Editor v-model Two-Way Binding Illustration

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:

  1. User Types
    The user edits content inside the Froala editor.
  2. Froala Editor
    Froala updates its internal document model, toolbar state, cursor position, and generated HTML.
  3. v-model
    The updated HTML is automatically assigned to your reactive variable.
  4. Vue State
    Your ref() or reactive() object now contains the latest editor content.
  5. Other Components
    Any component using that reactive data is updated automatically.
  6. 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:

EventCommon Use
initializedRun setup logic after the editor loads
contentChangedAutosave, validation, live preview
focusHighlight the active editor
blurValidate or save content
keyupCharacter or word counter
keydownKeyboard shortcuts
paste.afterSanitize pasted content
image.insertedTrack uploaded images
image.removedClean 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.

 

Posted on August 5, 2026

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!


Add a Comment

Your email address will not be published. Required fields are marked *