Using Odoo v16 OWL Component inside existing website page

1.3k views Asked by At

I'm using Odoo v16 and I want to use Odoo Official Frontend Docs for v16 as an example,

In the tutorial, we created the Counter OWL Component.

But in the example, they created a controller and initiated the whole OWL stack from the ground (owl_playground.assets_playground asset).

I want to use the component inside an existing frontend page. assume I want to show the counter on my home page of the website (not a custom page initiated with from a controller and custom template and main.js)

How can I do that?

What should I do?

Alternatively, it will be good if I can create a website snippet for that counter or any other Odoo component.

1

There are 1 answers

0
Ahrimann On

You can mount your owl-component in any place using a DOM-selector (document.getElementById(...)):

    //import { Component, useState } from "@odoo/owl";
    const { Component, useState } = owl;
    //export class Counter extends Component {
    class Counter extends Component {
      setup() {
        this.state = useState({ value: 1 });
        }

      increment() {
        this.state.value = this.state.value + 1;
        }
    }

    Counter.template = "owl_playground.Counter";

    owl.utils.whenReady().then(() => {
        const my_counter = new Counter();
        my_counter.mount(document.body);
    });

Example in Odoo 16 (odoo/addons/mass_mailing/static/src/snippets/s_rating /options.js):


/**
     * Allows to select a font awesome icon with media dialog.
     *
     * @see this.selectClass for parameters
     */
    customIcon: async function (previewMode, widgetValue, params) {
        const media = document.createElement('i');
        media.className = params.customActiveIcon === 'true' ? this.faClassActiveCustomIcons : this.faClassInactiveCustomIcons;
        const dialog = new ComponentWrapper(this, MediaDialogWrapper, {
            noImages: true,
            noDocuments: true,
            noVideos: true,
            media,
            save: icon => {
                const customClass = icon.className;
                const $activeIcons = this.$target.find('.s_rating_active_icons > i');
                const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i');
                const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons;
                $icons.removeClass().addClass(customClass);
                this.faClassActiveCustomIcons = $activeIcons.length > 0 ? $activeIcons.attr('class') : customClass;
                this.faClassInactiveCustomIcons = $inactiveIcons.length > 0 ? $inactiveIcons.attr('class') : customClass;
                this.$target[0].dataset.activeCustomIcon = this.faClassActiveCustomIcons;
                this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons;
                this.$target[0].dataset.icon = 'custom';
                this.iconType = 'custom';
            }
        });
        dialog.mount(document.body);
    },

CASE 2 : Emojis in mail (odoo/addons/mail/static/src/js /emojis_dropdown.js)

/** @odoo-module **/

import emojis from '@mail/js/emojis';

const { Component, useRef, onMounted } = owl;

export class EmojisDropdown extends Component {
    setup() {
        this.toggleRef = useRef('toggleRef');
        this.emojis = emojis;
        super.setup();
        onMounted(() => {
            new Dropdown(this.toggleRef.el, {
                  popperConfig: { placement: 'bottom-end', strategy: 'fixed' },
            });
        });
    }
};
EmojisDropdown.template = 'mail.EmojisDropdown';

CASE 3 : Widget to display odoo field (odoo/addons/mrp/static/src/widgets/timer.js)

/** @odoo-module **/

import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
import { parseFloatTime } from "@web/views/fields/parsers";
import { useInputField } from "@web/views/fields/input_field_hook";

const { Component, useState, onWillUpdateProps, onWillStart, onWillDestroy } = owl;

export class MrpTimer extends Component {
    setup() {
        this.orm = useService('orm');
        this.state = useState({
            // duration is expected to be given in minutes
            duration:
                this.props.value !== undefined ? this.props.value : this.props.record.data.duration,
        });
        useInputField({
            getValue: () => this.durationFormatted,
            refName: "numpadDecimal",
            parse: (v) => parseFloatTime(v),
        });

   
        onWillStart(async () => {
            if(this.props.ongoing === undefined && !this.props.record.model.useSampleModel && this.props.record.data.state == "progress") {
                const additionalDuration = await this.orm.call('mrp.workorder', 'get_working_duration', [this.props.record.resId]);
                this.state.duration += additionalDuration;
            }
            if (this.ongoing) {
                this._runTimer();
            }
        });
        

    _runTimer() {
        this.timer = setTimeout(() => {
            if (this.ongoing) {
                this.state.duration += 1 / 60;
                this._runTimer();
            }
        }, 1000);
    }
}

MrpTimer.supportedTypes = ["float"];
MrpTimer.template = "mrp.MrpTimer";

registry.category("fields").add("mrp_timer", MrpTimer);
registry.category("formatters").add("mrp_timer", formatMinutes);

--------------- OTHER USE CASES IN ODOO 16 --------------

** SNIPPETS Examples from the official source code:** https://github.com/odoo/odoo/blob/16.0/addons/website/views/snippets/snippets.xml Owl-components are so far just used as part of the js bound to an xml-snippet-template

** odoo/addons/website/views/snippets/s_image_gallery.xml (data-target)**:

     <?xml version="1.0" encoding="utf-8"?>
<odoo>

                    <t t-snippet="website.s_image_gallery" string="Image Gallery" t-thumbnail="/website/static/src/img/snippets_thumbs/s_image_gallery.svg">
                        <keywords>gallery, carousel</keywords>
                    </t>

<template id="s_image_gallery" name="Image Gallery">
    <section class="s_image_gallery o_slideshow s_image_gallery_show_indicators s_image_gallery_indicators_rounded pt24" data-vcss="001" data-columns="3" style="height: 500px; overflow: hidden;">
        <div class="container">
            <div id="slideshow_sample" class="carousel slide" data-bs-ride="carousel" data-bs-interval="0" style="margin: 0 12px;">
                <div class="carousel-inner" style="padding: 0;">
                    <div class="carousel-item active">
                        <img class="img img-fluid d-block" src="/web/image/website.library_image_08" data-name="Image" data-index="0"/>
                    </div>

dynamic part: odoo/addons/website/static/src/snippets/s_image_gallery/000.xml

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <!-- Gallery Slideshow: This template is used to display a slideshow of images inside a bootstrap carousel.-->
    <t t-name="website.gallery.slideshow">
        <div t-attf-id="#{id}" class="carousel slide" data-bs-ride="carousel" t-attf-data-bs-interval="#{interval}" style="margin: 0 12px;">
            <div class="carousel-inner" style="padding: 0;">
                 <t t-foreach="images" t-as="image">
                    <div t-attf-class="carousel-item #{image_index == index and 'active' or None}">
                        <img t-attf-class="#{attrClass || 'img img-fluid d-block'}" t-att-src="image.src" t-att-style="attrStyle" t-att-alt="image.alt" data-name="Image"/>
                    </div>
                 </t>
            </div>

            <ul class="carousel-indicators">
                <li class="o_indicators_left text-center d-none" aria-label="Previous" title="Previous">
                    <i class="fa fa-chevron-left"/>
                </li>
                <t t-foreach="images" t-as="image">
                    <li t-attf-data-bs-target="##{id}" t-att-data-bs-slide-to="image_index" t-att-class="image_index == index and 'active' or None" t-attf-style="background-image: url(#{image.src})"></li>
                </t>

Options allow users to edit a snippet’s appearance using the Website Builder. You can create snippet options easily and automatically add them to the Website Builder.Options are wrapped in groups. Groups can have properties that define how the included options interact with the user interface. data-selector binds all the options included in the group to a particular element. It can be used in combination with data-target. Example: option on .carousel-item:

           <div data-js="sizing_y"
        data-selector="section, .row > div, .parallax, .s_hr, .carousel-item, .s_rating"
        data-exclude="section:has(> .carousel), .s_image_gallery .carousel-item, .s_col_no_resize.row > div, .s_col_no_resize"/>

odoo/addons/website/static/src/snippets/s_image_gallery /options.js

The Website Builder provides several events you can use to trigger your custom functions. For example, the event "start" occurs when the publisher selects the snippet for the first time in an editing session or when the snippet is drag-and-dropped on the page:

       odoo.define('website.s_image_gallery_options', function (require) {
'use strict';

const { MediaDialogWrapper } = require('@web_editor/components/media_dialog/media_dialog');
const { ComponentWrapper } = require('web.OwlCompatibility');
var core = require('web.core');
var options = require('web_editor.snippets.options');
const wUtils = require("website.utils");

var _t = core._t;
var qweb = core.qweb;

options.registry.gallery = options.Class.extend({
    /**
     * @override
     */
    start: function () {
        var self = this;
        this.hasAddImages = this.el.querySelector("we-button[data-add-images]");

//...
/**
     * Get the image target's layout mode (slideshow, masonry, grid or nomode).
     *
     * @returns {String('slideshow'|'masonry'|'grid'|'nomode')}
     */
    getMode: function () {
        var mode = 'slideshow';
        if (this.$target.hasClass('o_masonry')) {
            mode = 'masonry';
        }
        if (this.$target.hasClass('o_grid')) {
            mode = 'grid';
        }
        if (this.$target.hasClass('o_nomode')) {
            mode = 'nomode';
        }
        return mode;
    },
    /**
     * Displays the images with a "slideshow" layout.
     */
    slideshow: function () {
        const imageEls = this._getImages();
        const images = _.map(imageEls, img => ({
            // Use getAttribute to get the attribute value otherwise .src
            // returns the absolute url.
            src: img.getAttribute('src'),
            alt: img.getAttribute('alt'),
        }));
        var currentInterval = this.$target.find('.carousel:first').attr('data-bs-interval');
        var params = {
            images: images,
            index: 0,
            title: "",
            interval: currentInterval || 0,
            id: 'slideshow_' + new Date().getTime(),
            attrClass: imageEls.length > 0 ? imageEls[0].className : '',
            attrStyle: imageEls.length > 0 ? imageEls[0].style.cssText : '',
        },
        $slideshow = $(qweb.render('website.gallery.slideshow', params));
        this._replaceContent($slideshow);
        _.each(this.$('img'), function (img, index) {
            $(img).attr({contenteditable: true, 'data-index': index});
        });
        this.$target.css('height', Math.round(window.innerHeight * 0.7));

        // Apply layout animation
        this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
        this.$('li.fa').off('click');
    },

    /**
     * Allows to select images to add as part of the snippet.
     *
     * @see this.selectClass for parameters
     */
    addImages: function (previewMode) {
        const $images = this.$('img');
        var $container = this.$('> .container, > .container-fluid, > .o_container_small');
        const lastImage = _.last(this._getImages());
        let index = lastImage ? this._getIndex(lastImage) : -1;
        const dialog = new ComponentWrapper(this, MediaDialogWrapper, {
            multiImages: true,
            onlyImages: true,
            save: images => {
                // TODO In master: restore addImages Promise result.
                this.trigger_up('snippet_edition_request', {exec: () => {
                    let $newImageToSelect;
                    for (const image of images) {
                        const $img = $('<img/>', {
                            class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block ',
                            src: image.src,
                            'data-index': ++index,
                            alt: image.alt || '',
                            'data-name': _t('Image'),
                            style: $images.length > 0 ? $images[0].style.cssText : '',
                        }).appendTo($container);
                        if (!$newImageToSelect) {
                            $newImageToSelect = $img;
                        }
                    }
                    if (images.length > 0) {
                        return this._modeWithImageWait('reset', this.getMode()).then(() => {
                            this.trigger_up('cover_update');
                            // Triggers the re-rendering of the thumbnail
                            $newImageToSelect.trigger('image_changed');
                        });
                    }
                }});
            },
        });
        dialog.mount(this.el);
    },    

Public Widget to handle visitor interactions: odoo/addons/website/static/src/snippets/s_image_gallery/000.js


odoo.define('website.s_image_gallery', function (require) {
'use strict';

var core = require('web.core');
var publicWidget = require('web.public.widget');

var qweb = core.qweb;

const GallerySliderWidget = publicWidget.Widget.extend({
    selector: '.o_slideshow',
    disabledInEditableMode: false,

    /**
     * @override
     */
    start: function () {
        var self = this;
        this.$carousel = this.$target.is('.carousel') ? this.$target : this.$target.find('.carousel');
        

Tutorial to create snippets:

To get inspiration, you can download free custom module having snippets in the odoo app store: