This article is a continuation of the previous blog "Custom Frontend in Odoo 17 Using Controllers and QWeb". With the page structure and controller routing already in place, the next step is to add JavaScript through the frontend asset mechanism to introduce dynamic behavior, interactivity, and controlled client-side logic. This article focuses on how JavaScript frontend assets work in Odoo 17, how they are registered within the appropriate asset bundles, and how they are directly linked to pages rendered by custom controllers without relying on backend features or the built-in website builder.
What is Frontend Assets in Odoo?
In Odoo 17 or later 17 manages JavaScript and CSS through an asset pipeline. Frontend assets are grouped into bundles and loaded based on context. For website pages rendered by controllers, JavaScript must be included through frontend asset bundles, not through inline scripts or backend assets.
Key characteristics of frontend assets:
- Loaded in the browser, not the backend
- Declared at module level
- Automatically bundled and minified
The primary bundle used for custom website pages is web.assets_frontend in the manifest.
Directory
custom_sgeede/ ├── controllers/ │ ├── init.py │ └── main.py
├── static/
│ └── src/
│ └── js/
│ └── sgeede_custom_frontend.js ├── views/ │ └── sgeede_custom_page.xml ├── init.py └── manifest.py
All JavaScript file must be placed under static/src/js/.
Step 1: Creating Javascript for Frontend Assets
Odoo 17 frontend JavaScript uses ES modules. Every file must declare itself as an Odoo module.
/** @odoo-module */
import publicWidget from "@web/legacy/js/public/public_widget";
publicWidget.registry.SgeedeCustomPage = publicWidget.Widget.extend({
selector: ".sgeede_custom_asset",
events: {
"click #btn-increase": "_increaseCounter",
"click #btn-decrease": "_decreaseCounter",
},
init(parent, options) {
this._super(parent, options);
this.count = 0;
this.counterEl = null;
},
start() {
this._cacheDom();
this._updateCounter();
return this._super(...arguments);
},
_cacheDom() {
this.counterEl = this.el.querySelector("#counter-value");
},
_increaseCounter() {
this.count += 1;
this._updateCounter();
},
_decreaseCounter() {
this.count -= 1;
this._updateCounter();
},
_updateCounter() {
this.counterEl.textContent = this.count;
},
});
This implementation follows the same structural pattern as your existing public widget code.
Event bindings are declared in the events map instead of inline listeners. Each user action is mapped to a dedicated method, keeping start() free from business logic.
init() is used to initialize widget state. start() is limited to DOM preparation and initial rendering.
All behavior is encapsulated in private methods:
- _increaseCounter and _decreaseCounter mutate state
- _updateCounter synchronizes UI
- _cacheDom centralizes DOM querying
Step 2: Modify the Previous Template
<template id="my_custom_page">
<t t-call="web.frontend_layout">
<t t-set="no_header" t-value="True"/>
<t t-set="no_footer" t-value="True"/>
<div class="container sgeede_custom_asset">
<h1>My Custom Frontend Page</h1>
<div style="font-size: 48px; margin: 20px 0;" id="counter-value">0</div>
<div>
<button id="btn-decrease" class="btn btn-secondary me-2">-</button>
<button id="btn-increase" class="btn btn-primary">+</button>
</div>
</div>
</t>
</template>
On this page, a simple counter interface is introduced by adding a display area for the counter value along with buttons to increase and decrease it. These elements serve as the visual layer that will later be controlled entirely by JavaScript frontend assets.
- Added <div id="counter-value"> to display dynamic numeric state
- Initialized default value directly in the template
- Added two buttons for increment and decrement actions
Step 3: Registering JavaScript in Frontend Assets
{
"name": "SGEEDE Custom For Blog",
"version": "17.0.1.0.0",
"summary": "Custom module for SGEEDE Blog",
"category": "Custom",
"author": "SGEEDE",
"website": "https://sgeede.com",
"license": "LGPL-3",
"depends": [
"base",
"sale",
"web",
],
"data": [
"views/sgeede_custom_page.xml",
],
"assets": {
# put your javascript file path here!
"web.assets_frontend": [
"custom_sgeede/static/src/js/sgeede_custom_frontend.js",
],
},
"installable": True,
"application": False,
}
Now you can upgrade the module and see the result.
