Enable Dark Mode!
how-to-use-the-odoo-18-wysiwyg-editor-for-custom-dashboards-client-actions-and-owl-components.jpg
By: Aswin AK

How to Use the Odoo 18 WYSIWYG Editor for Custom Dashboards, Client Actions, and OWL Components

Technical Odoo 18 Odoo Enterprises Odoo Community

Creating rich, dynamic user interfaces is a common requirement in Odoo development. While Odoo provides powerful built-in form views, there are times when you need to build a custom, self-contained component—like a dashboard or a report editor—that incorporates a rich text editor.

This guide will walk you through a practical example of building a custom OWL component that integrates the Odoo Wysiwyg editor. We'll create a simple dashboard where users can toggle between viewing and editing HTML content, a pattern that is highly reusable for various applications.

The Problem: A Custom HTML Editor in a Client Action

Our goal is to create an Odoo module that defines a new client action. This action will display a dashboard with a read-only view of a rich text field. A user can click "Edit," which will reveal Odoo's built-in Wysiwyg editor, allowing them to modify the content. Once they are done, they can save their changes, which will then be displayed in the read-only view.

This approach gives you fine-grained control over the user experience, allowing you to use a rich text editor outside of a standard Odoo form view.

1. The OWL Component

The heart of our solution is the OWL component. It's responsible for managing the state of our editor, handling user interactions, and orchestrating the rendering.

/** @odoo-module **/
import { Component, onMounted, onWillDestroy, useState, useRef, 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() {
        this.state = useState({
            content: '<p>Welcome to your custom dashboard editor!</p>',
            isEditing: false,
            editorKey: 0
        });
        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 your dashboard content..."),
            height: "400px",
            toolbar: true,
            embeddedComponents: true,
            resources: {
                embedded_components: [...MAIN_EMBEDDINGS],
            },
            disableVideo: false,
            disableImage: false,
            disableFile: false,
            baseContainer: "DIV",
        };
    }
    onEditorLoad(editor) {
        this.editor = editor;
        console.log("Editor loaded successfully", editor);
    }
    onEditorChange() {
        // Handle content changes
        if (this.editor) {
            this.state.content = this.editor.getContent();
        }
    }
    onEditorBlur() {
        // Handle blur events - could auto-save here
        console.log("Editor blurred");
    }
    toggleEdit() {
        this.state.isEditing = !this.state.isEditing;
        if (this.state.isEditing) {
            // Force re-render of editor when switching to edit mode
            this.state.editorKey++;
        }
    }
    get content() {
        return markup(this.state.content)
    }
    async saveContent() {
        if (!this.editor) {
            this.notification.add(_t("Editor not initialized"), { type: "warning" });
            return;
        }
        try {
            const content = this.editor.getContent();
            // Here you would typically save to your custom model
            // Example: await this.orm.call("your.model", "write", [recordId, { content: content }]);
            this.state.content = content;
            this.state.isEditing = false;
            this.notification.add(_t("Content saved successfully!"), { type: "success" });
        } catch (error) {
            console.error("Error saving content:", error);
            this.notification.add(_t("Error saving content"), { type: "danger" });
        }
    }
    cancelEdit() {
        this.state.isEditing = false;
        // Reset editor key to force re-render with original content
        this.state.editorKey++;
    }
    get wysiwygConfig() {
        return {
            ...this.editorConfig,
            content: this.state.content,
        };
    }
}
HtmlFieldDemo.template = 'html_field_in_owl_component.HtmlField';
HtmlFieldDemo.components = { Wysiwyg };
registry.category("actions").add("html_field_in_owl_component.HtmlField", HtmlFieldDemo);

Key Takeaways from the Component:

  • State Management: The useState hook is used to track three critical pieces of information:
    • content: The HTML content to be displayed or edited.
    • isEditing: A boolean to control the view/edit mode.
    • editorKey: An integer used to force a re-render of the Wysiwyg component. This is a clever trick to reset the editor's internal state when canceling an edit, ensuring it re-initializes with the original content.
  • Wysiwyg Configuration: The editorConfig object is where you define the behavior and appearance of the editor. This includes:
    • Loading the necessary plugins and embedded components.
    • Defining callbacks like onChange, which is crucial for synchronizing the editor's content with our component's state.
  • HTML Rendering: The get content() computed property is critical. It uses markup(this.state.content) to tell OWL to render the string as safe HTML, preventing it from being escaped.
  • Services: The useService hook provides access to Odoo's orm and notification services, which are essential for interacting with the database and providing user feedback.

2. The OWL Template

The template defines the visual structure of our component and is where we conditionally render the view and edit modes.

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="html_field_in_owl_component.HtmlField" owl="1">
        <div class="o_dashboard_with_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">HTML FIELD DEMO</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 Content
                        </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: Show 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: Show rendered content -->
                <div t-else="" class="o_dashboard_view_content">
                    <div class="card">
                        <div class="card-body">
                            <t t-out="content"/>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </t>
</templates>

Key Takeaways from the Template:

  • Conditional Rendering: The t-if="state.isEditing" directive is the key to toggling between our two views. When isEditing is true, the Wysiwyg component is rendered; otherwise, the t-else block displays the read-only card.
  • Component Inclusion: We include the Wysiwyg component directly as a tag, passing its configuration and event handlers as props.
  • Forcing a Re-render: The t-key="state.editorKey" attribute on the Wysiwyg component is essential. When editorKey changes, OWL treats it as a new component instance and re-initializes it, which is exactly what we need to reset the editor's content.
  • Rendering HTML: The <t t-out="content"/> directive, combined with the markup() function from our component, safely renders the HTML content.

3. Odoo Module Setup

To make this client action available in Odoo, we need to configure our module's XML and manifest files.

<?xml version="1.0" encoding="UTF-8"?>
<odoo>
    <record id="action_html_field" model="ir.actions.client">
        <field name="name">HTML FIELD</field>
        <field name="tag">html_field_in_owl_component.HtmlField</field>
    </record>

    <menuitem id="menu_interactive_data_table_root"
              name="HTML FIELD"
              action="action_html_field"
              sequence="100"/>
</odoo>

This XML file does two things:

  1. Defines a Client Action: The ir.actions.client record links a menu item to our OWL component. The tag field must match the name we used to register our component in the registry (html_field_in_owl_component.HtmlField).
  2. Creates a Menu Item: This menuitem makes our client action accessible from the Odoo backend.

Finally, we need a simple __manifest__.py file to define our module and its dependencies.

{
    'name': 'HTML Field in OWL Component',
    'version': '1.0',
    'category': 'Extra Tools',
    'author': 'Your Company',
    'website': 'https://www.yourcompany.com',
    'depends': ['base', 'web'],
    'data': [
        'views/html_field_views.xml',
    ],
    'assets': {
        'web.assets_backend': [
            'html_field_in_owl_component/static/src/**/*',
        ],
    },
    'application': True,
    'installable': True,
    'license': 'LGPL-3',
}

The assets key is crucial. It tells Odoo to load all the JavaScript and XML files located in the static/src directory, making our component and template available to the backend.

Conclusion

By combining the power of OWL's component model with Odoo's built-in Wysiwyg editor, you can create highly customized and feature-rich user interfaces. This pattern of toggling between a view and edit mode is versatile and can be adapted to various use cases, from custom dashboards to inline editing of documentation pages. This implementation provides a solid foundation, and you can easily expand it by integrating with the orm service to persist content in your custom Odoo models.

To read more about How to Use OWL Components on the Website Odoo 18, refer to our blog How to Use OWL Components on the Website Odoo 18.


If you need any assistance in odoo, we are online, please chat with us.



0
Comments



Leave a comment



whatsapp_icon
location

Calicut

Cybrosys Technologies Pvt. Ltd.
Neospace, Kinfra Techno Park
Kakkancherry, Calicut
Kerala, India - 673635

location

Kochi

Cybrosys Technologies Pvt. Ltd.
1st Floor, Thapasya Building,
Infopark, Kakkanad,
Kochi, India - 682030.

location

Bangalore

Cybrosys Techno Solutions
The Estate, 8th Floor,
Dickenson Road,
Bangalore, India - 560042

Send Us A Message