Wasting $14K Building Editors? Use Odoo 18 WYSIWYG in OWL Components
By Braincuber Team
Published on December 22, 2025
Developer needs rich text editor for custom dashboard. First attempt: Build from scratch. 47 hours later: Has basic formatting (bold, italic, underline). No tables. No images. No embedded content. Client asks: "Can we add bullet lists?" Developer: Rebuild entire editor logic. Another 23 hours. Client: "Actually, we need image uploads too." Developer realizes: Just spent 70 hours building what Odoo's WYSIWYG editor does out-of-box.
Your custom component problems: Need HTML editor outside standard form view. Options: (1) Integrate third-party library (TinyMCE, CKEditor) = licensing costs $399/year + complex integration + security risks. (2) Build custom editor = 70+ hours development + ongoing maintenance + missing features. (3) Copy-paste code from Stack Overflow = XSS vulnerabilities + breaks on Odoo upgrades. None good.
Cost: Developer spends 70 hours building custom rich text editor = $7,000 (at $100/hour). Third-party license: $399/year × 5 users = $1,995/year. Integration time: 12 hours = $1,200. Security patch when XSS vulnerability found: 8 hours emergency fix = $1,600. Maintenance: 3 hours quarterly updates = $1,200/year. Plus opportunity cost: Could've built 3 revenue-generating features instead.
Odoo 18 WYSIWYG Editor integration fixes this: Use Odoo's built-in rich text editor in custom OWL components. Zero licensing fees. Full feature set (formatting, tables, images, embedded content). Security maintained by Odoo. Automatic updates. Takes 2 hours to integrate (not 70). Here's how to embed WYSIWYG editor in custom dashboards so you stop wasting $13,995 building what already exists.
You're Wasting Time If:
What Odoo WYSIWYG Editor Provides
Full-featured rich text editor with enterprise capabilities:
Built-in Features:
- Text Formatting: Bold, italic, underline, strikethrough, colors, fonts
- Structure: Headings, paragraphs, bullet lists, numbered lists, checklists
- Tables: Insert, resize, merge cells, styling
- Media: Image upload, video embed, file attachments
- Embedded Components: Odoo views, charts, products
- Links: Hyperlinks, anchors, email links
- Code: Code blocks with syntax highlighting
- Security: XSS prevention, HTML sanitization built-in
Use Case: Custom Dashboard with Rich Text Editor
Scenario: Build custom dashboard where users can toggle between viewing and editing HTML content.
User Experience:
- User opens dashboard → sees formatted content (read-only)
- Clicks "Edit" button → WYSIWYG editor appears
- Edits content (formatting, images, tables)
- Clicks "Save" → content saved, returns to read-only view
- Or clicks "Cancel" → discards changes, original content preserved
Step 1: Create OWL Component
Create JavaScript file: static/src/components/html_field_demo.js
/** @odoo-module **/
import { Component, useState, markup } from "@odoo/owl";
import { Wysiwyg } from "@html_editor/wysiwyg";
import { MAIN_PLUGINS, EMBEDDED_COMPONENT_PLUGINS } from "@html_editor/plugin_sets";
import { MAIN_EMBEDDINGS } from "@html_editor/others/embedded_components/embedding_sets";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { _t } from "@web/core/l10n/translation";
export class HtmlFieldDemo extends Component {
setup() {
// State management
this.state = useState({
content: 'Welcome to your custom dashboard!
',
isEditing: false,
editorKey: 0 // Force re-render trick
});
// Odoo services
this.orm = useService("orm");
this.notification = useService("notification");
this.editor = null;
// Editor configuration
this.editorConfig = {
content: this.state.content,
Plugins: [
...MAIN_PLUGINS,
...EMBEDDED_COMPONENT_PLUGINS,
],
onChange: this.onEditorChange.bind(this),
placeholder: _t("Start typing..."),
height: "400px",
toolbar: true,
embeddedComponents: true,
resources: {
embedded_components: [...MAIN_EMBEDDINGS],
},
disableVideo: false,
disableImage: false,
disableFile: false,
};
}
onEditorLoad(editor) {
this.editor = editor;
console.log("Editor loaded");
}
onEditorChange() {
if (this.editor) {
this.state.content = this.editor.getContent();
}
}
toggleEdit() {
this.state.isEditing = !this.state.isEditing;
if (this.state.isEditing) {
this.state.editorKey++; // Force editor refresh
}
}
get content() {
return markup(this.state.content); // Safe HTML rendering
}
async saveContent() {
if (!this.editor) {
this.notification.add(_t("Editor not initialized"), { type: "warning" });
return;
}
try {
const content = this.editor.getContent();
// Save to database (example)
// await this.orm.call("your.model", "write", [recordId, { content }]);
this.state.content = content;
this.state.isEditing = false;
this.notification.add(_t("Content saved!"), { type: "success" });
} catch (error) {
console.error("Save error:", error);
this.notification.add(_t("Error saving"), { type: "danger" });
}
}
cancelEdit() {
this.state.isEditing = false;
this.state.editorKey++; // Reset to original content
}
get wysiwygConfig() {
return {
...this.editorConfig,
content: this.state.content,
};
}
}
HtmlFieldDemo.template = 'custom_dashboard.HtmlField';
HtmlFieldDemo.components = { Wysiwyg };
registry.category("actions").add("custom_dashboard.HtmlField", HtmlFieldDemo);
Step 2: Create OWL Template
Create XML file: static/src/components/html_field_demo.xml
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="custom_dashboard.HtmlField" owl="1">
<div class="o_dashboard_editor h-100 d-flex flex-column">
<!-- Header with controls -->
<div class="o_dashboard_header p-3 border-bottom bg-light">
<div class="d-flex justify-content-between align-items-center">
<h3 class="mb-0">Custom Dashboard</h3>
<div class="btn-group">
<button t-if="!state.isEditing"
class="btn btn-primary"
t-on-click="toggleEdit">
<i class="fa fa-edit me-2"></i>Edit
</button>
<t t-if="state.isEditing">
<button class="btn btn-success me-2"
t-on-click="saveContent">
<i class="fa fa-save me-2"></i>Save
</button>
<button class="btn btn-secondary"
t-on-click="cancelEdit">
<i class="fa fa-times me-2"></i>Cancel
</button>
</t>
</div>
</div>
</div>
<!-- Content area -->
<div class="o_dashboard_content flex-grow-1 p-3">
<!-- Edit Mode: WYSIWYG Editor -->
<div t-if="state.isEditing" class="h-100">
<Wysiwyg
config="wysiwygConfig"
onLoad.bind="onEditorLoad"
onBlur.bind="onEditorBlur"
class="'h-100'"
toolbar="true"
t-key="state.editorKey"
/>
</div>
<!-- View Mode: Read-only content -->
<div t-else="" class="o_dashboard_view">
<div class="card">
<div class="card-body">
<t t-out="content"/>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
Step 3: Define Client Action
Create XML file: views/client_action.xml
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<record id="action_html_dashboard" model="ir.actions.client">
<field name="name">HTML Dashboard</field>
<field name="tag">custom_dashboard.HtmlField</field>
</record>
<menuitem id="menu_html_dashboard"
name="HTML Dashboard"
action="action_html_dashboard"
sequence="10"/>
</odoo>
Step 4: Configure Module Manifest
Update __manifest__.py:
{
'name': 'Custom HTML Dashboard',
'version': '18.0.1.0.0',
'category': 'Tools',
'author': 'Your Company',
'depends': ['base', 'web'],
'data': [
'views/client_action.xml',
],
'assets': {
'web.assets_backend': [
'custom_dashboard/static/src/**/*',
],
},
'application': True,
'installable': True,
'license': 'LGPL-3',
}
Key Concepts Explained
1. State Management with useState
Three Critical State Variables:
- content: HTML content (displayed or edited)
- isEditing: Boolean controlling view/edit mode
- editorKey: Integer forcing Wysiwyg re-render
Why editorKey? When user clicks "Cancel," we need editor to reset to original content. Incrementing editorKey forces OWL to treat it as new component instance, re-initializing with fresh content.
2. Safe HTML Rendering with markup()
⚠️ Critical Security Concept
By default, OWL escapes HTML strings to prevent XSS attacks. But we want HTML to render (that's the whole point of rich text editor).
// WRONG: HTML will be escaped, shown as text
get content() { return this.state.content; }
// CORRECT: HTML rendered safely
get content() { return markup(this.state.content); }
markup() tells OWL: "This HTML is safe, render it." Odoo's WYSIWYG editor already sanitizes output, so it's safe to use.
3. Editor Configuration Object
editorConfig Options:
- Plugins: MAIN_PLUGINS (formatting, lists) + EMBEDDED_COMPONENT_PLUGINS (Odoo views)
- onChange: Callback fired when content changes (sync to state)
- placeholder: Text shown in empty editor
- height: Editor height (e.g., "400px")
- toolbar: Show formatting toolbar (true/false)
- embeddedComponents: Allow Odoo component embeds
- disableVideo/Image/File: Enable/disable media types
4. Conditional Rendering in Template
<!-- Show editor when editing -->
<div t-if="state.isEditing">
<Wysiwyg ... />
</div>
<!-- Show read-only content when NOT editing -->
<div t-else="">
<t t-out="content"/>
</div>
Advanced: Saving to Database
Real-world usage: Save editor content to custom model.
Step 1: Create Model
# models/dashboard_content.py
from odoo import models, fields, api
class DashboardContent(models.Model):
_name = 'dashboard.content'
_description = 'Dashboard HTML Content'
name = fields.Char('Title', required=True)
content = fields.Html('HTML Content')
user_id = fields.Many2one('res.users', 'Owner', default=lambda self: self.env.user)
Step 2: Load Content in Component
async setup() {
super.setup();
// Load content from database
const recordId = 1; // Or get from props
const record = await this.orm.call(
"dashboard.content",
"read",
[recordId, ['content']]
);
this.state.content = record[0].content || 'Empty dashboard
';
this.recordId = recordId;
}
Step 3: Save Content
async saveContent() {
const content = this.editor.getContent();
await this.orm.call(
"dashboard.content",
"write",
[this.recordId, { content: content }]
);
this.state.content = content;
this.state.isEditing = false;
this.notification.add("Saved!", { type: "success" });
}
Real-World Use Cases
Use Case 1: Company Announcement Dashboard
Requirement:
HR needs dashboard to post company announcements with formatting, images, embedded videos.
Implementation:
- Created custom client action with WYSIWYG editor
- Model:
hr.announcementwith Html field - Only HR group can edit, all employees can view
- Auto-email employees when new announcement published
Result:
Development time: 3 hours (would be 40+ hours custom-building editor). Zero licensing costs. Rich announcements with embedded content.
Use Case 2: Project Documentation Portal
Requirement:
Project managers need to create rich documentation with code blocks, tables, images.
Implementation:
- Extended
project.projectmodel with Html fielddocumentation - Created client action showing documentation with edit capability
- Enabled code blocks for technical docs
- Version history tracked automatically
Result:
Replaced external wiki tool ($15/user/month × 30 = $5,400/year saved). Better integration with Odoo projects.
Use Case 3: Custom Email Template Builder
Requirement:
Marketing needs visual email template editor with preview.
Implementation:
- Client action with split view: Editor + Live preview
- onChange callback updates preview in real-time
- Template variables inserted via dropdown
- Test email send before mass distribution
Result:
Replaced Mailchimp for internal emails ($299/month = $3,588/year saved). Better Odoo integration.
Common Mistakes
1. Forgetting markup() Function
Used t-out="state.content" directly. HTML escaped, shown as text instead of rendered.
Fix: Create computed property with return markup(this.state.content). Use t-out="content".
2. Not Using t-key for Force Re-render
User clicks "Cancel." Editor still shows edited content (not reset).
Fix: Add t-key="state.editorKey" to Wysiwyg component. Increment editorKey on cancel.
3. Missing onChange Callback
Content changes not synced to state. Save button saves old content.
Fix: Add onChange: this.onEditorChange.bind(this) to config. Update state in callback.
4. Wrong Assets Path in Manifest
Component not loading. Browser console: "Module not found."
Fix: Verify path in manifest matches actual file location. Use 'module_name/static/src/**/*'.
Troubleshooting Guide
| Problem | Cause | Solution |
|---|---|---|
| Editor not appearing | Assets not loaded | Check manifest assets path, restart server |
| HTML shown as text | Missing markup() | Use markup(content) in getter |
| Content not saving | onChange not syncing | Add onChange callback to config |
| Cancel doesn't reset | No force re-render | Add t-key, increment on cancel |
Performance Optimization
Best Practices:
- 1. Debounce onChange: Don't save on every keystroke. Debounce 500ms.
- 2. Lazy Load Editor: Only load Wysiwyg when entering edit mode (faster initial render).
- 3. Limit Plugin Count: Only include plugins you need (smaller bundle).
- 4. Cache Content: Store last-saved content in localStorage (offline editing).
Real-World Impact Example
Scenario: SaaS Company Needs Custom Documentation Portal
Option 1: Build Custom Editor
- Developer time: 70 hours × $100/hour = $7,000
- Features delivered: Basic formatting only
- Missing: Tables, images, code blocks, embeds
- Security review needed: +8 hours = $800
- Maintenance: 3 hours quarterly = $1,200/year
- Total: $9,000 initial + $1,200/year ongoing
Option 2: Third-Party Editor (TinyMCE)
- License: $399/year × 5 devs = $1,995/year
- Integration time: 12 hours × $100 = $1,200
- Odoo upgrade compatibility issues: 6 hours/year = $600
- Security patches: As third-party releases
- Total: $3,195 first year, $2,595/year ongoing
Option 3: Odoo WYSIWYG Integration (Implemented)
- Development time: 3 hours × $100 = $300
- Features: Full WYSIWYG (formatting, tables, images, videos, code, embeds)
- License cost: $0 (included in Odoo)
- Security: Maintained by Odoo core team
- Upgrade compatibility: Automatic (part of Odoo)
- Maintenance: 0 hours (Odoo handles it)
- Total: $300 one-time, $0 ongoing
Savings: $8,700 first year + $2,595 annually vs third-party + full feature parity
Quick Implementation Checklist
- Create module structure: models/, views/, static/src/components/
- Write OWL component: Import Wysiwyg, setup state, configure editor
- Create XML template: Conditional rendering (edit/view modes)
- Define client action: Link component tag to menu item
- Update manifest: Add assets path, dependencies
- Add markup() getter: Safe HTML rendering
- Implement save logic: Store content in database model
- Add force re-render: editorKey + t-key for cancel functionality
- Test thoroughly: Edit, save, cancel, reload scenarios
- Deploy: Upgrade module, clear browser cache
Pro Tip: Start simple. Get basic edit/view toggle working first. Then add save-to-database. Then add permission controls. Don't try to build everything at once. Iterate.
Wasting $14K Building Custom Rich Text Editors?
We integrate Odoo WYSIWYG editor into custom OWL components, dashboards, client actions. Stop reinventing the wheel—use Odoo's enterprise-grade editor for free.
