Odoo 18 OWL Framework Tutorial: Building a Product Color Selector Beginners Guide
OWL (Odoo Web Library) is Odoo's modern web framework for building user interface components. It's a lightweight, component-based framework that provides:

Introduction to OWL Framework

What is OWL?

OWL (Odoo Web Library) is Odoo’s modern web framework for building user interface components. It’s a lightweight, component-based framework that provides:

  • Reactive component system
  • State management
  • Event handling
  • Template system
  • Component lifecycle management

Why Use OWL?

  1. Better Performance: OWL is optimized for Odoo’s specific needs and provides better performance than jQuery-based widgets.
  2. Modern Development: Uses modern JavaScript features and follows current web development practices.
  3. Component-Based: Encourages reusable, modular code through components.
  4. Reactive: Automatically updates the UI when data changes.

Git Hub Repo Link
https://github.com/KtreeOpenSource/KTree_odoo_examples/tree/18.0

Module Structure

product_color_selector/
├── __init__.py
├── __manifest__.py
├── models/
│   ├── __init__.py
│   └── product_template.py
├── static/
│   └── src/
│       └── components/
│           └── color_selector/
│               ├── color_selector.js
│               ├── color_selector.xml
│               └── color_selector.scss
└── views/
    └── product_template_views.xml

Key Files Explained

  1. manifest.py:

{
‘name’: ‘Product Color Selector’,
‘version’: ‘1.0’,
‘depends’: [‘product’],
‘data’ : [
‘views/product_template_views.xml’
],
‘assets’: {
‘web.assets_backend’: [
‘product_color_selector/static/src/components/color_selector/color_selector.js’,
‘product_color_selector/static/src/components/color_selector/color_selector.xml’,
‘product_color_selector/static/src/components/color_selector/color_selector.scss’,
],
},
}

The manifest defines module metadata and assets loading.

  1. Python Model (models/product_template.py):

from odoo import models, fields
class ProductTemplate(models.Model):
_inherit = ‘product.template’
product_color = fields.Selection([
(‘red’, ‘Red’),
(‘blue’, ‘Blue’),
(‘green’, ‘Green’),
(‘yellow’, ‘Yellow’),
(‘black’, ‘Black’),
(‘white’, ‘White’)
], string=’Product Color’, default=’white’)

This extends the product template to add the color field.

Implementation Guide

1. JavaScript Component (color_selector.js)

/** @odoo-module **/
import { _t } from "@web/core/l10n/translation";
import { registry } from "@web/core/registry";
import { Component } from "@odoo/owl";
import { Field } from "@web/views/fields/field";
import { standardFieldProps } from "@web/views/fields/standard_field_props";

export class ColorSelectorField extends Component {
static template = "custom_module.ColorSelector";
static components = {};
static props = {
…standardFieldProps,
name: { type: String },
record: { type: Object },
readonly: { type: Boolean, optional: true },
value: { optional: true },
};

setup() {
this.colors = [
{ value: ‘red’, label: _t(‘Red’), color: ‘#FF0000’ },
{ value: ‘blue’, label: _t(‘Blue’), color: ‘#0000FF’ },
{ value: ‘green’, label: _t(‘Green’), color: ‘#008000’ },
{ value: ‘yellow’, label: _t(‘Yellow’), color: ‘#FFFF00’ },
{ value: ‘black’, label: _t(‘Black’), color: ‘#000000’ },
{ value: ‘white’, label: _t(‘White’), color: ‘#FFFFFF’ },
];
}

get selectedColor() {
// Get the current value directly from the record
return this.props.record.data[this.props.name] || ‘white’;
}

async onColorClick(color) {
if (!this.props.readonly) {
try {
this.env.services.ui.block();
await this.props.record.update({ [this.props.name]: color });
await this.props.record.save({
stayInEdition: true,
noReload: true
});
} catch (error) {
this.env.services.notification.notify({
title: _t("Error"),
message: _t("Failed to update color"),
type: "danger",
});
console.error("Error updating product color:", error);
} finally {
this.env.services.ui.unblock();
}
}
}
}

export const colorSelector = {
component: ColorSelectorField,
supportedTypes: ["selection"],
extractProps: ({ attrs, field }) => ({
name: field.name,
}),
};

registry.category("fields").add("color_selector", colorSelector);

2. XML Template (color_selector.xml)

<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="custom_module.ColorSelector" owl="1">
<div class="o_field_color_selector" t-att-class="{‘o_readonly’: props.readonly}">
<div class="d-flex flex-wrap gap-2">
<t t-foreach="colors" t-as="color" t-key="color.value">
<div
class="color-circle"
t-att-class="{‘selected’: color.value === selectedColor}"
t-on-click="() => this.onColorClick(color.value)"
t-att-style="’background-color: ‘ + color.color"
t-att-title="color.label"
/>
</t>
</div>
</div>
</t>
</templates>

Component Registration and Types

Registry Types in Odoo

  1. Fields Registry:

registry.category("fields").add("color_selector", colorSelector);

Used for registering field widgets.

  1. Views Registry:

registry.category("views").add("view_type", viewComponent);

Used for registering custom views.

  1. Services Registry:

registry.category("services").add("service_name", service);

Used for registering services.

  1. Actions Registry:

registry.category("actions").add("action_type", actionHandler);

Used for registering custom actions.

When to Use Each Registry

  1. Fields Registry: Use when creating custom field widgets (like our color selector)
  2. Views Registry: Use when creating entirely new view types
  3. Services Registry: Use when creating global services (like notifications)
  4. Actions Registry: Use when creating custom client actions

Detailed Component Analysis

JavaScript Component (color_selector.js)

Let’s break down the JavaScript component line by line:

1. Module Declaration and Imports

/** @odoo-module **/ import { _t } from "@web/core/l10n/translation"; import { registry } from "@web/core/registry"; import { Component } from "@odoo/owl"; import { standardFieldProps } from "@web/views/fields/standard_field_props";

  • @odoo-module: Marks this as an Odoo module
  • _t: Translation function for internationalization
  • registry: Odoo’s registry system for component registration
  • Component: Base OWL component class
  • standardFieldProps: Standard properties for field components

2. Component Class Definition

export class ColorSelectorField extends Component { static template = "custom_module.ColorSelector";

  • Extends OWL’s Component class
  • static template: Links to the XML template file

3. Props Definition

static props = { …standardFieldProps, name: { type: String }, record: { type: Object }, readonly: { type: Boolean, optional: true }, value: { optional: true }, };

Props are properties passed to the component:

  • standardFieldProps: Base field properties
  • name: Field name (required)
  • record: Odoo record object (required)
  • readonly: Controls if field is editable
  • value: Current field value

4. Setup Method

setup() {
this.colors = [ { value: ‘red’, label: _t(‘Red’), color: ‘#FF0000’ }, // … other colors ]; }

  • setup(): Lifecycle method called when component is created
  • Initializes color options with translations
  • Runs before the first render

5. Getter Method

get selectedColor() { return this.props.record.data[this.props.name] || ‘white’; }

  • Reactive getter for the currently selected color
  • Accesses color value from record data
  • Provides default ‘white’ if no color selected

6. Event Handler

sync onColorClick(color) {
if (!this.props.readonly) {
try {
this.env.services.ui.block();
await this.props.record.update({ [this.props.name]: color });
await this.props.record.save({
stayInEdition: true,
noReload: true
});
} catch (error) {
this.env.services.notification.notify({
title: _t("Error"),
message: _t("Failed to update color"),
type: "danger",
});
console.error("Error updating product color:", error);
} finally {
this.env.services.ui.unblock();
}
}
}

  • this.env.services.ui.block(): Shows loading indicator
  • record.update(): Updates field value
  • record.save(): Saves to database
  • Error handling with notification service
  • ui.unblock(): Removes loading indicator

XML Template (color_selector.xml)

Let’s analyze the template structure:

<templates xml:space="preserve">
<t t-name="custom_module.ColorSelector" owl="1">
<div class="o_field_color_selector" t-att-class="{‘o_readonly’: props.readonly}">
<div class="d-flex flex-wrap gap-2">
<t t-foreach="colors" t-as="color" t-key="color.value">
<div
class="color-circle"
t-att-class="{‘selected’: color.value === selectedColor}"
t-on-click="() => this.onColorClick(color.value)"
t-att-style="’background-color: ‘ + color.color"
t-att-title="color.label"
/>
</t>
</div>
</div>
</t>
</templates>

Template Directives:

  • t-name: Template identifier
  • owl=”1″: Marks as OWL template
  • t-att-class: Dynamic class binding
  • t-foreach: Loop directive
  • t-key: Unique key for loop items
  • t-on-click: Event binding
  • t-att-style: Dynamic style binding
  • t-att-title: Dynamic title attribute

SCSS Styling (color_selector.scss)

.o_field_color_selector {
padding: 0;

.color-circle {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #e2e2e2;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
box-sizing: border-box;

&:hover {
transform: scale(1.1);
border-color: #7C7BAD;
}

&.selected {
transform: scale(1.1);
border: 2px solid white;
box-shadow: 0 0 0 2px #7C7BAD;
z-index: 1;

&:hover {
transform: scale(1.1);
}
}

/* Special handling for white color */
&[style*="background-color: #FFFFFF"],
&[style*="background-color: rgb(255, 255, 255)"] {
border: 2px solid #e2e2e2;

// &.selected {
// border: 2px solid #7C7BAD;
// box-shadow: 0 0 0 2px #7C7BAD;
// }

&:hover {
border-color: #7C7BAD;
}
}
}

&.o_readonly {
.color-circle {
cursor: not-allowed;
opacity: 0.6;

&:hover {
transform: none;
border-color: #e2e2e2;
}

&.selected {
opacity: 1;
border-color: #7C7BAD;
transform: scale(1.1);
}
}
}
}

SCSS Features:

  1. Base Styling
    • Sets dimensions and shape
    • Defines border and cursor
  2. Interactive States
    • Hover effects with scaling
    • Selection highlight
    • Transition animations
  3. Special Cases
    • White color handling
    • Read-only state styling
  4. Nesting
    • Uses SCSS nesting for organization
    • Separates states with & operator

Best Practices

  1. Component Structure:
    • Keep components small and focused
    • Use proper props validation
    • Handle errors gracefully
  2. State Management:
    • Use reactive getters for computed values
    • Avoid direct DOM manipulation
    • Use props for parent-child communication
  3. Performance:
    • Use t-key in loops for optimal rendering
    • Avoid unnecessary computations in getters
    • Use async/await for database operations

Troubleshooting

Common issues and solutions:

  1. Component Not Rendering:
    • Check registry registration
    • Verify asset loading in manifest
    • Check browser console for errors
  2. Data Not Saving:
    • Verify record.update() calls
    • Check error handling
    • Verify field name matches model
  3. Styling Issues:
    • Check SCSS compilation
    • Verify class names
    • Check browser dev tools for CSS conflicts