¿Por qué este capítulo? Todo componente OWL que vayas a escribir es una
class, cada prop y resultado del ORM que manejes se beneficia del destructuring, y cada llamada al servidor es asíncrona — este es el capítulo donde esas cuatro piezas dejan de ser sintaxis abstracta y pasan a ser cómo escribirás código a diario. ¿Qué trata de resolver Odoo con esto? El modelo de componentes de OWL (class ... extends Component), su sistema de módulos (/** @odoo-module **/eimport/export) y sus servicios (orm,notification, etc.) están construidos sobre estas cuatro características de JavaScript — no hay forma de escribir un componente OWL sin ellas. Aplicación en la vida real: Los patrones deasync/awaitytry/catchde aquí son exactamente lo que usarás cada vez que un componente hable con el servidor de Odoo — si te equivocas aquí, obtienes fallos silenciosos o errores de "unhandled promise rejection" en producción.
En el capítulo anterior, cubrimos los bloques de construcción básicos de JavaScript moderno. Ahora, exploraremos cuatro conceptos más avanzados que no solo son importantes—son la estructura misma sobre la cual está construido OWL. Entender clases, destructuring, módulos y código asíncrono es el paso final antes de que puedas realmente entender cómo funciona un componente OWL.
La Palabra Clave class: Un Plano para Componentes
Un componente OWL es, en su núcleo, una clase de JavaScript. Una clase es un plano para crear objetos. Agrupa datos (propiedades) y las funciones que operan sobre esos datos (métodos) en un paquete único y reutilizable.
Piensa en una class como el plano arquitectónico de una casa. Puedes usar el mismo plano para construir muchas casas idénticas. Cada casa que construyes es una "instancia" de la clase.
Definamos una clase simple Dog (Perro):
class Dog {
// El constructor es un método especial que se ejecuta cuando se crea una nueva instancia.
// Se usa para configurar propiedades iniciales.
constructor(name, breed) {
this.name = name;
this.breed = breed;
this.energy = 100;
}
// Un método es una función que pertenece a la clase.
bark() {
console.log(`${this.name} dice: ¡Guau!`);
}
play() {
this.energy -= 10;
console.log(`${this.name} está jugando. La energía ahora es ${this.energy}.`);
}
// Los métodos pueden devolver información sobre la instancia
getInfo() {
return `${this.name} es un ${this.breed} con ${this.energy} de energía.`;
}
}
// Ahora, creemos dos instancias (dos perros diferentes) desde nuestro plano.
const dog1 = new Dog("Rex", "Pastor Alemán");
const dog2 = new Dog("Buddy", "Golden Retriever");
dog1.bark(); // Salida: Rex dice: ¡Guau!
dog2.play(); // Salida: Buddy está jugando. La energía ahora es 90.
console.log(dog1.getInfo()); // Salida: Rex es un Pastor Alemán con 100 de energía.
Extendiendo una Clase
El verdadero poder viene de la palabra clave extends. Te permite crear una nueva clase que hereda todas las propiedades y métodos de una existente. Este es el prerequisito directo para entender los componentes OWL, que siempre comienzan con class MyComponent extends Component.
Creemos una clase especializada Puppy (Cachorro) que herede de Dog:
class Puppy extends Dog {
constructor(name, breed) {
// Llamar al constructor padre con super()
super(name, breed);
// Los cachorros empiezan con menos energía
this.energy = 50;
this.isTeething = true;
}
// Podemos añadir un nuevo método que solo tienen los cachorros.
chewShoe() {
if (this.isTeething) {
console.log(`${this.name} está masticando un zapato! ¡Mal cachorro!`);
} else {
console.log(`${this.name} es muy viejo para masticar zapatos.`);
}
}
// También podemos sobrescribir un método existente.
bark() {
console.log(`${this.name} dice: ¡Yip! ¡Yip!`); // El ladrido de un cachorro es diferente.
}
// Podemos extender métodos existentes mientras usamos la funcionalidad del padre
play() {
super.play(); // Llamar al método play del padre
if (this.energy < 20) {
console.log(`${this.name} se está cansando y podría dormir pronto.`);
}
}
}
const myPuppy = new Puppy("Leo", "Labrador");
myPuppy.play(); // Heredado de Dog, pero con comportamiento específico de cachorro
myPuppy.bark(); // Sobrescrito. Salida: Leo dice: ¡Yip! ¡Yip!
myPuppy.chewShoe(); // Nuevo método. Salida: Leo está masticando un zapato! ¡Mal cachorro!
Cuando escribes class MyComponent extends Component, estás creando una nueva clase que hereda todas las características poderosas de la clase base Component de OWL, tal como nuestro cachorro heredó del Perro.
Destructuring Assignment: Extrayendo Datos con Estilo
Destructuring es una característica poderosa que te permite extraer valores de arrays y objetos en variables separadas. Es una de las características más elegantes de JavaScript moderno y la verás en todas partes en el código OWL.
Object Destructuring: Desempacando Propiedades
En lugar de acceder a propiedades de objetos de la forma antigua, destructuring te permite extraer múltiples valores en una declaración limpia:
// La forma antigua y repetitiva
const contact = { name: "Juan Pérez", email: "[email protected]", age: 35, department: "Ventas" };
const name = contact.name;
const email = contact.email;
const age = contact.age;
// La forma moderna y limpia con destructuring
const contact = { name: "Juan Pérez", email: "[email protected]", age: 35, department: "Ventas" };
// Extraer múltiples propiedades en una línea
const { name, email, age } = contact;
console.log(name); // "Juan Pérez"
console.log(email); // "[email protected]"
console.log(age); // 35
Patrones Avanzados de Object Destructuring
Renombrando variables durante destructuring:
const contact = { name: "Juan Pérez", email: "[email protected]" };
// Renombrar variables durante destructuring
const { name: fullName, email: emailAddress } = contact;
console.log(fullName); // "Juan Pérez"
console.log(emailAddress); // "[email protected]"
Estableciendo valores por defecto:
const contact = { name: "Juan Pérez" }; // Sin propiedad email
// Proporcionar valores por defecto para propiedades faltantes
const { name, email = "[email protected]", age = 0 } = contact;
console.log(email); // "[email protected]"
console.log(age); // 0
Destructuring anidado:
const customer = {
name: "Acme Corp",
address: {
street: "123 Business Ave",
city: "Ciudad Comercial",
country: "España"
},
contact: {
phone: "555-0123",
email: "[email protected]"
}
};
// Extraer propiedades anidadas directamente
const {
name,
address: { city, country },
contact: { email }
} = customer;
console.log(`${name} está ubicada en ${city}, ${country}`);
console.log(`Contáctalos en ${email}`);
Array Destructuring: Extrayendo por Posición
Array destructuring extrae valores basado en su posición en el array:
const colors = ["rojo", "verde", "azul", "amarillo"];
// Extraer los primeros elementos
const [primary, secondary, tertiary] = colors;
console.log(primary); // "rojo"
console.log(secondary); // "verde"
console.log(tertiary); // "azul"
// Saltar elementos que no necesitas
const [first, , third] = colors; // Nota el espacio vacío para saltar "verde"
console.log(first); // "rojo"
console.log(third); // "azul"
El Patrón Rest: Recolectando lo que Sobra
La sintaxis ... que verás dentro de un destructuring se llama patrón rest. Recolecta las propiedades (o elementos de un array) que no se extrajeron en su propia variable, agrupándolas en un nuevo objeto (o array):
const customer = { id: 1, name: "Acme Corp", email: "[email protected]", isActive: true };
// Extraer "id" por separado, y agrupar todo lo demás en "rest"
const { id, ...rest } = customer;
console.log(id); // 1
console.log(rest); // { name: "Acme Corp", email: "[email protected]", isActive: true }
Esto se ve idéntico al operador spread del Capítulo 3 ({ ...customer, summary }), pero la dirección es inversa: spread expande las propiedades de un objeto hacia uno nuevo, mientras que rest recolecta las propiedades restantes en uno nuevo. Cuál de los dos comportamientos obtienes depende únicamente de dónde aparece el ...—del lado izquierdo del = (un patrón que se está destructurando) es rest; del lado derecho (construyendo un valor nuevo) es spread.
Destructuring en Parámetros de Función
Aquí es donde destructuring se vuelve increíblemente poderoso para el desarrollo OWL:
// En lugar de acceder a propiedades dentro de la función
const createUserCard = (user) => {
const name = user.name;
const email = user.email;
const isActive = user.isActive;
return `<div class="user-card ${isActive ? 'active' : 'inactive'}">
<h3>${name}</h3>
<p>${email}</p>
</div>`;
};
// Destructurar directamente en los parámetros de la función
const createUserCard = ({ name, email, isActive = true }) => {
return `<div class="user-card ${isActive ? 'active' : 'inactive'}">
<h3>${name}</h3>
<p>${email}</p>
</div>`;
};
// Uso
const userData = { name: "Alice Johnson", email: "[email protected]", isActive: true };
console.log(createUserCard(userData));
Por Qué Destructuring es Perfecto para OWL
Destructuring es especialmente común en OWL para varios escenarios:
1. Extrayendo props en componentes:
// En un componente OWL
setup() {
const { recordId, model, readonly = false } = this.props;
// Ahora puedes usar recordId, model, y readonly directamente
}
2. Trabajando con respuestas de servicios:
// orm.read devuelve un array — toma el primer registro con destructuring de array
const [partner] = await this.orm.read("res.partner", [partnerId], ["name", "email"]);
const { name, email } = partner;
3. Manejo de eventos:
// Extrayendo datos de eventos
onButtonClick({ target, currentTarget }) {
// Trabajar directamente con target y currentTarget
}
4. Gestión de estado:
// Extrayendo valores de estado
const { isLoading, errorMessage, data = [] } = this.state;
Módulos: import y export
A medida que tu aplicación crece, no puedes mantener todo tu código en un archivo gigante. Los módulos te permiten dividir tu código en archivos separados y reutilizables. Puedes export (exportar) variables, funciones o clases de un archivo e import (importarlas) en otro donde se necesiten.
Así es como funciona el sistema de assets de Odoo. Cada archivo JavaScript es un módulo.
Export e Import Básicos
Imaginemos que tenemos un archivo llamado utils.js con algunas funciones auxiliares:
// Archivo: utils.js
export const PI = 3.14159;
export const add = (a, b) => {
return a + b;
};
export const formatCurrency = (amount, currency = "EUR") => {
return `${currency} ${amount.toFixed(2)}`;
};
// Export por defecto (uno por archivo)
const calculateTax = (amount, rate = 0.21) => {
return amount * rate;
};
export default calculateTax;
Ahora, en otro archivo, main.js, podemos importar y usar ese código:
// Archivo: main.js
// Imports nombrados - especifica lo que quieres dentro de llaves
import { PI, add, formatCurrency } from "./utils.js";
// Import por defecto - no se necesitan llaves
import calculateTax from "./utils.js";
// También puedes importar todo
import * as Utils from "./utils.js";
console.log("El valor de PI es:", PI); // Salida: El valor de PI es: 3.14159
console.log("2 + 3 es:", add(2, 3)); // Salida: 2 + 3 es: 5
console.log(formatCurrency(99.99)); // Salida: EUR 99.99
console.log("IVA de €100:", calculateTax(100)); // Salida: IVA de €100: 21
// Usando el import de namespace
console.log("PI del namespace:", Utils.PI);
console.log("Suma:", Utils.add(5, 7));
Los Imports Nombrados Se Parecen al Destructuring
Los imports nombrados usan llaves, por lo que se ven igual que el destructuring (aunque técnicamente son una sintaxis propia):
// Elige exactamente las piezas de la librería que necesitas
import { useState, useEffect } from "@odoo/owl";
Ejemplo Real de Import en OWL
Así se ve un archivo típico de componente OWL:
// Archivo: my_component.js
// Importar la clase Component base y hooks
import { Component, useState, useService } from "@odoo/owl";
// Importar funciones utilitarias de otros archivos
import { formatDate, validateEmail } from "./utils.js";
// Definir y exportar nuestro componente
export class MyComponent extends Component {
static template = "my.Component";
setup() {
// useState devuelve un objeto reactivo (no un par [valor, setter] como en React)
this.state = useState({
email: "",
isValid: false
});
this.orm = useService("orm");
// Usar funciones utilitarias importadas
this.validateAndSetEmail = (email) => {
this.state.email = email;
this.state.isValid = validateEmail(email);
};
}
}
// Exportar para usar en otros módulos
export default MyComponent;
Cuando ves import { Component } from "@odoo/owl"; al inicio de un archivo OWL, simplemente estás importando la clase base Component del módulo de la librería OWL para que puedas extenderla.
JavaScript Asíncrono: Manejando Retrasos
El tema más difícil pero más importante es el código asíncrono. No todas las tareas son instantáneas. Cuando le pides datos al servidor de Odoo (una llamada RPC), hay un retraso mientras la solicitud viaja por la red y el servidor la procesa.
Si JavaScript esperara por la respuesta, toda tu aplicación—toda la interfaz de usuario—se congelaría. Esto se llama código "bloqueante", y es una experiencia de usuario terrible.
El código asíncrono permite a JavaScript comenzar una tarea de larga duración (como una solicitud de red), y luego continuar con otro trabajo. Cuando la tarea finalmente termina, notifica a tu código para que puedas manejar el resultado.
La Forma Antigua: Promises
La solución original para esto fue un objeto llamado Promise (Promesa). Una Promise es un objeto que representa un valor futuro. Adjuntas .then() para manejar un resultado exitoso y .catch() para manejar un error.
// Este es un ejemplo simplificado de obtener datos.
fetch('https://api.example.com/data')
.then(response => {
// Este código se ejecuta cuando el servidor responde exitosamente.
return response.json(); // Obtener los datos de la respuesta.
})
.then(data => {
// Este código se ejecuta después de que los datos han sido procesados.
console.log("Datos recibidos:", data);
})
.catch(error => {
// Este código se ejecuta si algo salió mal.
console.error("Falló al obtener datos:", error);
});
console.log("¡Solicitud enviada! Esperando respuesta...");
La Forma Moderna: async/await
Aunque las Promises funcionan, encadenar .then() puede volverse complicado. JavaScript moderno introdujo una sintaxis mucho más limpia llamada async/await. Te permite escribir código asíncrono que se ve como código normal y síncrono.
async: Pones esta palabra clave antes de una declaración de función para decirle a JavaScript que contiene operaciones asíncronas.await: Usas esta palabra clave dentro de una funciónasyncpara decirle a JavaScript que pause la ejecución en esa línea hasta que la Promise se resuelva, sin congelar la UI.
Aquí está el mismo ejemplo usando async/await. Esta es la sintaxis que usarás para todas las llamadas RPC en OWL:
// Definimos una función async para contener nuestra lógica.
const fetchData = async () => {
try {
// La palabra clave 'await' pausa la función aquí hasta que el fetch se complete.
const response = await fetch('https://api.example.com/data');
// Una vez que la respuesta regresa, la función continúa.
const data = await response.json();
console.log("Datos recibidos:", data);
// Puedes devolver los datos para uso en otro lugar
return data;
} catch (error) {
// Si cualquier llamada 'await' falla, el bloque catch se ejecutará.
console.error("Falló al obtener datos:", error);
throw error; // Re-lanzar si es necesario
}
};
fetchData();
console.log("¡Solicitud enviada! Esperando respuesta...");
Patrones Async del Mundo Real para OWL
Aquí hay patrones asíncronos comunes que usarás en el desarrollo OWL:
1. Cargando datos al inicio del componente:
class MyComponent extends Component {
setup() {
this.state = useState({
isLoading: true,
records: [],
error: null
});
this.loadData();
}
async loadData() {
try {
// searchRead devuelve un array de objetos de registro
const records = await this.orm.searchRead("res.partner", [], ["name", "email"]);
this.state.isLoading = false;
this.state.records = records;
} catch (error) {
this.state.isLoading = false;
this.state.error = error.message;
}
}
}
2. Manejando acciones de usuario de forma asíncrona:
async onSaveClick() {
const { name, email } = this.state.formData;
try {
// Mostrar estado de carga
this.state.isSaving = true;
// Guardar en el servidor (create devuelve un array de ids nuevos)
const [recordId] = await this.orm.create("res.partner", [{ name, email }]);
// Actualizar UI con éxito
this.state.isSaving = false;
this.state.lastSavedId = recordId;
// Mostrar notificación
this.notification.add("¡Registro guardado exitosamente!", { type: "success" });
} catch (error) {
this.state.isSaving = false;
this.notification.add(`Error: ${error.message}`, { type: "danger" });
}
}
3. Usando destructuring con funciones async:
async fetchCustomerData(customerId) {
try {
// Destructurar la respuesta directamente
const [customer] = await this.orm.read("res.partner", [customerId], ["name", "email", "phone"]);
const { name, email, phone = "No hay teléfono" } = customer;
return {
displayName: `${name} (${email})`,
contactInfo: phone,
isComplete: email && phone !== "No hay teléfono"
};
} catch (error) {
console.error(`Falló al obtener cliente ${customerId}:`, error);
return null;
}
}
Esta sintaxis async/await con un bloque try...catch es la forma estándar y moderna de manejar cualquier operación que involucre un retraso, y es fundamental para comunicarse con el servidor de Odoo desde tus componentes OWL.
Juntándolo Todo: Un Ejemplo Completo
Combinemos todos los conceptos de este capítulo en un ejemplo realista estilo OWL. Lo iremos construyendo método por método en vez de todo de una vez.
Primero, los imports y el método setup(), que prepara los servicios y el estado inicial del componente, y luego dispara la carga de datos desde el servidor:
// Archivo: customer-manager.js
// Imports de módulos con destructuring
import { Component, useState, useService } from "@odoo/owl";
import { formatDate, validateEmail } from "./utils.js";
export class CustomerManager extends Component {
static template = "CustomerManager";
setup() {
// Destructurar servicios
const orm = useService("orm");
const notification = useService("notification");
// Configurar estado con destructuring assignment
this.state = useState({
customers: [],
selectedCustomer: null,
isLoading: false,
filters: { activeOnly: true, hasEmail: false }
});
// Almacenar servicios para uso en métodos
this.orm = orm;
this.notification = notification;
// Cargar datos iniciales
this.loadCustomers();
}
Luego, loadCustomers() muestra el patrón completo async/await + try/catch de este capítulo, combinado con destructuring para leer los filtros activos y el patrón rest/spread para enriquecer cada registro con campos adicionales para mostrar:
async loadCustomers() {
try {
// Destructurar filtros del estado
const { activeOnly, hasEmail } = this.state.filters;
this.state.isLoading = true;
// Construir dominio basado en filtros
const domain = [];
if (activeOnly) domain.push(["active", "=", true]);
if (hasEmail) domain.push(["email", "!=", false]);
// searchRead devuelve un array de objetos de registro
const records = await this.orm.searchRead(
"res.partner",
domain,
["name", "email", "phone", "active", "create_date"]
);
const totalCount = records.length;
// Transformar los datos usando métodos de array y destructuring
const processedCustomers = records.map(customer => {
const { name, email, phone, create_date } = customer;
return {
...customer, // Spread de propiedades originales
displayName: `${name}${email ? ` (${email})` : ''}`,
formattedDate: formatDate(create_date),
isValidEmail: email ? validateEmail(email) : false,
hasContact: !!(email || phone)
};
});
// Actualizar estado
this.state.customers = processedCustomers;
this.state.isLoading = false;
this.notification.add(
`Cargados ${totalCount} clientes exitosamente`,
{ type: "success" }
);
} catch (error) {
this.state.isLoading = false;
console.error("Falló al cargar clientes:", error);
this.notification.add(
`Error cargando clientes: ${error.message}`,
{ type: "danger" }
);
}
}
onCustomerSelect() es más pequeño: solo demuestra el uso de .find() con un parámetro destructurado (({ id }) => ...) para localizar un registro por su id:
async onCustomerSelect(customerId) {
// Encontrar cliente usando método de array con destructuring
const selectedCustomer = this.state.customers.find(({ id }) => id === customerId);
if (selectedCustomer) {
this.state.selectedCustomer = selectedCustomer;
// Destructurar para logging
const { name, email } = selectedCustomer;
console.log(`Cliente seleccionado: ${name} (${email || 'sin email'})`);
}
}
Por último, updateCustomerEmail() pone en práctica el patrón rest de verdad: extrae id por separado y guarda el resto de las propiedades en otherProps, así actualizar el email no requiere volver a listar cada campo:
// Método de clase usando async/await y destructuring
async updateCustomerEmail(customerId, newEmail) {
try {
// Validar usando utilidad importada
if (!validateEmail(newEmail)) {
throw new Error("Formato de email inválido");
}
// Actualizar en el servidor
await this.orm.write("res.partner", [customerId], { email: newEmail });
// Actualizar estado local usando array map con destructuring
this.state.customers = this.state.customers.map(customer => {
const { id, ...otherProps } = customer;
if (id === customerId) {
return {
id,
...otherProps,
email: newEmail,
isValidEmail: true,
hasContact: true
};
}
return customer;
});
this.notification.add("¡Email actualizado exitosamente!", { type: "success" });
} catch (error) {
console.error("Falló al actualizar email:", error);
this.notification.add(`Error: ${error.message}`, { type: "danger" });
}
}
}
// Export por defecto
export default CustomerManager;
Este ejemplo demuestra: - Clases y herencia (extendiendo Component) - Destructuring en múltiples contextos (imports, estado, parámetros de función, métodos de array) - Módulos (import/export) - Async/await para comunicación con el servidor - Template literals para formateo de strings - Métodos modernos de array (map, find) combinados con destructuring
Estos son los bloques de construcción fundamentales que hacen que los componentes OWL sean poderosos y elegantes. En el siguiente capítulo, ¡empezaremos a construir tu primer componente OWL real!
Errores Comunes
Confundir spread y rest. Ambos usan ..., pero spread (a la derecha del =, construyendo un valor nuevo: { ...customer, email }) expande propiedades hacia afuera, mientras que rest (a la izquierda del =, en un patrón de destructuring: const { id, ...rest } = customer) recolecta las propiedades sobrantes hacia adentro. Si tu código no hace lo que esperas, revisa de qué lado del = está tu ....
Usar this antes de super() en una subclase. En una clase que hace extends de otra, super(...) debe ser la primera línea del constructor si usas this en cualquier parte de él. JavaScript lanzará un ReferenceError si intentas acceder a this antes.
Olvidar await en una llamada async. const records = this.orm.searchRead(...) (sin await) te da un objeto Promise pendiente, no los datos reales. Si intentas usar .map() o .length sobre algo y no funciona, revisa si olvidaste un await.
No envolver el await en try/catch. Si una promesa es rechazada (por ejemplo, el servidor devuelve un error) y no hay un catch, obtienes un "unhandled promise rejection" y tu componente falla silenciosamente en vez de mostrar retroalimentación útil al usuario.
Ejercicios
- Extiende la clase
Puppyde este capítulo con una nueva subclaseServiceDogque agregue una propiedadtask(ej."guía","alerta") y un métodoperformTask()que registre"${name} está realizando: ${task}". Asegúrate de llamar asuper()correctamente. - Dado
const order = { id: 5, product: "Widget", qty: 3, customer: { name: "Alice", email: "[email protected]" } };, destructuraid,qty, y elnameanidado (renombrado acustomerName) en una sola instrucción. - Escribe una función async
checkStock(productId)que simule una llamada de red conawait new Promise(resolve => setTimeout(resolve, 500)), y luego devuelva{ productId, inStock: true }. Llámala desde otra función async usandotry/catchy registra el resultado.
TL;DR: Los componentes OWL están construidos por completo sobre la sintaxis de class, el destructuring, los módulos ES (import/export) y async/await — domina estos cuatro y no quedará sintaxis de OWL que te sorprenda.
Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch4_ex1. Las instrucciones de instalación están en el README del repositorio.
¿Qué Sigue?
Con JavaScript moderno en tus manos, el Capítulo 5 es donde todo esto rinde frutos: escribirás tu primer componente OWL real, registrado como una client action, y verás correr en el navegador la primera línea de código del framework en este libro.