Registering Sub-components
Every component used in a template must be declared in the parent's static components object. OWL uses this map to look up the component class when it encounters a tag name in the template.
import { Component, xml } from "@odoo/owl";
import UserAvatar from "./UserAvatar";
import StatusBadge from "./StatusBadge";
import ActionMenu from "./ActionMenu";
class UserCard extends Component {
// Every component used in the template must be listed here
static components = { UserAvatar, StatusBadge, ActionMenu };
static template = xml`
<div class="user-card">
<UserAvatar src="props.user.avatarUrl" size="48"/>
<div class="user-info">
<strong t-esc="props.user.name"/>
<StatusBadge status="props.user.status"/>
</div>
<ActionMenu items="props.actions" onSelect.bind="handleAction"/>
</div>
`;
}
If UserAvatar is used in the template but not listed in static components, OWL throws "Cannot find the definition of component 'UserAvatar'". Every sub-component, at every level, must be registered by its immediate parent. Grandchildren register with their own parent — not the root.
Parent-Child Communication
OWL uses one-way data flow: data goes down via props, events go up via callback props or trigger(). This pattern keeps data flow predictable.
// ── Child ──────────────────────────────────────────────────────────
class QuantityInput extends Component {
static template = xml`
<div class="qty-input">
<button t-on-click="() => change(-1)" t-att-disabled="props.value <= props.min">−</button>
<span t-esc="props.value"/>
<button t-on-click="() => change(+1)" t-att-disabled="props.value >= props.max">+</button>
</div>
`;
static props = {
value: { type: Number },
min: { type: Number, optional: true },
max: { type: Number, optional: true },
onChange: { type: Function },
};
static defaultProps = { min: 0, max: 999 };
change(delta) {
const next = this.props.value + delta;
if (next >= this.props.min && next <= this.props.max) {
this.props.onChange(next); // notify parent
}
}
}
// ── Parent ──────────────────────────────────────────────────────────
class CartItem extends Component {
static components = { QuantityInput };
static template = xml`
<div class="cart-item">
<span t-esc="props.product.name"/>
<QuantityInput
value="state.qty"
min="1"
max="99"
onChange.bind="updateQty"
/>
<span>$<t t-esc="(state.qty * props.product.price).toFixed(2)"/></span>
</div>
`;
state = useState({ qty: 1 });
updateQty(newQty) {
this.state.qty = newQty;
}
}
Dynamic Components with t-component
t-component renders a component class stored in a variable — useful for registries, plugin systems, and dynamic widget rendering (common in Odoo views).
import TextWidget from "./TextWidget";
import NumberWidget from "./NumberWidget";
import SelectWidget from "./SelectWidget";
import DateWidget from "./DateWidget";
// Widget registry — maps field type to component class
const WIDGET_MAP = {
char: TextWidget,
integer: NumberWidget,
float: NumberWidget,
many2one: SelectWidget,
date: DateWidget,
};
class DynamicField extends Component {
static components = { TextWidget, NumberWidget, SelectWidget, DateWidget };
static template = xml`
<div class="field-widget">
<!-- t-component renders whichever class widgetComponent resolves to -->
<t t-component="widgetComponent"
value="props.field.value"
readonly="props.readonly"
onChange.bind="handleChange"
/>
</div>
`;
static props = {
field: { type: Object },
readonly: { type: Boolean, optional: true },
onChange: { type: Function, optional: true },
};
get widgetComponent() {
// Return the component class for this field type
return WIDGET_MAP[this.props.field.type] || TextWidget;
}
handleChange(value) {
this.props.onChange?.(value);
}
}
Composition Patterns
Container / Presentational Split
// Presentational: pure display, no data fetching
class ProductCard extends Component {
static template = xml`
<div class="product-card">
<img t-att-src="props.imageUrl" t-att-alt="props.name"/>
<h3 t-esc="props.name"/>
<p>$<t t-esc="props.price.toFixed(2)"/></p>
<button t-on-click="() => props.onAddToCart(props.id)">Add to Cart</button>
</div>
`;
static props = {
id: Number, name: String, imageUrl: String,
price: Number, onAddToCart: Function,
};
}
// Container: fetches data, passes to presentational children
class ProductListContainer extends Component {
static components = { ProductCard };
state = useState({ products: [], cartItems: [] });
setup() {
this.orm = useService("orm");
onWillStart(() => this.loadProducts());
}
async loadProducts() {
this.state.products = await this.orm.searchRead("product.product",
[["active", "=", true]], ["id","name","list_price","image_url"], { limit: 20 });
}
addToCart(productId) {
this.state.cartItems.push(productId);
}
static template = xml`
<div class="product-list">
<ProductCard
t-foreach="state.products" t-as="p" t-key="p.id"
id="p.id" name="p.name" imageUrl="p.image_url"
price="p.list_price"
onAddToCart.bind="addToCart"
/>
</div>
`;
}
Re-render Isolation
A major benefit of splitting into sub-components: OWL re-renders only the component whose state or props changed. A large monolithic component re-renders entirely on any change. Split components give you free performance optimization.
| Scenario | Monolithic component | Split into sub-components |
|---|---|---|
| User edits a text field | Entire page re-renders | Only the field component re-renders |
| One list item changes | Entire list re-renders | Only that item's component re-renders |
| Counter in sidebar updates | Header, sidebar, content all re-render | Only the counter component re-renders |
📋 Summary
- Every component used in a template must be listed in
static components = { ComponentName }. - Data flows down via props; events go up via callback props (
onChange.bind="method") orthis.trigger(). t-component="expression"renders a component class resolved at runtime — the foundation for plugin and widget registry systems.- Split components into Container (data fetching, state) and Presentational (props-driven display) for clarity and reuse.
- Sub-components isolate re-renders — only the component whose state/props changed updates, not the entire tree.
🏋️ Exercise
Build a DataTable system split into three components:
TableCell: propsvalue,type("text"|"number"|"date"|"badge"). Renders value differently per type.TableRow: propsrow(object),columns(array of{key, type}),onSelect(Function). Renders aTableCellper column. Clicking the row callsonSelect(row).DataTable: propscolumns,rows. State:selectedId. Renders aTableRowper row, usest-att-classto highlight the selected row. Shows "Nothing selected" or selected row data below the table.
FAQ
No — and this is intentional. A sub-component only receives what its parent explicitly passes as props. It cannot reach up and read parent.state.something. This isolation is what makes components reusable and testable. If a deeply nested component needs data that is far up the tree, use OWL's environment (via useEnv()) or Odoo services instead of prop drilling.
t-component renders one component whose class is determined at runtime — clean for large dynamic registries. Multiple t-if/t-elif blocks with explicit component tags is more verbose but more readable for a small number of known cases (2–4 options). Use t-component when the set of possible components is open-ended, comes from a map/registry, or grows over time.