Enable Dark Mode!
how-to-use-bottom-sheet-service-in-odoo-19-website.jpg
By: Aswin AK

How to use Bottom Sheet Service in Odoo 19 Website

Technical Odoo 19 Website&E-commerce

Bottom sheets are modern UI components that slide up from the bottom of the screen, providing contextual information without navigating away. In this guide, we'll implement bottom sheets in Odoo 19 using the new Interaction class system.

What is a Bottom Sheet?

A bottom sheet is a modal dialog that appears from the bottom of the viewport. In Odoo 19, the bottom sheet component provides smooth animations, scroll-based interactions, and automatic dismissal behaviors.

Architecture Overview

Our implementation consists of:

  1. OWL Component: Content displayed inside the bottom sheet
  2. Interaction Class: Handles user interactions (replaces publicWidget in Odoo 19)
  3. Bottom Sheet Service: Manages the lifecycle
  4. Controller: Serves the webpage
  5. Template: Renders the page with trigger buttons

Step 1: Create the OWL Component

Create a product card component that will be displayed inside the bottom sheet.

File: static/src/components/product_card/product_card.js

/** @odoo-module **/
import { Component, useState } from "@odoo/owl";
export class ProductCard extends Component {
    static template = "bottom_sheet_test.ProductCard";
    static props = {
        product: Object,
    };
}

File: static/src/components/product_card/product_card.xml

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
    <t t-name="bottom_sheet_test.ProductCard">
        <div class="product-card-sheet">
            <!-- Product Image -->
            <div class="product-image">
                <img t-att-src="props.product?.image or '/web/static/img/placeholder.png'"
                     class="img-fluid"
                     alt="Product"/>
            </div>
            <!-- Product Info -->
            <div class="product-info">
                <h3 class="product-title" t-esc="props.product?.name or 'Product Name'"/>
                <div class="product-price">
                    <span class="currency">$</span>
                    <span class="amount" t-esc="props.product?.price or '0.00'"/>
                </div>
                <p class="product-description" t-esc="props.product?.description or 'Product description goes here.'"/>
            </div>
        </div>
    </t>
</templates>

File: static/src/components/product_card/product_card.scss

.product-card-sheet {
    display: flex;
    flex-direction: column;
    min-height: 400px;
    background: white;
    .product-image {
        padding: 0 2rem;
        text-align: center;
        img {
            max-height: 200px;
            object-fit: contain;
            border-radius: 12px;
        }
    }
    .product-info {
        padding: 2rem;
        flex: 1;
        .product-title {
            font-size: 1.75rem;
            font-weight: 700;
            margin-bottom: 0.5rem;
            color: #1a1a1a;
        }
        .product-price {
            font-size: 2rem;
            font-weight: 700;
            color: #875a7b;
            margin-bottom: 1rem;
            .currency {
                font-size: 1.5rem;
            }
        }
        .product-description {
            color: #666;
            line-height: 1.6;
            margin-bottom: 1.5rem;
        }
      
    }
}

Step 2: Create the Interaction Class

The Interaction class is the new way to handle DOM interactions in Odoo 19, replacing publicWidget.

File: static/src/interactions/bottom_sheet_interaction.js

/** @odoo-module **/
import { registry } from '@web/core/registry';
import { Interaction } from "@web/public/interaction";
import { ProductCard } from '../components/product_card/product_card';
export class BottomSheetInteraction extends Interaction {
    static selector = ".bottom-sheet-demo-page";
    dynamicContent = {
        ".trigger-product-sheet": { "t-on-click": this.onTriggerProductSheet },
    };

    setup() {
        super.setup();
        // Get the bottom sheet service from the Odoo service registry
        this.bottomSheetService = this.services.bottom_sheet;
    }
    /**
     * Trigger product details bottom sheet
     */
    onTriggerProductSheet(ev) {
        const productData = {
            name: ev.target.dataset.productName || "Premium Wireless Headphones",
            price: ev.target.dataset.productPrice || "299.99",
            description: ev.target.dataset.productDescription ||
                "High-quality wireless headphones with active noise cancellation, premium sound quality, and 30-hour battery life. Perfect for music lovers and professionals.",
            image: ev.target.dataset.productImage || "/web/static/img/placeholder.png",
        };
        this.bottomSheetService.add(
            ev.target,
            ProductCard,
            {
                product: productData,
            },
            {
                class: "product-bottom-sheet",
                onClose: () => {
                    console.log("Product bottom sheet closed");
                },
            }
        );
    }
}
registry.category("public.interactions").add("BottomSheetInteraction", BottomSheetInteraction);

Step 3: Create the Controller

The controller handles the HTTP request and renders the template.

File: controllers/main.py

from odoo import http
from odoo.http import request

class BottomSheetController(http.Controller):
    @http.route('/bottom-sheet-demo', type='http', auth='public', website=True)
    def bottom_sheet_demo(self, **kwargs):
        """Render the bottom sheet demo page"""
        products = request.env['product.product'].search_read([],['name', 'list_price', 'description'], limit=3)
        return request.render('your_module.bottom_sheet_demo_page', {
            'products': products,
        })

Step 4: Create the Template

Create a QWeb template with buttons to trigger bottom sheets.

File: views/bottom_sheet_demo.xml

<?xml version="1.0" encoding="utf-8"?>
<odoo>
    <template id="bottom_sheet_demo_page" name="Bottom Sheet Demo Page">
        <t t-call="website.layout">
            <div id="wrap" class="oe_structure bottom-sheet-demo-page">
                <section class="py-5" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
                    <div class="container">
                        <div class="row">
                            <div class="col-lg-8 mx-auto text-center">
                                <h1 class="display-4 mb-3">Bottom Sheet Demo</h1>
                                <p class="lead">Click any product to view details in a beautiful bottom sheet</p>
                            </div>
                        </div>
                    </div>
                </section>
                <section class="container py-5">
                    <div class="row g-4">
                        <t t-foreach="products" t-as="product">
                            <div class="col-md-4">
                                <div class="card h-100 shadow-sm">
                                    <img t-attf-src="/web/image?model=product.product&amp;id={{product['id']}}&amp;field=image_1920"
                                         class="card-img-top"
                                         alt="Product"
                                         style="height: 200px; object-fit: cover;"/>
                                    <div class="card-body">
                                        <h5 class="card-title" t-esc="product['name']"/>
                                        <p class="card-text text-muted" t-esc="product['description']"/>
                                        <div class="d-flex justify-content-between align-items-center">
                                            <span class="h4 mb-0 text-primary">
                                                $<t t-esc="product['list_price']"/>
                                            </span>
                                            <button class="btn btn-primary trigger-product-sheet"
                                                    t-att-data-product-name="product['name']"
                                                    t-att-data-product-price="product['list_price']"
                                                    t-att-data-product-description="product['description']"
                                                    t-attf-data-product-image="/web/image?model=product.product&amp;id={{product['id']}}&amp;field=image_1920">
                                                View Details
                                            </button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </t>
                    </div>
                    <!-- Info Section -->
                    <div class="row mt-5">
                        <div class="col-lg-8 mx-auto">
                            <div class="alert alert-info" role="alert">
                                <h5 class="alert-heading">
                                    <i class="fa fa-info-circle"/> How It Works
                                </h5>
                                <p class="mb-2">Bottom sheets can be dismissed by:</p>
                                <ul class="mb-0">
                                    <li>Scrolling down past the dismiss threshold</li>
                                    <li>Pressing the ESC key</li>
                                    <li>Using the browser back button</li>
                                    <li>Clicking the close button or action buttons</li>
                                </ul>
                            </div>
                        </div>
                    </div>
                </section>
            </div>
        </t>
    </template>
    <!-- Menu Item -->
    <record id="menu_bottom_sheet_demo" model="website.menu">
        <field name="name">Bottom Sheet Demo</field>
        <field name="url">/bottom-sheet-demo</field>
        <field name="parent_id" ref="website.main_menu"/>
        <field name="sequence" type="int">99</field>
    </record>
</odoo>

Step 6: Module Manifest

Update your __manifest__.py:

File: __manifest__.py

{
    'name': 'Bottom Sheet Demo',
    'version': '1.0',
    'category': 'Website',
    'summary': 'Bottom Sheet with Interaction Class in Odoo 19',
    'depends': ['website'],
    'data': [
        'views/bottom_sheet_demo.xml',
    ],
    'assets': {
        'web.assets_frontend': [
            'your_module/static/src/components/product_card/product_card.xml',
            'your_module/static/src/components/product_card/product_card.js',
            'your_module/static/src/interactions/bottom_sheet_interaction.js',
        ],
    },
    'installable': True,
    'application': False,
    'license': 'LGPL-3',
}

Key differences from publicWidget:

  • Extends Interaction instead of publicWidget.Widget
  • Uses dynamicContent property for event bindings
  • Access services via this.services
  • Cleaner, more modern API

Key Features

  • Modern Interaction class (no publicWidget)
  • Beautiful, functional product card component
  • Smooth animations and transitions
  • Responsive design
  • Multiple dismissal methods
  • Data passed via HTML data attributes

Conclusion

This implementation successfully demonstrates how to use Odoo 19's new Interaction Class and the built-in Bottom Sheet Service to create a modern, non-intrusive UI component. By moving away from the publicWidget to the Interaction class and integrating our OWL component, we have established a clean, maintainable, and highly responsive pattern. This is the recommended approach for delivering engaging, mobile-friendly contextual information in Odoo 19 websites, significantly improving the user experience by keeping users on the same page.

To read more about How to Use the Interaction Class with OWL Components in Odoo 19 Website, refer to our blog How to Use the Interaction Class with OWL Components in Odoo 19 Website.


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