Why this chapter? Static HTML gets you a demo; QWeb directives get you a real application. This is the vocabulary you'll be reading and writing in every single template file from here on. What is Odoo trying to solve with this? A single, consistent templating language shared across the entire platform — the same
t-if/t-foreach/t-escyou learn here work identically in every Odoo module, so knowledge transfers between projects instead of resetting each time. Real-world application: Every list view, kanban card, and dashboard widget you'll build for a client leans on these five or six directives — get comfortable here and 80% of "how do I show this data" questions answer themselves.
In the last chapter, we created our first component and linked it to a template file. That template contained static HTML, meaning it never changed. The real power of a UI framework, however, is its ability to render dynamic data that responds to user interactions and state changes.
This is where QWeb, Odoo's templating language, truly shines. QWeb templates are not just plain HTML; they are enhanced with special attributes (directives) that can display variables, run loops, make decisions, and handle user interactions. These directives are the bridge between your component's JavaScript logic and the final HTML that the user sees.
To illustrate these concepts comprehensively, let's enhance our HelloWorld component with a more realistic state structure that demonstrates all the key QWeb features.
Our Enhanced Component State (in hello_world.js):
import { Component, markup, useState } from "@odoo/owl";
export class HelloWorld extends Component {
static template = "my_odoo_module.HelloWorld";
setup() {
this.state = useState({
user: {
name: "Alice Johnson",
isAdmin: true,
avatar: "/web/static/img/user_menu_avatar.png",
lastLogin: "2024-03-15T10:30:00Z"
},
counter: 5,
tasks: [
{ id: 1, text: "Learn QWeb templating", completed: true, priority: "high" },
{ id: 2, text: "Build a component", completed: false, priority: "medium" },
{ id: 3, text: "Add user interactions", completed: false, priority: "high" },
{ id: 4, text: "Deploy to production", completed: false, priority: "low" }
],
notifications: [
{ type: "success", message: "Welcome back!" },
{ type: "warning", message: "System maintenance in 1 hour" }
],
settings: {
theme: "dark",
showCompleted: true,
maxItems: 10
},
rawHtml: markup("<span>This contains <strong>formatted</strong> HTML content.</span>")
});
}
// Methods we'll use in our template examples
getTaskColor(priority) {
const colors = { high: "danger", medium: "warning", low: "info" };
return colors[priority] || "secondary";
}
toggleTask(taskId) {
const task = this.state.tasks.find(t => t.id === taskId);
if (task) {
task.completed = !task.completed;
}
}
incrementCounter() {
this.state.counter++;
}
}
Now, let's explore how to use this rich state data in our XML template with all the essential QWeb features.
{width=100%}
Displaying Data: t-esc and t-out
The most fundamental task is displaying variables from your state. QWeb provides two primary ways to render data:
t-esc: The Safe Default Choice
The t-esc directive evaluates an expression and safely renders the result as text. This is your default choice for displaying data, as it protects against Cross-Site Scripting (XSS) attacks by automatically escaping any HTML characters.
<!-- Basic data display -->
<div class="user-profile">
<h2>Welcome, <t t-esc="state.user.name"/>!</h2>
<p>Current counter: <t t-esc="state.counter"/></p>
<p>Tasks completed: <t t-esc="state.tasks.filter(t => t.completed).length"/> / <t t-esc="state.tasks.length"/></p>
<small class="text-muted">Last login: <t t-esc="state.user.lastLogin"/></small>
</div>
Result:
<div class="user-profile">
<h2>Welcome, Alice Johnson!</h2>
<p>Current counter: 5</p>
<p>Tasks completed: 1 / 4</p>
<small class="text-muted">Last login: 2024-03-15T10:30:00Z</small>
</div>
Key Points about t-esc:
- Can execute JavaScript expressions, not just simple variables
- Automatically escapes HTML to prevent XSS attacks
- Perfect for user-generated content or any untrusted data
- Supports complex expressions like filtering arrays or calling methods
t-out: Rendering Trusted HTML Content
Sometimes you have a variable containing actual HTML that needs to be rendered as markup. Using t-esc would display the HTML tags as plain text. For these cases, OWL 2.0 provides t-out.
By default, t-out escapes its content exactly like t-esc. To render actual HTML, the value must be explicitly marked as safe with OWL's markup() helper—that's why we wrapped rawHtml in markup() in the component above. (The old t-raw directive from OWL 1.x was removed precisely because it made rendering untrusted HTML too easy.)
Security Warning: Only wrap content in markup() when you completely trust it (e.g., HTML generated by your own system or sanitized content). Never use it with user-provided data.
<!-- Rendering pre-formatted HTML content -->
<div class="content-area">
<div class="formatted-content">
<t t-out="state.rawHtml"/>
</div>
</div>
Result:
<div class="content-area">
<div class="formatted-content">
<span>This contains <strong>formatted</strong> HTML content.</span>
</div>
</div>
Dynamic Attributes: t-att and t-attf
Static attributes are straightforward, but dynamic attributes based on your component's state are where QWeb becomes powerful.
Simple Dynamic Attributes with t-att
The t-att- prefix followed by the attribute name lets you set attributes dynamically:
<!-- Dynamic classes and attributes -->
<div t-att-class="state.settings.theme === 'dark' ? 'theme-dark' : 'theme-light'">
<img t-att-src="state.user.avatar"
t-att-alt="state.user.name + ' avatar'"
class="user-avatar"/>
<button class="btn"
t-att-disabled="state.counter >= 10"
t-on-click="incrementCounter">
Count: <t t-esc="state.counter"/>
</button>
</div>
Complex Dynamic Attributes with t-attf
For more complex attribute values that need string interpolation, use t-attf- (attribute format) with {{}} placeholders:
<!-- String interpolation in attributes -->
<div t-attf-id="user-panel-{{state.user.name.replace(' ', '-').toLowerCase()}}"
t-attf-data-user-id="{{state.user.id || 'anonymous'}}"
t-attf-style="background-color: {{state.settings.theme === 'dark' ? '#2c3e50' : '#ecf0f1'}};">
<div class="status-badge"
t-attf-class="badge badge-{{state.user.isAdmin ? 'success' : 'secondary'}}">
<t t-esc="state.user.isAdmin ? 'Administrator' : 'User'"/>
</div>
</div>
Object-Based Class Binding
A powerful pattern for conditional classes:
<!-- Object syntax for multiple conditional classes -->
<div t-att-class="{
'admin-panel': state.user.isAdmin,
'user-panel': !state.user.isAdmin,
'theme-dark': state.settings.theme === 'dark',
'has-notifications': state.notifications.length > 0
}">
<h3>Dashboard</h3>
</div>
Conditionals: t-if, t-elif, t-else
Control the visibility and rendering of template sections based on your state:
Basic Conditionals
<div class="user-status">
<t t-if="state.user.isAdmin">
<div class="alert alert-info">
<i class="fa fa-crown"></i>
You have administrator privileges.
</div>
</t>
<t t-elif="state.tasks.filter(t => !t.completed).length > 5">
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
You have many pending tasks!
</div>
</t>
<t t-else="">
<div class="alert alert-success">
<i class="fa fa-check-circle"></i>
Everything looks good!
</div>
</t>
</div>
Nested Conditionals and Complex Logic
<!-- More complex conditional logic -->
<div class="task-summary">
<t t-if="state.tasks.length > 0">
<h4>Task Overview</h4>
<t t-if="state.settings.showCompleted">
<p>Showing all tasks (including completed)</p>
</t>
<t t-else="">
<p>Showing only pending tasks</p>
</t>
<!-- Show warning for high-priority incomplete tasks -->
<t t-if="state.tasks.filter(t => !t.completed and t.priority === 'high').length > 0">
<div class="alert alert-danger">
<strong>Attention:</strong> You have high-priority tasks pending!
</div>
</t>
</t>
<t t-else="">
<div class="empty-state">
<p>No tasks yet. Create your first task to get started!</p>
</div>
</t>
</div>
Loops: t-foreach and t-as
Render lists of items from arrays in your state:
Basic Loop Structure
t-foreach: Specifies the array to iterate overt-as: Names the variable for each itemt-key: Provides a unique key for optimal performance (crucial for OWL's rendering efficiency)
<div class="task-list">
<h3>My Tasks (<t t-esc="state.tasks.length"/>)</h3>
<div class="list-group">
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<div class="list-group-item d-flex justify-content-between align-items-center">
<div class="task-content">
<h5 t-att-class="{ 'text-decoration-line-through': task.completed }">
<t t-esc="task.text"/>
</h5>
<small t-attf-class="badge bg-{{getTaskColor(task.priority)}}">
<t t-esc="task.priority"/> priority
</small>
</div>
<div class="task-actions">
<button class="btn btn-sm btn-outline-primary"
t-on-click="() => this.toggleTask(task.id)">
<i t-att-class="task.completed ? 'fa fa-undo' : 'fa fa-check'"></i>
<t t-esc="task.completed ? 'Undo' : 'Complete'"/>
</button>
</div>
</div>
</t>
</div>
</div>
Advanced Loop Patterns
Loop with Index
<!-- Access the current index with _index -->
<ol class="numbered-list">
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<li>
<strong>Item #<t t-esc="task_index + 1"/>:</strong>
<t t-esc="task.text"/>
</li>
</t>
</ol>
Filtered Loops
<!-- Show only incomplete tasks -->
<div class="pending-tasks">
<h4>Pending Tasks</h4>
<t t-foreach="state.tasks.filter(t => !t.completed)" t-as="task" t-key="task.id">
<div class="task-item alert alert-light">
<t t-esc="task.text"/>
</div>
</t>
</div>
Nested Loops
<!-- Group tasks by priority -->
<div class="tasks-by-priority">
<t t-foreach="['high', 'medium', 'low']" t-as="priority" t-key="priority">
<t t-set="priorityTasks" t-value="state.tasks.filter(t => t.priority === priority)"/>
<t t-if="priorityTasks.length > 0">
<div class="priority-section">
<h4 t-attf-class="text-{{getTaskColor(priority)}}">
<t t-esc="priority.toUpperCase()"/> Priority
(<t t-esc="priorityTasks.length"/>)
</h4>
<t t-foreach="priorityTasks" t-as="task" t-key="task.id">
<div class="task-item">
<t t-esc="task.text"/>
</div>
</t>
</div>
</t>
</t>
</div>
Advanced QWeb Features
Setting Variables with t-set
Create local variables within your template:
<!-- Calculate and store values for reuse -->
<div class="statistics">
<t t-set="completedTasks" t-value="state.tasks.filter(t => t.completed)"/>
<t t-set="completionRate" t-value="Math.round((completedTasks.length / state.tasks.length) * 100)"/>
<div class="progress mb-3">
<div class="progress-bar"
t-attf-style="width: {{completionRate}}%"
t-attf-aria-valuenow="{{completionRate}}">
<t t-esc="completionRate"/>% Complete
</div>
</div>
<p>You've completed <t t-esc="completedTasks.length"/> out of <t t-esc="state.tasks.length"/> tasks!</p>
</div>
Calling Component Methods
<!-- Methods can be called directly in templates -->
<div class="task-priority-colors">
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<span t-attf-class="badge bg-{{getTaskColor(task.priority)}}">
<t t-esc="task.text"/>
</span>
</t>
</div>
Complete Example: Putting It All Together
Here's a comprehensive template that demonstrates all the concepts we've covered. We'll walk through it in three pieces so each part is easier to digest.
Part 1 — the header and notifications. This section renders a personalized greeting, toggling a dark-theme class with the object syntax of t-att-class, and switches the badge color depending on state.user.isAdmin. Notifications are rendered with t-foreach, each one dismissible:
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="my_odoo_module.HelloWorld" owl="1">
<div class="hello-world-component p-4"
t-att-class="{ 'theme-dark': state.settings.theme === 'dark' }">
<!-- User Header with Dynamic Content -->
<div class="user-header d-flex align-items-center mb-4">
<img t-att-src="state.user.avatar"
t-att-alt="state.user.name + ' avatar'"
class="rounded-circle me-3" width="64" height="64"/>
<div>
<h2 class="mb-1">Welcome, <t t-esc="state.user.name"/>!</h2>
<span t-att-class="{
'badge': true,
'bg-success': state.user.isAdmin,
'bg-secondary': !state.user.isAdmin
}">
<t t-esc="state.user.isAdmin ? 'Administrator' : 'User'"/>
</span>
</div>
</div>
<!-- Notifications -->
<t t-if="state.notifications.length > 0">
<div class="notifications mb-4">
<t t-foreach="state.notifications" t-as="notification" t-key="notification_index">
<div t-attf-class="alert alert-{{notification.type}} alert-dismissible">
<t t-esc="notification.message"/>
</div>
</t>
</div>
</t>
Part 2 — the counter and task list (same template, continued). The counter section disables its button once state.settings.maxItems is reached. The tasks section uses t-set to precompute a completed count once, nests a t-if inside t-foreach to respect the showCompleted setting, and falls back to a t-else empty state when there are no tasks:
<!-- Counter Section -->
<div class="counter-section mb-4">
<div class="card">
<div class="card-body text-center">
<h3>Counter: <t t-esc="state.counter"/></h3>
<button class="btn btn-primary"
t-att-disabled="state.counter >= state.settings.maxItems"
t-on-click="incrementCounter">
<i class="fa fa-plus"></i>
Increment
</button>
<t t-if="state.counter >= state.settings.maxItems">
<p class="text-warning mt-2">Maximum limit reached!</p>
</t>
</div>
</div>
</div>
<!-- Tasks Section -->
<div class="tasks-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Tasks</h3>
<t t-set="completedCount" t-value="state.tasks.filter(t => t.completed).length"/>
<span class="badge bg-info">
<t t-esc="completedCount"/> / <t t-esc="state.tasks.length"/> completed
</span>
</div>
<t t-if="state.tasks.length > 0">
<div class="task-list">
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<!-- Only show task if settings allow or it's incomplete -->
<t t-if="state.settings.showCompleted or !task.completed">
<div class="card mb-2"
t-att-class="{ 'opacity-50': task.completed }">
<div class="card-body d-flex justify-content-between align-items-center">
<div>
<h5 t-att-class="{ 'text-decoration-line-through': task.completed }">
<t t-esc="task.text"/>
</h5>
<small t-attf-class="badge bg-{{getTaskColor(task.priority)}}">
<t t-esc="task.priority"/> priority
</small>
</div>
<button class="btn btn-sm btn-outline-primary"
t-on-click="() => this.toggleTask(task.id)">
<i t-att-class="task.completed ? 'fa fa-undo' : 'fa fa-check'"></i>
<t t-esc="task.completed ? 'Undo' : 'Complete'"/>
</button>
</div>
</div>
</t>
</t>
</div>
</t>
<t t-else="">
<div class="empty-state text-center py-5">
<i class="fa fa-tasks fa-3x text-muted mb-3"></i>
<h4 class="text-muted">No tasks yet</h4>
<p class="text-muted">Create your first task to get started!</p>
</div>
</t>
</div>
Part 3 — trusted HTML, and closing the template. Finally, the pre-sanitized HTML content is rendered with t-out, and we close every tag we opened above:
<!-- Raw HTML Example -->
<div class="html-content mt-4">
<h4>Formatted Content:</h4>
<div class="border p-3 rounded">
<t t-out="state.rawHtml"/>
</div>
</div>
</div>
</t>
</templates>
Best Practices and Tips
Performance Considerations:
- Always use t-key in loops for optimal rendering performance
- Prefer t-esc (or plain t-out) over markup() unless you specifically need HTML rendering
- Complex calculations should be done in JavaScript methods, not in templates
Security:
- Never wrap user-provided content in markup()
- Always validate data before rendering
- Use t-esc for any dynamic content that could contain HTML
Maintainability:
- Keep template logic simple; move complex logic to component methods
- Use descriptive variable names in t-as directives
- Group related template sections with comments
Common Patterns:
- Use t-set for complex calculations that are used multiple times
- Combine t-if with t-foreach for conditional rendering of lists
- Use object syntax for t-att-class when dealing with multiple conditional classes
These four fundamental concepts—displaying data, dynamic attributes, conditionals, and loops—are the building blocks of QWeb. By mastering them and understanding their advanced patterns, you can build sophisticated, data-driven user interfaces that react instantly and efficiently to changes in your component's state.
TL;DR: QWeb directives (t-esc/t-out, t-att*, t-if/t-elif/t-else, t-foreach+t-key) are how your template reads and reacts to your component's data — always pair loops with a stable t-key, and never put user content in markup().
Try it yourself: This chapter's example is available as an installable Odoo 19 addon: simplifyit_owl_book_ch6_ex1. Installation instructions are in the repository README.
Common Pitfalls
Pitfall 1: Forgetting t-key in t-foreach
<!-- Wrong - no t-key: OWL can't track which item is which across re-renders -->
<t t-foreach="state.tasks" t-as="task">
<div><t t-esc="task.text"/></div>
</t>
<!-- Correct - a stable, unique key per item -->
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<div><t t-esc="task.text"/></div>
</t>
Without t-key, OWL may reuse the wrong DOM node for the wrong item, causing stale text, lost input focus, or flickering when the list changes.
Pitfall 2: Reaching for t-raw
<!-- Wrong - t-raw was removed in OWL 2 -->
<div t-raw="state.rawHtml"/>
<!-- Correct - explicitly mark the string as safe, then use t-out -->
<!-- in the component: this.state.rawHtml = markup("<b>Bold</b>"); -->
<div t-out="state.rawHtml"/>
t-out renders plain text safely by default; it only renders HTML when the value was explicitly wrapped in markup() in JavaScript. This two-step requirement exists specifically to prevent accidental XSS.
Pitfall 3: Confusing t-att-class with t-attf-class
<!-- t-att-class expects a JS expression: either a string OR an object of {className: boolean} -->
<div t-att-class="{ 'active': state.isActive }"/>
<!-- t-attf-class expects a plain string with {{ }} placeholders for interpolation -->
<div t-attf-class="badge bg-{{state.color}}"/>
Mixing them up—passing an object to t-attf-class or a {{ }} template to t-att-class—will either throw an error or silently render the wrong class.
Pitfall 4: Mutating State That Isn't Reactive Yet
<!-- The template only re-renders when a *reactive* property changes. -->
<!-- If state.tasks was never wrapped in useState() in the component's setup(), -->
<!-- pushing to it here has no visible effect on the page. -->
If a list or object stops updating the UI, the first thing to check is whether it was created with useState(...) in the component (covered in the next chapter)—not whether the template syntax is wrong.
Exercises
- Toggle a class by hand. Add a new
<button>to the counter section that calls atoggleHighlight()method, and uset-att-class(object syntax) to add abg-warningclass to the card only whilestate.highlightedistrue. - Fix the missing key. Take the task list from the "Complete Example" above, remove
t-key="task.id", and reload the page after reorderingstate.tasksin the console. Notice what breaks, then restoret-keyand confirm it's fixed. - Build a filtered loop. Add a new section that lists only tasks with
priority === "high", using at-foreachcombined with at-ifinside it (as shown in "Advanced Loop Patterns").
What's Next?
Templates can only display what your component gives them. Chapter 7 dives into where that data actually lives — useState and OWL's reactivity system — so you understand why changing a value automatically triggers everything you just learned about re-rendering.