¿Por qué este capítulo? El HTML estático te da una demo; las directivas de QWeb te dan una aplicación real. Este es el vocabulario que vas a leer y escribir en cada archivo de plantilla de aquí en adelante. ¿Qué trata de resolver Odoo con esto? Un único lenguaje de plantillas consistente compartido en toda la plataforma — el mismo
t-if/t-foreach/t-escque aprendes aquí funciona igual en cualquier módulo de Odoo, así que el conocimiento se transfiere entre proyectos en vez de reiniciarse cada vez. Aplicación en la vida real: Cada vista de lista, tarjeta kanban y widget de dashboard que construyas para un cliente se apoya en estas cinco o seis directivas — domínalas aquí y el 80% de las preguntas de "cómo muestro este dato" se responden solas.
En el capítulo anterior, creamos nuestro primer componente y lo vinculamos a un archivo de plantilla. Esa plantilla contenía HTML estático, lo que significa que nunca cambiaba. Sin embargo, el verdadero poder de un framework de UI es su capacidad para renderizar datos dinámicos que responden a las interacciones del usuario y cambios de estado.
Aquí es donde QWeb, el lenguaje de plantillas de Odoo, brilla verdaderamente. Las plantillas QWeb no son solo HTML simple; están mejoradas con atributos especiales (directivas) que pueden mostrar variables, ejecutar bucles, tomar decisiones y manejar interacciones del usuario. Estas directivas son el puente entre la lógica JavaScript de tu componente y el HTML final que ve el usuario.
Para ilustrar estos conceptos de manera integral, mejoremos nuestro componente HelloWorld con una estructura de estado más realista que demuestre todas las características clave de QWeb.
El Estado de Nuestro Componente Mejorado (en 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: "Alicia Johnson",
isAdmin: true,
avatar: "/web/static/img/user_menu_avatar.png",
lastLogin: "2024-03-15T10:30:00Z"
},
counter: 5,
tasks: [
{ id: 1, text: "Aprender plantillas QWeb", completed: true, priority: "high" },
{ id: 2, text: "Construir un componente", completed: false, priority: "medium" },
{ id: 3, text: "Añadir interacciones de usuario", completed: false, priority: "high" },
{ id: 4, text: "Desplegar a producción", completed: false, priority: "low" }
],
notifications: [
{ type: "success", message: "¡Bienvenido de vuelta!" },
{ type: "warning", message: "Mantenimiento del sistema en 1 hora" }
],
settings: {
theme: "dark",
showCompleted: true,
maxItems: 10
},
rawHtml: markup("<span>Esto contiene contenido HTML <strong>formateado</strong>.</span>")
});
}
// Métodos que usaremos en nuestros ejemplos de plantilla
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++;
}
}
Ahora, exploremos cómo usar estos ricos datos de state en nuestra plantilla XML con todas las características esenciales de QWeb.
{width=100%}
Mostrando Datos: t-esc y t-out
La tarea más fundamental es mostrar variables de tu estado. QWeb proporciona dos formas principales para renderizar datos:
t-esc: La Opción Segura por Defecto
La directiva t-esc evalúa una expresión y renderiza de forma segura el resultado como texto. Esta es tu opción por defecto para mostrar datos, ya que protege contra ataques de Cross-Site Scripting (XSS) escapando automáticamente cualquier carácter HTML.
<!-- Mostrar datos básicos -->
<div class="user-profile">
<h2>¡Bienvenido, <t t-esc="state.user.name"/>!</h2>
<p>Contador actual: <t t-esc="state.counter"/></p>
<p>Tareas completadas: <t t-esc="state.tasks.filter(t => t.completed).length"/> / <t t-esc="state.tasks.length"/></p>
<small class="text-muted">Último login: <t t-esc="state.user.lastLogin"/></small>
</div>
Resultado:
<div class="user-profile">
<h2>¡Bienvenido, Alicia Johnson!</h2>
<p>Contador actual: 5</p>
<p>Tareas completadas: 1 / 4</p>
<small class="text-muted">Último login: 2024-03-15T10:30:00Z</small>
</div>
Puntos Clave sobre t-esc:
- Puede ejecutar expresiones JavaScript, no solo variables simples
- Automáticamente escapa HTML para prevenir ataques XSS
- Perfecto para contenido generado por usuarios o cualquier dato no confiable
- Soporta expresiones complejas como filtrar arrays o llamar métodos
t-out: Renderizando Contenido HTML Confiable
A veces tienes una variable que contiene HTML real que necesita ser renderizado como marcado. Usar t-esc mostraría las etiquetas HTML como texto plano. Para estos casos, OWL 2.0 proporciona t-out.
Por defecto, t-out escapa su contenido exactamente igual que t-esc. Para renderizar HTML real, el valor debe marcarse explícitamente como seguro con el helper markup() de OWL—por eso envolvimos rawHtml en markup() en el componente de arriba. (La antigua directiva t-raw de OWL 1.x fue eliminada precisamente porque hacía demasiado fácil renderizar HTML no confiable.)
Advertencia de Seguridad: Solo envuelve contenido en markup() cuando confíes completamente en él (ej., HTML generado por tu propio sistema o contenido sanitizado). Nunca lo uses con datos proporcionados por el usuario.
<!-- Renderizando contenido HTML pre-formateado -->
<div class="content-area">
<div class="formatted-content">
<t t-out="state.rawHtml"/>
</div>
</div>
Resultado:
<div class="content-area">
<div class="formatted-content">
<span>Esto contiene contenido HTML <strong>formateado</strong>.</span>
</div>
</div>
Atributos Dinámicos: t-att y t-attf
Los atributos estáticos son directos, pero los atributos dinámicos basados en el estado de tu componente son donde QWeb se vuelve poderoso.
Atributos Dinámicos Simples con t-att
El prefijo t-att- seguido del nombre del atributo te permite establecer atributos dinámicamente:
<!-- Clases y atributos dinámicos -->
<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">
Contar: <t t-esc="state.counter"/>
</button>
</div>
Atributos Dinámicos Complejos con t-attf
Para valores de atributos más complejos que necesitan interpolación de cadenas, usa t-attf- (formato de atributo) con marcadores {{}}:
<!-- Interpolación de cadenas en atributos -->
<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 ? 'Administrador' : 'Usuario'"/>
</div>
</div>
Vinculación de Clases Basada en Objetos
Un patrón poderoso para clases condicionales:
<!-- Sintaxis de objeto para múltiples clases condicionales -->
<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>Panel de Control</h3>
</div>
Condicionales: t-if, t-elif, t-else
Controla la visibilidad y renderizado de secciones de plantilla basándote en tu estado:
Condicionales Básicos
<div class="user-status">
<t t-if="state.user.isAdmin">
<div class="alert alert-info">
<i class="fa fa-crown"></i>
Tienes privilegios de administrador.
</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>
¡Tienes muchas tareas pendientes!
</div>
</t>
<t t-else="">
<div class="alert alert-success">
<i class="fa fa-check-circle"></i>
¡Todo se ve bien!
</div>
</t>
</div>
Condicionales Anidados y Lógica Compleja
<!-- Lógica condicional más compleja -->
<div class="task-summary">
<t t-if="state.tasks.length > 0">
<h4>Vista General de Tareas</h4>
<t t-if="state.settings.showCompleted">
<p>Mostrando todas las tareas (incluyendo completadas)</p>
</t>
<t t-else="">
<p>Mostrando solo tareas pendientes</p>
</t>
<!-- Mostrar advertencia para tareas incompletas de alta prioridad -->
<t t-if="state.tasks.filter(t => !t.completed and t.priority === 'high').length > 0">
<div class="alert alert-danger">
<strong>Atención:</strong> ¡Tienes tareas de alta prioridad pendientes!
</div>
</t>
</t>
<t t-else="">
<div class="empty-state">
<p>No hay tareas aún. ¡Crea tu primera tarea para empezar!</p>
</div>
</t>
</div>
Bucles: t-foreach y t-as
Renderiza listas de elementos de arrays en tu estado:
Estructura Básica de Bucle
t-foreach: Especifica el array sobre el cual iterart-as: Nombra la variable para cada elementot-key: Proporciona una clave única para un rendimiento óptimo (crucial para la eficiencia de renderizado de OWL)
<div class="task-list">
<h3>Mis Tareas (<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)}}">
Prioridad <t t-esc="task.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 ? 'Deshacer' : 'Completar'"/>
</button>
</div>
</div>
</t>
</div>
</div>
Patrones Avanzados de Bucles
Bucle con Índice
<!-- Acceder al índice actual con _index -->
<ol class="numbered-list">
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<li>
<strong>Elemento #<t t-esc="task_index + 1"/>:</strong>
<t t-esc="task.text"/>
</li>
</t>
</ol>
Bucles Filtrados
<!-- Mostrar solo tareas incompletas -->
<div class="pending-tasks">
<h4>Tareas Pendientes</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>
Bucles Anidados
<!-- Agrupar tareas por prioridad -->
<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)}}">
Prioridad <t t-esc="priority.toUpperCase()"/>
(<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>
Características Avanzadas de QWeb
Estableciendo Variables con t-set
Crea variables locales dentro de tu plantilla:
<!-- Calcular y almacenar valores para reutilización -->
<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"/>% Completo
</div>
</div>
<p>¡Has completado <t t-esc="completedTasks.length"/> de <t t-esc="state.tasks.length"/> tareas!</p>
</div>
Llamando Métodos del Componente
<!-- Los métodos pueden ser llamados directamente en las plantillas -->
<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>
Ejemplo Completo: Juntando Todo
Aquí tienes una plantilla integral que demuestra todos los conceptos que hemos cubierto. La veremos en tres partes para que sea más fácil de digerir.
Parte 1 — el encabezado y las notificaciones. Esta sección renderiza un saludo personalizado, alterna una clase de tema oscuro con la sintaxis de objeto de t-att-class, y cambia el color de la insignia según state.user.isAdmin. Las notificaciones se renderizan con t-foreach, cada una descartable:
<?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' }">
<!-- Encabezado de Usuario con Contenido Dinámico -->
<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">¡Bienvenido, <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 ? 'Administrador' : 'Usuario'"/>
</span>
</div>
</div>
<!-- Notificaciones -->
<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>
Parte 2 — el contador y la lista de tareas (misma plantilla, continuación). La sección del contador deshabilita su botón al alcanzar state.settings.maxItems. La sección de tareas usa t-set para precalcular una sola vez el conteo de completadas, anida un t-if dentro de t-foreach para respetar la configuración showCompleted, y recurre a un t-else para el estado vacío cuando no hay tareas:
<!-- Sección del Contador -->
<div class="counter-section mb-4">
<div class="card">
<div class="card-body text-center">
<h3>Contador: <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>
Incrementar
</button>
<t t-if="state.counter >= state.settings.maxItems">
<p class="text-warning mt-2">¡Límite máximo alcanzado!</p>
</t>
</div>
</div>
</div>
<!-- Sección de Tareas -->
<div class="tasks-section">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3>Tareas</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"/> completadas
</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">
<!-- Solo mostrar tarea si la configuración lo permite o está incompleta -->
<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)}}">
Prioridad <t t-esc="task.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 ? 'Deshacer' : 'Completar'"/>
</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 hay tareas aún</h4>
<p class="text-muted">¡Crea tu primera tarea para empezar!</p>
</div>
</t>
</div>
Parte 3 — HTML confiable, y cerrando la plantilla. Finalmente, el contenido HTML pre-sanitizado se renderiza con t-out, y cerramos todas las etiquetas que abrimos arriba:
<!-- Ejemplo de HTML Crudo -->
<div class="html-content mt-4">
<h4>Contenido Formateado:</h4>
<div class="border p-3 rounded">
<t t-out="state.rawHtml"/>
</div>
</div>
</div>
</t>
</templates>
Mejores Prácticas y Consejos
Consideraciones de Rendimiento:
- Siempre usa t-key en bucles para un rendimiento de renderizado óptimo
- Prefiere t-esc (o t-out sin markup()) a menos que específicamente necesites renderizado HTML
- Los cálculos complejos deberían hacerse en métodos JavaScript, no en plantillas
Seguridad:
- Nunca envuelvas contenido proporcionado por usuarios en markup()
- Siempre valida los datos antes de renderizar
- Usa t-esc para cualquier contenido dinámico que pueda contener HTML
Mantenibilidad:
- Mantén la lógica de plantillas simple; mueve la lógica compleja a métodos del componente
- Usa nombres descriptivos de variables en directivas t-as
- Agrupa secciones de plantilla relacionadas con comentarios
Patrones Comunes:
- Usa t-set para cálculos complejos que se usan múltiples veces
- Combina t-if con t-foreach para renderizado condicional de listas
- Usa sintaxis de objeto para t-att-class cuando manejes múltiples clases condicionales
Estos cuatro conceptos fundamentales—mostrar datos, atributos dinámicos, condicionales y bucles—son los bloques de construcción de QWeb. Al dominarlos y entender sus patrones avanzados, puedes construir interfaces de usuario sofisticadas y basadas en datos que reaccionan instantánea y eficientemente a los cambios en el estado de tu componente.
TL;DR: Las directivas de QWeb (t-esc/t-out, t-att*, t-if/t-elif/t-else, t-foreach+t-key) son cómo tu plantilla lee y reacciona a los datos de tu componente — siempre combina bucles con un t-key estable, y nunca pongas contenido de usuario en markup().
Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch6_ex1. Las instrucciones de instalación están en el README del repositorio.
Errores Comunes
Error 1: Olvidar t-key en t-foreach
<!-- Incorrecto - sin t-key: OWL no puede rastrear qué elemento es cuál entre renderizados -->
<t t-foreach="state.tasks" t-as="task">
<div><t t-esc="task.text"/></div>
</t>
<!-- Correcto - una clave única y estable por elemento -->
<t t-foreach="state.tasks" t-as="task" t-key="task.id">
<div><t t-esc="task.text"/></div>
</t>
Sin t-key, OWL puede reutilizar el nodo DOM equivocado para el elemento equivocado, causando texto desactualizado, pérdida de foco en inputs, o parpadeo cuando la lista cambia.
Error 2: Recurrir a t-raw
<!-- Incorrecto - t-raw fue eliminado en OWL 2 -->
<div t-raw="state.rawHtml"/>
<!-- Correcto - marca explícitamente el string como seguro, luego usa t-out -->
<!-- en el componente: this.state.rawHtml = markup("<b>Negrita</b>"); -->
<div t-out="state.rawHtml"/>
t-out renderiza texto plano de forma segura por defecto; solo renderiza HTML cuando el valor fue envuelto explícitamente en markup() en JavaScript. Este requisito de dos pasos existe específicamente para prevenir XSS accidental.
Error 3: Confundir t-att-class con t-attf-class
<!-- t-att-class espera una expresión JS: un string O un objeto {nombreClase: booleano} -->
<div t-att-class="{ 'active': state.isActive }"/>
<!-- t-attf-class espera un string plano con marcadores {{ }} para interpolación -->
<div t-attf-class="badge bg-{{state.color}}"/>
Confundirlos—pasar un objeto a t-attf-class o una plantilla {{ }} a t-att-class—lanzará un error o renderizará silenciosamente la clase equivocada.
Error 4: Mutar Estado que Aún No es Reactivo
<!-- La plantilla solo se vuelve a renderizar cuando cambia una propiedad *reactiva*. -->
<!-- Si state.tasks nunca fue envuelto en useState() en el setup() del componente, -->
<!-- hacer push aquí no tiene efecto visible en la página. -->
Si una lista u objeto deja de actualizar la interfaz, lo primero que hay que revisar es si fue creado con useState(...) en el componente (lo veremos en el próximo capítulo)—no si la sintaxis de la plantilla está mal.
Ejercicios
- Alterna una clase a mano. Añade un nuevo
<button>a la sección del contador que llame a un métodotoggleHighlight(), y usat-att-class(sintaxis de objeto) para añadir una clasebg-warninga la tarjeta solo mientrasstate.highlightedseatrue. - Arregla la clave faltante. Toma la lista de tareas del "Ejemplo Completo" de arriba, elimina
t-key="task.id", y recarga la página después de reordenarstate.tasksdesde la consola. Observa qué se rompe, luego restaurat-keyy confirma que se arregla. - Construye un bucle filtrado. Añade una nueva sección que liste solo las tareas con
priority === "high", usando unt-foreachcombinado con unt-ifdentro (como se muestra en "Patrones Avanzados de Bucles").
¿Qué Sigue?
Las plantillas solo pueden mostrar lo que tu componente les da. El Capítulo 7 profundiza en dónde vive realmente ese dato — useState y el sistema de reactividad de OWL — para que entiendas por qué cambiar un valor dispara automáticamente todo lo que acabas de aprender sobre re-renderizado.