Ir al contenido
  • Inicio
  • Libro
  • Blog
  • Sobre nosotros
  • Ayuda
  • Contáctanos
  •  [email protected]
  • Inicia sesión
  • English (US) Español (BO)
  • Simplify It S.R.L.
    • Contáctanos
Simplify It S.R.L.
      • Inicio
      • Libro
      • Blog
      • Sobre nosotros
      • Ayuda
      • Contáctanos
    •  [email protected]
    • English (US) Español (BO)
    • Inicia sesión
    • Contáctanos

    Capítulo 9: Manejo de Eventos: Comunicación de Hijo a Padre

  • Todos los blogs
  • Libro OWL
  • Capítulo 9: Manejo de Eventos: Comunicación de Hijo a Padre
  • 25 de agosto de 2026 por
    Capítulo 9: Manejo de Eventos: Comunicación de Hijo a Padre
    Grover Menacho

    ¿Por qué este capítulo? Heredé más de un addon donde un widget hijo se metía directamente al estado de su padre para "arreglar" algo, y cada uno de esos se volvió frágil en cuanto alguien cambiaba el padre. Las callback props son la alternativa disciplinada. ¿Qué trata de resolver Odoo con esto? OWL eliminó a propósito el viejo sistema de trigger/bus de eventos de OWL 1 — Odoo necesitaba comunicación hijo-a-padre rastreable desde el propio template del padre, no dispersa en listeners de eventos que tienes que ir a buscar. Aplicación en la vida real: Un TodoItem diciéndole a su TodoList "bórrame" o una fila de tabla diciéndole a su padre "me hicieron clic" tienen la misma forma que un widget de campo de formulario diciéndole a la vista de formulario "mi valor cambió" — el patrón que usarás constantemente en cuanto empieces a construir UI real de Odoo.

    En el Capítulo 8, establecimos la regla de oro de las props: los datos fluyen en una dirección, del padre hacia el hijo. Un componente hijo nunca debe modificar sus propias props directamente. Esto crea aplicaciones predecibles y mantenibles donde siempre sabes dónde se originan los cambios de datos.

    Pero esto plantea una pregunta importante: ¿qué sucede cuando un componente hijo necesita comunicarse de vuelta con su padre? ¿Qué pasa si un usuario hace clic en un botón "Eliminar" dentro de un componente TodoItem? El hijo no puede eliminar los datos por sí mismo porque esos datos pertenecen al padre y fueron pasados hacia abajo como prop.

    Aquí es donde entra la segunda mitad del ciclo de comunicación: las callback props (props de función).

    Mientras los datos fluyen hacia abajo a través de props, la comunicación fluye hacia arriba a través de llamadas a funciones. El padre le pasa una función al hijo como prop; cuando algo sucede dentro del hijo (un clic, un envío, un error), el hijo simplemente llama a esa función. El padre decide qué hacer con la información. Esto completa el ciclo de flujo de datos mientras mantiene al padre firmemente en control del estado de la aplicación.

    ¿Vienes de OWL 1.x? El framework antiguo tenía un mecanismo trigger() que despachaba eventos personalizados hacia arriba en el árbol de componentes, y los padres los escuchaban con t-on-nombre-evento en la etiqueta del componente. Ese mecanismo fue eliminado en OWL 2.0. En OWL 2.0, t-on- funciona solo en elementos DOM reales (para eventos nativos como click e input), y la comunicación hijo-a-padre se hace exclusivamente con callback props. Si ves this.trigger("algun-evento") en código antiguo, eso es OWL 1 legacy (cubriremos su migración en el Capítulo 17).


    Entendiendo el Flujo de Comunicación

    Visualicemos cómo funciona esta comunicación bidireccional:

    Componente Padre
    |-- Estado: { todos: [...], users: [...] }
    |-- Los datos fluyen HACIA ABAJO vía props ↓
    |-- Los callbacks fluyen HACIA ABAJO vía props ↓
    |
    Componente Hijo
    |-- Recibe: props de datos (solo lectura) + callback props
    |-- Ocurre una interacción del usuario
    |-- El hijo LLAMA al callback ↑
    |
    Componente Padre
    |-- El callback se ejecuta en el padre
    |-- Actualiza su propio estado
    |-- El nuevo estado fluye HACIA ABAJO como props ↓
    

    Este patrón asegura que: - Los padres controlan los datos y la lógica de negocio - Los hijos permanecen puros y reutilizables - El flujo de datos es predecible y fácil de depurar - Los componentes están débilmente acoplados y son mantenibles


    Patrón Básico de Callbacks

    Empecemos con un ejemplo simple. Crearemos un componente TodoItem que puede notificar a su padre cuando necesita ser eliminado.

    El Componente Hijo: TodoItem

    JavaScript de TodoItem (todo_item.js):

    import { Component } from "@odoo/owl";
    
    export class TodoItem extends Component {
      static template = "my_module.TodoItem";
    
      static props = {
        todo: {
          type: Object,
          shape: {
            id: Number,
            text: String,
            completed: Boolean,
            priority: { type: String, optional: true },
            dueDate: { type: String, optional: true }
          }
        },
        // Callback props: funciones que el padre nos da para "llamar a casa"
        onDelete: { type: Function },
        onToggle: { type: Function },
        onEdit: { type: Function, optional: true },
        canEdit: { type: Boolean, optional: true },
        canDelete: { type: Boolean, optional: true },
        showPriority: { type: Boolean, optional: true }
      };
    
      static defaultProps = {
        canEdit: true,
        canDelete: true,
        showPriority: false
      };
    
      // Manejador de evento para el botón eliminar
      onDeleteClick() {
        console.log("TodoItem: Clic en eliminar, llamando al callback onDelete");
    
        // Llamar a la función del padre con un objeto de payload
        this.props.onDelete({
          todoId: this.props.todo.id,
          todoText: this.props.todo.text
        });
      }
    
      // Manejador de evento para alternar completado
      onToggleClick() {
        console.log("TodoItem: Clic en toggle, llamando al callback onToggle");
    
        this.props.onToggle({
          todoId: this.props.todo.id,
          currentStatus: this.props.todo.completed
        });
      }
    
      // Manejador de evento para solicitar edición
      onEditClick() {
        console.log("TodoItem: Clic en editar, llamando al callback onEdit");
    
        // Los callbacks opcionales pueden ser undefined — usa optional chaining
        this.props.onEdit?.({
          todoId: this.props.todo.id,
          currentText: this.props.todo.text
        });
      }
    
      // Propiedades computadas para estilos
      get todoClasses() {
        const classes = ['todo-item'];
        if (this.props.todo.completed) {
          classes.push('todo-completed');
        }
        if (this.props.todo.priority === 'high') {
          classes.push('todo-high-priority');
        }
        return classes.join(' ');
      }
    
      get priorityBadgeClass() {
        const priorityClasses = {
          low: 'badge-secondary',
          medium: 'badge-warning',
          high: 'badge-danger'
        };
        return priorityClasses[this.props.todo.priority] || 'badge-secondary';
      }
    }
    

    Plantilla de TodoItem (todo_item.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
    
        <t t-name="my_module.TodoItem" owl="1">
            <div t-att-class="todoClasses" class="d-flex align-items-center p-3 border rounded mb-2">
    
                <!-- Checkbox para Alternar Completado -->
                <div class="todo-toggle me-3">
                    <input type="checkbox"
                           class="form-check-input"
                           t-att-checked="props.todo.completed"
                           t-on-click="onToggleClick"/>
                </div>
    
                <!-- Contenido del Todo -->
                <div class="todo-content flex-grow-1">
                    <div class="d-flex justify-content-between align-items-start">
                        <div class="todo-text">
                            <span t-att-class="props.todo.completed ? 'text-decoration-line-through text-muted' : ''">
                                <t t-esc="props.todo.text"/>
                            </span>
    
                            <!-- Insignia de Prioridad -->
                            <t t-if="props.showPriority and props.todo.priority">
                                <span t-att-class="'badge ms-2 ' + priorityBadgeClass">
                                    <t t-esc="props.todo.priority"/>
                                </span>
                            </t>
                        </div>
    
                        <!-- Fecha Límite -->
                        <t t-if="props.todo.dueDate">
                            <small class="text-muted">
                                Vence: <t t-esc="props.todo.dueDate"/>
                            </small>
                        </t>
                    </div>
                </div>
    
                <!-- Botones de Acción -->
                <div class="todo-actions ms-3">
                    <t t-if="props.canEdit">
                        <button class="btn btn-sm btn-outline-primary me-1"
                                t-on-click="onEditClick">
                            <i class="fa fa-edit"></i>
                        </button>
                    </t>
    
                    <t t-if="props.canDelete">
                        <button class="btn btn-sm btn-outline-danger"
                                t-on-click="onDeleteClick">
                            <i class="fa fa-trash"></i>
                        </button>
                    </t>
                </div>
            </div>
        </t>
    
    </templates>
    

    Fíjate en que t-on-click se usa en elementos DOM reales (el checkbox y los botones)—eso es exactamente para lo que sirve t-on- en OWL 2.0: escuchar eventos nativos del navegador. El puente hacia el padre es la callback prop.

    El Componente Padre: TodoList

    Ahora creemos el componente padre que proporciona esos callbacks:

    JavaScript de TodoList (todo_list.js):

    import { Component, useState } from "@odoo/owl";
    import { TodoItem } from "../todo_item/todo_item";
    
    export class TodoList extends Component {
      static template = "my_module.TodoList";
      static components = { TodoItem };
    
      setup() {
        this.state = useState({
          todos: [
            {
              id: 1,
              text: "Aprender lo básico de OWL",
              completed: true,
              priority: "medium",
              dueDate: "2024-03-20"
            },
            {
              id: 2,
              text: "Construir una app de todos",
              completed: false,
              priority: "high",
              dueDate: "2024-03-25"
            },
            {
              id: 3,
              text: "Dominar la comunicación entre componentes",
              completed: false,
              priority: "low",
              dueDate: "2024-03-30"
            }
          ],
    
          editingTodoId: null,
          newTodoText: "",
          showCompleted: true,
          showPriority: true
        });
      }
    
      // Callback: el hijo nos pidió eliminar un todo
      onTodoDelete({ todoId, todoText }) {
        console.log(`TodoList: onTodoDelete llamado para ID ${todoId}`);
    
        // Mostrar confirmación (opcional)
        if (confirm(`¿Estás seguro de que quieres eliminar "${todoText}"?`)) {
          // Actualizar el estado del padre - esto causará un re-render
          this.state.todos = this.state.todos.filter(todo => todo.id !== todoId);
    
          console.log(`TodoList: Todo ${todoId} eliminado exitosamente`);
        }
      }
    
      // Callback: el hijo nos pidió alternar el completado
      onTodoToggle({ todoId }) {
        console.log(`TodoList: onTodoToggle llamado para ID ${todoId}`);
    
        // Encontrar y actualizar el todo
        const todo = this.state.todos.find(t => t.id === todoId);
        if (todo) {
          todo.completed = !todo.completed;
          console.log(`TodoList: Estado de completado del todo ${todoId} cambiado a ${todo.completed}`);
        }
      }
    
      // Callback: el hijo nos pidió empezar a editar un todo
      onTodoEdit({ todoId, currentText }) {
        console.log(`TodoList: onTodoEdit llamado para ID ${todoId}`);
    
        // Entrar en modo de edición
        this.state.editingTodoId = todoId;
        this.state.newTodoText = currentText;
      }
    
      // Métodos locales para gestionar todos
      addNewTodo() {
        if (!this.state.newTodoText.trim()) return;
    
        const newTodo = {
          id: Math.max(...this.state.todos.map(t => t.id), 0) + 1,
          text: this.state.newTodoText.trim(),
          completed: false,
          priority: "medium",
          dueDate: null
        };
    
        this.state.todos.push(newTodo);
        this.state.newTodoText = "";
      }
    
      saveEdit() {
        if (!this.state.newTodoText.trim()) return;
    
        const todo = this.state.todos.find(t => t.id === this.state.editingTodoId);
        if (todo) {
          todo.text = this.state.newTodoText.trim();
        }
    
        this.cancelEdit();
      }
    
      cancelEdit() {
        this.state.editingTodoId = null;
        this.state.newTodoText = "";
      }
    
      toggleShowCompleted() {
        this.state.showCompleted = !this.state.showCompleted;
      }
    
      toggleShowPriority() {
        this.state.showPriority = !this.state.showPriority;
      }
    
      // Propiedades computadas
      get visibleTodos() {
        if (this.state.showCompleted) {
          return this.state.todos;
        }
        return this.state.todos.filter(todo => !todo.completed);
      }
    
      get completedCount() {
        return this.state.todos.filter(todo => todo.completed).length;
      }
    
      get totalCount() {
        return this.state.todos.length;
      }
    }
    

    Plantilla de TodoList (todo_list.xml):

    <?xml version="1.0" encoding="UTF-8"?>
    <templates xml:space="preserve">
    
        <t t-name="my_module.TodoList" owl="1">
            <div class="todo-list-container p-4">
    
                <!-- Encabezado -->
                <div class="todo-header mb-4">
                    <div class="d-flex justify-content-between align-items-center">
                        <div>
                            <h2>Mi Lista de Todos</h2>
                            <p class="text-muted mb-0">
                                <t t-esc="completedCount"/> completados de <t t-esc="totalCount"/> todos
                            </p>
                        </div>
    
                        <!-- Controles de Vista -->
                        <div class="view-controls">
                            <button class="btn btn-sm me-2"
                                    t-att-class="state.showCompleted ? 'btn-primary' : 'btn-outline-primary'"
                                    t-on-click="toggleShowCompleted">
                                <i class="fa fa-eye"></i>
                                <t t-esc="state.showCompleted ? 'Ocultar Completados' : 'Mostrar Todos'"/>
                            </button>
    
                            <button class="btn btn-sm btn-outline-secondary"
                                    t-att-class="state.showPriority ? 'active' : ''"
                                    t-on-click="toggleShowPriority">
                                <i class="fa fa-flag"></i>
                                Prioridad
                            </button>
                        </div>
                    </div>
                </div>
    
                <!-- Formulario para Añadir Nuevo Todo -->
                <div class="add-todo-form mb-4 p-3 bg-light rounded">
                    <div class="row">
                        <div class="col">
                            <input type="text"
                                   class="form-control"
                                   placeholder="Añadir un nuevo todo..."
                                   t-model="state.newTodoText"
                                   t-on-keyup.enter="addNewTodo"/>
                        </div>
                        <div class="col-auto">
                            <button class="btn btn-success" t-on-click="addNewTodo">
                                <i class="fa fa-plus"></i>
                                Añadir Todo
                            </button>
                        </div>
                    </div>
                </div>
    
                <!-- Formulario de Edición -->
                <t t-if="state.editingTodoId">
                    <div class="edit-todo-form mb-4 p-3 bg-warning bg-opacity-10 rounded">
                        <h5>Editar Todo</h5>
                        <div class="row">
                            <div class="col">
                                <input type="text"
                                       class="form-control"
                                       t-model="state.newTodoText"
                                       t-on-keyup.enter="saveEdit"/>
                            </div>
                            <div class="col-auto">
                                <button class="btn btn-success me-2" t-on-click="saveEdit">
                                    <i class="fa fa-check"></i>
                                    Guardar
                                </button>
                                <button class="btn btn-secondary" t-on-click="cancelEdit">
                                    <i class="fa fa-times"></i>
                                    Cancelar
                                </button>
                            </div>
                        </div>
                    </div>
                </t>
    
                <!-- Elementos Todo -->
                <div class="todo-items">
                    <t t-if="visibleTodos.length > 0">
                        <t t-foreach="visibleTodos" t-as="todo" t-key="todo.id">
                            <!-- Aquí es donde le entregamos nuestros callbacks al hijo -->
                            <TodoItem
                                todo="todo"
                                canEdit="true"
                                canDelete="true"
                                showPriority="state.showPriority"
                                onDelete.bind="onTodoDelete"
                                onToggle.bind="onTodoToggle"
                                onEdit.bind="onTodoEdit"
                            />
                        </t>
                    </t>
    
                    <!-- Estado Vacío -->
                    <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 todos que mostrar</h4>
                            <p class="text-muted">
                                <t t-if="!state.showCompleted">
                                    ¡Todos los todos están completados! Activa "Mostrar Todos" para verlos.
                                </t>
                                <t t-else="">
                                    Añade tu primer todo arriba para empezar.
                                </t>
                            </p>
                        </div>
                    </t>
                </div>
            </div>
        </t>
    
    </templates>
    

    El Sufijo .bind: Detalle Pequeño, Gran Diferencia

    Mira de cerca cómo se pasan los callbacks:

    <TodoItem onDelete.bind="onTodoDelete"/>
    

    El sufijo .bind le dice a OWL que vincule (bind) la función al componente padre antes de pasarla hacia abajo. Sin él, cuando el hijo llame a this.props.onDelete(...), el this dentro de onTodoDelete sería undefined y this.state fallaría.

    Tienes tres opciones equivalentes—elige una y sé consistente:

    <!-- Opción 1 (recomendada): el sufijo .bind -->
    <TodoItem onDelete.bind="onTodoDelete"/>
    
    <!-- Opción 2: una función flecha inline (vincula `this` automáticamente) -->
    <TodoItem onDelete="(payload) => this.onTodoDelete(payload)"/>
    
    // Opción 3: vincular manualmente en setup()
    setup() {
      this.onTodoDelete = this.onTodoDelete.bind(this);
    }
    

    El sufijo .bind es la forma idiomática de OWL 2.0 y lo que verás en todo el código de Odoo.


    Patrones Avanzados de Callbacks

    1. Eventos Nativos y Detener la Propagación

    t-on- en elementos DOM soporta modificadores útiles:

    <!-- .stop llama a event.stopPropagation() por ti -->
    <button t-on-click.stop="onDeleteClick">Eliminar</button>
    
    <!-- .prevent llama a event.preventDefault() -->
    <form t-on-submit.prevent="onSubmit">...</form>
    
    <!-- .enter se dispara solo con la tecla Enter -->
    <input t-on-keyup.enter="addNewTodo"/>
    

    Usa estos para controlar el evento DOM nativo; luego llama a tu callback prop como de costumbre.

    2. Invocación Condicional de Callbacks

    // Solo llamar al callback si se cumplen ciertas condiciones
    onDeleteClick() {
      if (this.props.todo.completed) {
        this.props.onDelete({ todoId: this.props.todo.id });
      } else {
        this.props.onConfirmDelete?.({
          todoId: this.props.todo.id,
          message: "Este todo no está completado. ¿Estás seguro?"
        });
      }
    }
    

    3. Callbacks Asíncronos

    Los callbacks pueden ser async, lo que permite al hijo esperar el trabajo del padre (por ejemplo, para mostrar un spinner mientras el padre guarda):

    // Hijo
    async onSaveClick() {
      this.state.saving = true;
      try {
        await this.props.onSave({ data: this.state.formData });
      } finally {
        this.state.saving = false;
      }
    }
    

    4. Validación en el Padre

    // El padre valida el payload antes de procesarlo
    onTodoUpdate({ todoId, newText }) {
      // Validar los datos
      if (!todoId || typeof newText !== 'string' || newText.trim().length === 0) {
        console.error("Actualización de todo inválida:", { todoId, newText });
        return;
      }
    
      // Proceder con la actualización
      const todo = this.state.todos.find(t => t.id === todoId);
      if (todo) {
        todo.text = newText.trim();
      }
    }
    

    Convenciones de Nombres para Callbacks

    Seguir convenciones de nombres consistentes hace tu código más mantenible:

    // En la definición de props del hijo: "on" + lo que pasó, en camelCase
    static props = {
      onTodoCreated: { type: Function },
      onUserSelected: { type: Function },
      onFormSubmitted: { type: Function },
      onError: { type: Function, optional: true },
    };
    

    Mejores Prácticas:

    1. Prefijo on: onDelete, onSave, onUserSelected—se lee instantáneamente como "callback"
    2. Usa camelCase: las callback props son props normales, así que siguen la convención de nombres de props (onTodoDeleted, no on-todo-deleted)
    3. Sé descriptivo: onUserProfileUpdated en vez de onUpdate cuando el contexto no sea obvio
    4. Incluye contexto en los payloads: pasa un objeto ({ todoId, todoText }) en vez de argumentos sueltos—es auto-documentado y extensible
    5. Marca los callbacks realmente opcionales con optional: true y llámalos con ?.()

    Patrón de Comunicación Complejo

    Creemos un ejemplo más sofisticado con un padre UserManager y múltiples componentes hijos:

    Padre UserManager (user_manager.js):

    import { Component, useState } from "@odoo/owl";
    import { UserCard } from "../user_card/user_card";
    import { UserForm } from "../user_form/user_form";
    import { UserFilters } from "../user_filters/user_filters";
    
    export class UserManager extends Component {
      static template = "my_module.UserManager";
      static components = { UserCard, UserForm, UserFilters };
    
      setup() {
        this.state = useState({
          users: [
            { id: 1, name: "Alice Johnson", role: "admin", active: true, department: "IT" },
            { id: 2, name: "Bob Smith", role: "user", active: true, department: "Ventas" },
            { id: 3, name: "Carol Brown", role: "user", active: false, department: "RRHH" }
          ],
          filters: {
            role: "all",
            department: "all",
            active: true
          },
          editingUser: null,
          showForm: false,
          searchText: ""
        });
      }
    
      // Callbacks para UserCard
      onUserEdit({ user }) {
        console.log("UserManager: Edición de usuario solicitada", user);
        this.state.editingUser = { ...user }; // Clonar para editar
        this.state.showForm = true;
      }
    
      onUserDelete({ userId, userName }) {
        console.log("UserManager: Eliminación de usuario solicitada", userId);
    
        if (confirm(`¿Eliminar usuario ${userName}?`)) {
          this.state.users = this.state.users.filter(u => u.id !== userId);
        }
      }
    
      onUserToggleStatus({ userId }) {
        console.log("UserManager: Alternar estado del usuario", userId);
    
        const user = this.state.users.find(u => u.id === userId);
        if (user) {
          user.active = !user.active;
        }
      }
    
      // Callbacks para UserForm
      onUserSave({ user }) {
        console.log("UserManager: Guardar usuario", user);
    
        if (user.id) {
          // Actualizar usuario existente
          const index = this.state.users.findIndex(u => u.id === user.id);
          if (index !== -1) {
            this.state.users[index] = user;
          }
        } else {
          // Crear nuevo usuario
          user.id = Math.max(...this.state.users.map(u => u.id), 0) + 1;
          this.state.users.push(user);
        }
    
        this.onFormCancel();
      }
    
      onFormCancel() {
        console.log("UserManager: Formulario cancelado");
        this.state.showForm = false;
        this.state.editingUser = null;
      }
    

    Tres hijos distintos (UserCard, UserForm, UserFilters) tienen arriba sus propios métodos de callback dedicados — UserManager nunca necesita adivinar qué hijo lo llamó, ya que cada callback se pasa explícitamente a un único componente en la plantilla.

      // Callbacks para UserFilters
      onFiltersChanged({ filters }) {
        console.log("UserManager: Filtros cambiados", filters);
        this.state.filters = { ...this.state.filters, ...filters };
      }
    
      onSearchChanged({ searchText }) {
        console.log("UserManager: Búsqueda cambiada", searchText);
        this.state.searchText = searchText;
      }
    
      // Acciones locales
      openNewUserForm() {
        this.state.editingUser = {
          id: null,
          name: "",
          role: "user",
          active: true,
          department: ""
        };
        this.state.showForm = true;
      }
    

    Finalmente, filteredUsers es un simple getter computado — sin props ni callbacks involucrados — que combina los filtros activos y el texto de búsqueda en la lista que realmente se muestra en la plantilla:

      // Propiedades computadas
      get filteredUsers() {
        let filtered = this.state.users;
    
        // Aplicar filtro de rol
        if (this.state.filters.role !== "all") {
          filtered = filtered.filter(u => u.role === this.state.filters.role);
        }
    
        // Aplicar filtro de departamento
        if (this.state.filters.department !== "all") {
          filtered = filtered.filter(u => u.department === this.state.filters.department);
        }
    
        // Aplicar filtro de activos
        if (this.state.filters.active !== null) {
          filtered = filtered.filter(u => u.active === this.state.filters.active);
        }
    
        // Aplicar búsqueda
        if (this.state.searchText) {
          const search = this.state.searchText.toLowerCase();
          filtered = filtered.filter(u =>
            u.name.toLowerCase().includes(search) ||
            u.department.toLowerCase().includes(search)
          );
        }
    
        return filtered;
      }
    }
    

    Y en la plantilla, cada hijo recibe exactamente los callbacks que necesita:

    <UserFilters
        filters="state.filters"
        onFiltersChanged.bind="onFiltersChanged"
        onSearchChanged.bind="onSearchChanged"
    />
    
    <t t-foreach="filteredUsers" t-as="user" t-key="user.id">
        <UserCard
            user="user"
            onEdit.bind="onUserEdit"
            onDelete.bind="onUserDelete"
            onToggleStatus.bind="onUserToggleStatus"
        />
    </t>
    
    <t t-if="state.showForm">
        <UserForm
            user="state.editingUser"
            onSave.bind="onUserSave"
            onCancel.bind="onFormCancel"
        />
    </t>
    

    Este ejemplo demuestra cómo un solo padre puede coordinar múltiples componentes hijos a través de callbacks, manteniendo la gestión de estado centralizada mientras permite que los hijos permanezcan enfocados y reutilizables.


    Props de Datos vs Callback Props: Marco de Decisión

    Ambas viajan hacia abajo como props, pero sirven a direcciones opuestas de comunicación:

    Usa Callback Props Cuando:

    • El hijo necesita notificar al padre de acciones del usuario
    • El hijo necesita solicitar al padre que cambie datos
    • El hijo encuentra un error que el padre debería manejar
    • El hijo completa una operación asíncrona
    static props = {
      onDeleted: { type: Function },       // "Algo pasó"
      onSaveRequested: { type: Function }, // "Por favor haz algo"
      onError: { type: Function },         // "Algo salió mal"
    };
    

    Usa Props de Datos Cuando:

    • El padre necesita configurar el comportamiento del hijo
    • El padre necesita pasar datos al hijo
    • El padre controla el estado de visualización del hijo
    static props = {
      user: { type: Object },          // Prop de datos
      readonly: { type: Boolean },     // Prop de configuración
      showDetails: { type: Boolean }   // Prop de estado
    };
    

    ¿Y los componentes que no son padre e hijo? Los callbacks funcionan a través del árbol de componentes. Para comunicación entre componentes distantes (hermanos, o a través de toda la app), usarás estado compartido y el bus del entorno—ambos cubiertos en el Capítulo 15.


    Depurando Callbacks

    Patrón de Console Logging

    // En el componente hijo
    onAction() {
      console.log("Hijo: Llamando al callback onAction", { payload: "data" });
      this.props.onAction({ payload: "data" });
    }
    
    // En el componente padre
    onChildAction(payload) {
      console.log("Padre: onChildAction recibió", payload);
      // Manejarlo...
    }
    

    Problemas Comunes y Soluciones

    Problema 1: "Cannot read properties of undefined (reading 'state')"

    <!-- Problema: la función sin vincular pierde `this` -->
    <TodoItem onDelete="onTodoDelete"/>
    
    <!-- Solución: usa el sufijo .bind -->
    <TodoItem onDelete.bind="onTodoDelete"/>
    

    Problema 2: "props.onEdit is not a function"

    // El padre no pasó un callback opcional.
    // Decláralo opcional y llámalo con seguridad:
    static props = {
      onEdit: { type: Function, optional: true },
    };
    
    onEditClick() {
      this.props.onEdit?.({ todoId: this.props.todo.id });
    }
    

    Problema 3: El callback se ejecuta inmediatamente al renderizar

    <!-- Problema: esto LLAMA a la función durante el renderizado -->
    <TodoItem onDelete="onTodoDelete(todo.id)"/>
    
    <!-- Solución: pasa una función, no la llames -->
    <TodoItem onDelete="() => this.onTodoDelete(todo.id)"/>
    

    Consideraciones de Rendimiento

    1. Aplica Debounce a Callbacks de Alta Frecuencia

    // Malo: llama al padre en cada pulsación de tecla
    onInputChange(event) {
      this.props.onTextChanged({ value: event.target.value });
    }
    
    // Bueno: callback con debounce. "Debounce" significa retrasar una llamada
    // hasta que una ráfaga de eventos disparadores (ej. pulsaciones de tecla)
    // se detenga durante un período dado, para no hacer una llamada por cada
    // tecla. `debounce` está disponible en @web/core/utils/timing.
    setup() {
      this.debouncedNotify = debounce((value) => {
        this.props.onTextChanged({ value });
      }, 300);
    }
    
    onInputChange(event) {
      this.debouncedNotify(event.target.value);
    }
    

    2. Minimiza el Tamaño del Payload

    // Malo: payload grande con datos innecesarios
    this.props.onUserSelected({
      fullUser: this.props.user,   // El objeto de usuario completo
      allUsers: this.props.users   // Datos innecesarios
    });
    
    // Bueno: payload mínimo
    this.props.onUserSelected({
      userId: this.props.user.id   // Solo el ID
    });
    

    3. Usa Referencias de Función Estables

    El sufijo .bind crea la función vinculada una sola vez, así que los hijos no ven una prop "nueva" en cada render. Prefiérelo sobre definir funciones flecha nuevas en las plantillas cuando el callback no necesita variables del bucle.


    Errores Comunes

    Error 1: Olvidar .bind

    <!-- `this` dentro de onTodoDelete será undefined -->
    <TodoItem onDelete="onTodoDelete"/>
    

    Usa siempre .bind, una función flecha en línea, o un .bind(this) manual en setup() — ver "El Sufijo .bind" más arriba.

    Error 2: Llamar al Callback en Vez de Pasarlo

    <!-- Problema: esto LLAMA a onTodoDelete durante el renderizado, no al hacer clic -->
    <TodoItem onDelete="onTodoDelete(todo.id)"/>
    
    <!-- Solución -->
    <TodoItem onDelete="() => this.onTodoDelete(todo.id)"/>
    

    Error 3: Tratar los Callbacks Opcionales Como Siempre Presentes

    Si una callback prop es optional: true, llamarla directamente (this.props.onEdit(...)) lanza un error cuando el padre no la pasó. Usa encadenamiento opcional: this.props.onEdit?.(...).

    Error 4: Enviar Payloads Voluminosos

    Pasar todo el estado del componente, o datos no relacionados, en el payload de un callback acopla al hijo con detalles internos que no debería conocer. Envía lo mínimo que el padre necesita (un id, un objeto pequeño) en vez de objetos o arrays completos.


    Ejercicios

    Ejercicio 1: Añade un Callback

    Extiende el ejemplo TodoItem/TodoList con una nueva callback prop onPriorityChange que el hijo llame cuando el usuario haga clic en un botón "subir prioridad". El padre debe actualizar la prioridad del todo en su estado.

    Ejercicio 2: Corrige el Bug

    Dado <TodoItem onDelete="onTodoDelete"/> (sin .bind, sin función flecha), predice qué pasa al hacer clic en el botón de eliminar, y luego verifícalo en la consola del navegador. Corrígelo usando el sufijo .bind.

    Ejercicio 3: Búsqueda con Debounce

    Construye un componente SearchBox con un input de texto y una callback prop onSearch. Aplica debounce al callback para que solo se dispare 300ms después de que el usuario deja de escribir, usando debounce de @web/core/utils/timing.


    Las callback props son la clave para construir aplicaciones OWL mantenibles donde los componentes pueden comunicarse efectivamente mientras permanecen débilmente acoplados. Al dominar este patrón—datos hacia abajo, callbacks hacia arriba—creas aplicaciones que son tanto poderosas como predecibles.

    TL;DR: Los hijos le hablan a los padres llamando a una función que el padre pasó hacia abajo como prop (siempre con .bind), nunca mutando el estado del padre ni recurriendo a un bus de eventos legado.

    Pruébalo tú mismo: El ejemplo de este capítulo está disponible como addon instalable de Odoo 19: simplifyit_owl_book_ch9_ex1. Las instrucciones de instalación están en el README del repositorio.

    ¿Qué Sigue?

    Con datos hacia abajo y callbacks hacia arriba, el ciclo de comunicación padre-hijo está completo. En el siguiente capítulo, exploraremos los hooks del ciclo de vida del componente que te permiten aprovechar momentos clave en la vida de un componente para realizar inicialización, limpieza y optimización.

    en Libro OWL
    Capítulo 10: Los Hooks del Ciclo de Vida del Componente
    Reading the whole book? Take it with you — free, no sign-up.
    PDF EPUB All chapters
    Enlaces útiles
    • Inicio
    • Sobre nosotros
    • Blog
    • El libro de OWL 2.0
    • Ayuda
    • Contáctanos
    Sobre nosotros

    Simplify It S.R.L. es una firma de implementación y desarrollo Odoo con base en La Paz, Bolivia, que trabaja con empresas de Latinoamérica y Norteamérica. Desarrollamos sobre Odoo desde la versión 6.1: módulos a medida, migraciones de versión y localización boliviana.

    También somos autores del libro de OWL 2.0, que publicamos gratis capítulo a capítulo en nuestro blog.

    Conecta con nosotros
    • Contáctanos
    • [email protected]
    • +591 65144144
    • La Paz, Bolivia
    Síguenos
    Copyright © Simplify It S.R.L.
    English (US) | Español (BO)
    Con la tecnología de Odoo - Cree un sitio web gratuito