Skip to content

Unidad 5.22 - Livewire: Comunicación entre Componentes con Eventos

5.22.1 Introducción

En Livewire, la comunicación entre componentes es una de las funcionalidades más poderosas. A través del sistema de eventos, los componentes pueden emitir mensajes que pueden ser escuchados por otros componentes, lo que permite una interacción fluida entre diferentes partes de la interfaz de usuario sin recargar la página.

Esta capacidad de comunicar componentes entre sí sin depender de JavaScript es crucial para la creación de aplicaciones dinámicas y reactivas. En esta unidad, vamos a construir una aplicación paso a paso que involucra tres componentes Livewire que se comunicarán entre sí mediante eventos, todo esto usando el patrón MVVM (Model-View-ViewModel).

Vamos a crear un carrito de compras como ejemplo práctico. El carrito permitirá a los usuarios añadir productos y ver el total actualizado, además de tener la opción de filtrar productos por nombre.

🧭 Flujo básico de eventos en Livewire

Los eventos en Livewire permiten que un componente "emita" un evento que puede ser "escuchado" por otros componentes. A continuación, mostramos un flujo básico de cómo funcionan los eventos entre los componentes:

Flujo de eventos en Livewire

sequenceDiagram
    participant Usuario
    participant ProductosComponent
    participant Livewire
    participant CartItemsComponent

    Usuario->>ProductosComponent: Click en "Agregar al carrito"
    ProductosComponent->>Livewire: dispatch('cartUpdated')
    Livewire->>CartItemsComponent: escucha 'cartUpdated'
    CartItemsComponent->>CartItemsComponent: actualizar vista y total

En este diagrama:

  • El Usuario realiza una acción, como hacer clic en el botón de "Agregar al carrito".
  • El ProductosComponent emite el evento cartUpdated usando el método dispatch().
  • Livewire se encarga de gestionar el evento y lo transmite al componente receptor, en este caso, el CartItemsComponent.
  • El CartItemsComponent actualiza su vista para reflejar el cambio en el carrito.

Este es el flujo que vamos a usar para integrar los diferentes componentes y hacerlos reaccionar a los cambios de estado.

Flujo de eventos en Livewire

sequenceDiagram
    participant Usuario
    participant ProductosComponent
    participant Livewire
    participant CartItemsComponent
    participant NavComponent

    Usuario->>ProductosComponent: Click en "Agregar al carrito"
    ProductosComponent->>Livewire: dispatch('cartUpdated', productoId)
    Livewire->>CartItemsComponent: escucha 'cartUpdated'
    CartItemsComponent->>CartItemsComponent: Actualizar elementos del carrito
    CartItemsComponent->>Livewire: dispatch('updateTotal', total)
    Livewire->>ProductosComponent: Escucha 'updateTotal'
    ProductosComponent->>ProductosComponent: Actualiza la vista con el total
    Usuario->>NavComponent: Escribe búsqueda
    NavComponent->>Livewire: dispatch('filtro', busqueda)
    Livewire->>ProductosComponent: Escucha 'filtro'
    ProductosComponent->>ProductosComponent: Filtra productos
    ProductosComponent->>Livewire: dispatch('cartUpdated')  %% Si el carrito cambia, actualizar vista

5.22.2 Estructura del proyecto

En esta unidad, vamos a crear los siguientes componentes y estructuras:

  1. Migraciones y modelos: Definiremos las tablas necesarias para los productos, carritos y la relación entre ellos.
  2. Componentes Livewire:

  3. ProductosComponent: Lista los productos y permite agregarlos al carrito.

  4. CartItemsComponent: Muestra los productos añadidos al carrito y el total.
  5. NavComponent: Permite buscar productos y filtrar la lista.
  6. Vistas integradas con Bootstrap: Usaremos Bootstrap para mejorar el diseño y hacer que la aplicación sea más atractiva.
  7. Sistema de eventos entre componentes: Utilizaremos eventos para que los componentes se comuniquen entre sí de forma eficiente.

5.22.3 Migraciones y Modelos

Para gestionar los datos de productos y carritos, necesitamos crear las migraciones y modelos adecuados. Las migraciones nos permitirán definir la estructura de nuestras tablas en la base de datos.

Paso 1: Crear las Migraciones

Primero, ejecutamos los siguientes comandos para generar los modelos y las migraciones:

php artisan make:model Product -m
php artisan make:model Cart -m
php artisan make:model CartItem -m

Paso 2: Migración para la tabla products

La tabla products almacenará la información de los productos que los usuarios pueden agregar al carrito. Necesitamos los campos title, description, price e image.

Contenido de la migración para products

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->decimal('price', 10, 2);
        $table->string('image');
        $table->timestamps();
    });
}

public function down()
{
    Schema::dropIfExists('products');
}

Paso 3: Migración para la tabla carts

La tabla carts almacenará la información del carrito. En este caso, solo necesitamos un campo id y las fechas created_at y updated_at.

contenido de la migración para carts

public function up()
{
    Schema::create('carts', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });
}

public function down()
{
    Schema::dropIfExists('carts');
}

Paso 4: Migración para la tabla cart_items

La tabla cart_items será la tabla intermedia que almacenará la relación entre los productos y el carrito. Incluye los campos product_id, cart_id, quantity y price.

contenido de la migración para cart_items

public function up()
{
    Schema::create('cart_items', function (Blueprint $table) {
        $table->id();
        $table->foreignId('product_id')->constrained()->onDelete('cascade');
        $table->foreignId('cart_id')->constrained()->onDelete('cascade');
        $table->unsignedInteger('quantity')->default(0);
        $table->decimal('price', 10, 2)->default(0.00);
        $table->unique(['product_id', 'cart_id']); // Aseguramos que un producto no se pueda agregar varias veces al mismo carrito
        $table->timestamps();
    });
}

public function down()
{
    Schema::dropIfExists('cart_items');
}

Paso 5: Ejecutar las Migraciones

Una vez que las migraciones estén configuradas, ejecutamos:

php artisan migrate

Esto creará las tablas necesarias en la base de datos.


Paso5: Modelos

1. Modelo Product

Este modelo representa los productos que los usuarios pueden agregar al carrito. En el modelo se define la relación belongsToMany con el carrito, ya que un producto puede estar en muchos carritos.

Contenido del modelo Product

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
    use HasFactory;

    // Los atributos que se pueden asignar masivamente
    protected $guarded = ['id'];

    /**
     * Relación muchos a muchos con el carrito.
     * Un producto puede estar en varios carritos.
     */
    public function cartItems()
    {
        return $this->belongsToMany(Cart::class, 'cart_items')
                    ->withPivot('quantity', 'price')
                    ->withTimestamps();
    }
}

2. Modelo Cart

El modelo Cart representa un carrito de compras. Un carrito puede contener múltiples productos, y es utilizado para gestionar los elementos en el carrito.

Contenido del modelo Cart

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Cart extends Model
{
    use HasFactory;

    // Los atributos que se pueden asignar masivamente
    protected $guarded = ['id'];

    /**
     * Relación muchos a muchos con los productos.
     * Un carrito puede contener muchos productos.
     */
    public function items()
    {
        return $this->belongsToMany(Product::class, 'cart_items')
                    ->withPivot('quantity', 'price')
                    ->withTimestamps();
    }
}

3. Modelo CartItem

El modelo CartItem representa un elemento dentro de un carrito. En este modelo, almacenamos la cantidad de un producto específico dentro del carrito, junto con su precio.

Contenido del modelo CartItem

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CartItem extends Model
{
    use HasFactory;

    // Los atributos que se pueden asignar masivamente
    protected $guarded = ['id'];

    /**
     * Relación inversa con el producto.
     * Un CartItem pertenece a un Producto.
     */
    public function product()
    {
        return $this->belongsTo(Product::class);
    }

    /**
     * Relación inversa con el carrito.
     * Un CartItem pertenece a un Carrito.
     */
    public function cart()
    {
        return $this->belongsTo(Cart::class);
    }
}

4. Modelo CartItem (con campos adicionales)

Este modelo será utilizado para gestionar la relación entre los productos y el carrito, además de almacenar los campos quantity (cantidad) y price (precio). Específicamente, será necesario para la tabla intermedia cart_items.


Recuerda que estos modelos gestionan la relación entre productos, carritos y los elementos dentro del carrito, utilizando la relación belongsToMany y withPivot para acceder a los campos adicionales de la tabla intermedia (cart_items).


5.22.4 Seeders y Factorys

Para poblar la base de datos con productos de ejemplo, vamos a crear un seeder y una factory para los productos.

Paso 1: Crear el Seeder para Productos

Ejecutamos el siguiente comando para crear el seeder:

php artisan make:seeder ProductsSeeder

En database/seeders/ProductsSeeder.php, añadimos el siguiente código para generar productos de ejemplo:

Contenido del Seeder para Productos

1
2
3
4
5
6
use App\Models\Product;

public function run()
{
    Product::factory()->count(20)->create();
}

Paso 2: Crear la Factory para Product

Ejecutamos el siguiente comando para crear la factory:

php artisan make:factory ProductFactory --model=Product

En database/factories/ProductFactory.php, definimos los campos que se generarán para los productos:

Contenido de la Factory para Product

public function definition()
{
    $images = ['bottle-1.png', 'bottle-2.png', 'bottle-3.png'];
    return [
        'title' => $this->faker->word(),
        'description' => $this->faker->sentence(),
        'price' => $this->faker->randomFloat(2, 10, 20),
        'image' => $images[rand(1,3) - 1],
    ];
}

En el ejemplo anterior, la image se selecciona aleatoriamente de un conjunto de imágenes predefinidas. Asegúrate de que las imágenes estén disponibles en la carpeta storage/app/public/img de tu proyecto. Las imagenes son las siguientes:

Imagen 1 Imagen 2 Imagen 3
Imagen 1 Imagen 2 Imagen 3

Puedes descargarlas y guardarlas en la carpeta storage/app/public/img de tu proyecto.

Seeder para Cart

Ejecuta el siguiente comando para crear el seeder:

php artisan make:seeder CartSeeder

Contenido del Seeder para Cart

En database/seeders/CartSeeder.php, añade el siguiente código:

Contenido del Seeder para Cart

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Cart;

class CartSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Crear un solo carrito, ya que en este ejemplo solo hay uno.
        Cart::create([
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }
}

Este seeder simplemente crea una entrada en la tabla carts con las fechas created_at y updated_at actuales. Como mencionamos, en este ejemplo simplificado solo existe un carrito

Paso 3: Ejecutar los Seeders

Registramos los seeders en DatabaseSeeder.php:

Contenido de DatabaseSeeder.php

1
2
3
4
5
6
7
public function run()
{
    $this->call([
        ProductsSeeder::class, 
        CartSeeder::class,
        ]);
}

Luego ejecutamos el comando para poblar la base de datos:

php artisan db:seed

5.22.5 Creación de Componentes Livewire

Ahora que la base de datos está configurada y poblada, vamos a crear los componentes Livewire necesarios para nuestra aplicación.

Paso 1: Crear los Componentes

Ejecutamos los siguientes comandos para crear los tres componentes principales:

php artisan make:livewire ProductsComponent
php artisan make:livewire CartItemsComponent
php artisan make:livewire NavComponent

1️⃣ ProductsComponent

Este componente es el responsable de mostrar la lista de productos y agregar productos al carrito.

Lógica del Componente

En app/Http/Livewire/ProductsComponent.php, definimos la lógica:

Contenido del Componente ProductsComponent

use App\Models\Product;
use App\Models\Cart;
use App\Models\CartItem;

class ProductsComponent extends Component
{
    public $productos;

    protected $listeners = ['filtro' => 'filtrarProductos'];

    public function mount()
    {
        $this->productos = Product::all();
    }

    public function agregarAlCarrito($id)
    {
        $cart = Cart::firstOrCreate([]);
        $item = CartItem::firstOrNew([
            'product_id' => $id,
            'cart_id' => $cart->id
        ]);
        $item->quantity++;
        $item->price = Product::find($id)->price;
        $item->save();

        $this->dispatch('cartUpdated');
    }

    public function filtrarProductos($busqueda)
    {
        $this->productos = $busqueda ?
            Product::where('title', 'like', "%{$busqueda}%")->
                     orWhere('title', 'like', "%{$busqueda}%")->get() :
            Product::all();
    }

    public function render()
    {
        return view('livewire.productos-component');
    }
}

Vista del Componente

En resources/views/livewire/products-component.blade.php, creamos la vista que muestra los productos:

Contenido de la Vista products-component

<div class="container mt-5">
    <div class="row">
        @foreach($productos as $producto)
            <div class="col-md-3 mb-4">
                <div class="card">
                    <img src="{{ asset('storage/img/' . $producto->image) }}" 
                        class="card-img-top" 
                        height="128"
                        alt="{{ $producto->titulo }}">
                    <div class="card-body">
                        <h5 class="card-title">{{ $producto->title }}</h5>
                        <p class="card-text">{{ $producto->description }}</p>
                        <p class="card-text">${{ $producto->price }}</p>
                        <button wire:click="agregarAlCarrito({{ $producto->id }})" class="btn btn-primary">Agregar al carrito</button>
                    </div>
                </div>
            </div>
        @endforeach
    </div>
</div>

2️⃣ CartItemsComponent

Este componente muestra los productos añadidos al carrito y el total.

Lógica del Componente

En app/Http/Livewire/CartItemsComponent.php, gestionamos los elementos del carrito:

Contenido del Componente CartItemsComponent

use App\Models\Cart;
use App\Models\CartItem;

class CartItemsComponent extends Component
{
    public $items = [];
    public $totalPrice = 0;
    protected $listeners = ['cartUpdated' => 'actualizarCarrito'];

    public function mount()
    {
        $this->actualizarCarrito();
    }

    public function actualizarCarrito()
    {
        $cart = Cart::first();
        $this->items = $cart->items;
        $this->totalPrice = $this->items->sum(fn($item) => $item->pivot->price * $item->pivot->quantity);
    }

    public function removeFromCart($productId)
    {
        CartItem::where('cart_id', Cart::first()->id)
                ->where('product_id', $productId)
                ->delete();
        $this->actualizarCarrito();
    }

    public function render()
    {
        return view('livewire.cart-items-component');
    }
}

Vista del Componente

En resources/views/livewire/cart-items-component.blade.php, mostramos el carrito:

Contenido de la Vista cart-items-component

<div class="container mt-5">
    <h2>Carrito: {{ $this->totalPrice }}$</h2>
    @foreach($items as $item)
        <div class="row mb-3">
            <div class="col-md-12">
                <div class="card shadow-sm">
                    <div class="row no-gutters">
                        <div class="col-md-12">
                            <img src="{{ asset('storage/img/' . $item->image) }}" 
                                class="card-img-top" 
                                height="128"
                                alt="{{ $item->title }}">
                            <div class="card-body">
                                <h5 class="card-title">{{ $item->title }}</h5>
                                <p class="card-text">Precio: {{ $item->pivot->price }}€</p>
                                <p class="card-text">Cantidad: {{ $item->pivot->quantity }}</p>
                                <button wire:click="removeFromCart({{ $item->id }})" class="btn btn-danger">Eliminar</button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    @endforeach
</div>

3️⃣ NavComponent

Este componente permite filtrar los productos por nombre.

Lógica del Componente

En app/Http/Livewire/NavComponent.php, gestionamos la búsqueda de productos:

Contenido del Componente NavComponent

class NavComponent extends Component
{
    public $busqueda = '';

    public function filtrarProductos()
    {
        $this->dispatch('filtro', $this->busqueda);
    }

    public function cancelarFiltro()
    {
        $this->busqueda = '';
        $this->dispatch('filtro', '');
    }

    public function render()
    {
        return view('livewire.nav-component');
    }
}

Vista del Componente

En resources/views/livewire/nav-component.blade.php:

Contenido de la Vista nav-component

1
2
3
4
5
6
7
8
9
<div class="navbar navbar-expand-lg navbar-light bg-light">
    <div class="container">
        <input type="text" wire:model="busqueda" class="form-control" placeholder="Buscar productos...">
        <button wire:click="filtrarProductos" class="btn btn-primary" title="Filtrar"> Filtrar</button>
        @if($busqueda)
            <button wire:click="cancelarFiltro" class="btn btn-danger" title="Eliminar Filtro">X</button>
        @endif
    </div>
</div>

5.22.6 Eventos entre Componentes

En este apartado vamos a ver detalles de los eventos en livewire que no hemos utilizado en el proyecto de ejemplo que estamos desarrollando.

  • Cómo emitir eventos con parámetros
  • Cómo escuchar eventos con parámetros
  • Escuchar múltiples eventos

Cómo emitir eventos con parámetros Cuando se emite un evento con dispatch(), puedes incluir datos adicionales, como el ID del producto, la cantidad, el precio, etc. Este enfoque es útil para eventos que necesitan pasar información más allá de un simple nombre de evento.

Cómo emitir eventos con parámetros

// En ProductosComponent
public function agregarAlCarrito($id)
{
    // Lógica para agregar al carrito...

    // Emitir el evento y pasar datos
    $this->dispatch('cartUpdated', [
        'product_id' => $id,
        'quantity' => 1,
        'price' => Product::find($id)->price
    ]);
}

Cómo escuchar eventos con parámetros

Luego, en el componente que escucha el evento (CartItemsComponent), puedes acceder a los datos pasados como parámetros y usar esos datos en la lógica:

Cómo escuchar eventos con parámetros

// En CartItemsComponent
protected $listeners = ['cartUpdated' => 'actualizarCarrito'];

public function actualizarCarrito($eventData)
{
    // Usamos los datos enviados con el evento
    $productId = $eventData['product_id'];
    $quantity = $eventData['quantity'];
    $price = $eventData['price'];

    // Lógica para actualizar el carrito con el nuevo producto
    // ...
}

Escuchar múltiples eventos Si un componente necesita escuchar varios eventos, puedes hacerlo de la siguiente manera:

Cómo escuchar múltiples eventos

1
2
3
4
5
protected $listeners = [
    'cartUpdated' => 'actualizarCarrito',
    'filtro' => 'filtrarProductos',
    'updateTotal' => 'actualizarTotal'
];

Aquí, el componente escucha tres eventos diferentes (cartUpdated, filtro, updateTotal) y ejecuta diferentes métodos según el evento recibido.

Registrar eventos dinámicos Podemos crear un método getListeners() en el componente para registrar los eventos de forma dinámica. Esto es útil si queremos que los eventos se registren de manera más organizada o si queremos añadir lógica adicional al registro de eventos.

Cómo registrar eventos dinámicos

1
2
3
4
5
6
7
8
public function getListeners()
{
    return [
        'cartUpdated' => 'actualizarCarrito',
        'filtro' => 'filtrarProductos',
        'updateTotal' => 'actualizarTotal'
    ];
}

Esta lógica permitiría al componenente variar los eventos que escucha en función de la lógica interna del componente, lo que puede ser útil en aplicaciones más complejas.


5.22.6 Integración Final de Componentes

1. Crear el Layout Base con Bootstrap

En Laravel, el layout base generalmente se coloca en resources/views/layouts/app.blade.php. Este archivo es utilizado por las vistas de la aplicación para aplicar un diseño consistente.

Contenido de app.blade.php

Contenido del Layout app.blade.php

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Carrito Demo</title>
    @livewireStyles
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
    {{-- @livewire('nav-component') --}}
    @yield('content')
    @livewireScripts
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Explicación del Layout

  • Meta etiquetas: Se incluye la meta etiqueta para la codificación en UTF-8 y para el diseño responsivo.
  • Bootstrap: Se incluyen los archivos CSS y JS de Bootstrap 5 desde un CDN para el diseño responsivo.
  • @livewireStyles y @livewireScripts: Estas directivas son esenciales para que Livewire funcione correctamente en el frontend. @livewireStyles se incluye en la sección <head>, y @livewireScripts se coloca justo antes de cerrar la etiqueta </body>.
  • Navbar: Se crea una barra de navegación utilizando las clases de Bootstrap. Este ejemplo incluye enlaces básicos a "Inicio", "Productos" y "Carrito", pero puedes modificarlo según las necesidades de tu aplicación.
  • @yield('content'): Esta directiva es donde se insertará el contenido de cada vista específica que extienda este layout. Esto permite tener una estructura común en todas las páginas de la aplicación.

2. Usar el Layout en las Vistas

Con el layout base creado, ahora puedes usarlo en las vistas específicas de tu aplicación. Por ejemplo, en resources/views/welcome.blade.php, puedes hacer lo siguiente:

Contenido de welcome.blade.php

@extends('layouts.app')

@section('content')
    <div class="row">
        <div class="col-md-9">
            <livewire:products-component />
        </div>
        <div class="col-md-3">
            <livewire:cart-items-component />
        </div>
    </div>
@endsection

Explicación:

  • @extends('layouts.app'): Esta directiva hace que la vista welcome.blade.php extienda el layout app.blade.php. Esto significa que la vista tomará la estructura de navegación, estilos y scripts definidos en el layout base.
  • @section('content'): Todo el contenido de la vista se colocará dentro del @yield('content') del layout base. Esto permite que cada vista tenga su propio contenido dentro de la estructura común.

4. Conclusión

Ahora tienes un layout base utilizando Bootstrap que incluye una barra de navegación, un área para el contenido específico de la vista, y el soporte necesario para los componentes de Livewire. Puedes extender este layout para cualquier otra página de tu aplicación, manteniendo una estructura coherente y profesional en todo el sitio.

5.22.7 Pruebas de Integración

Realizamos las siguientes pruebas:

  1. Agregar productos al carrito: Al hacer clic en "Agregar al carrito", el producto se agrega correctamente al carrito.
  2. Filtrar productos: Al escribir un término de búsqueda en NavComponent y hacer clic en "Filtrar", ProductosComponent actualiza la lista de productos mostrados.
  3. Actualizar el total del carrito: Al agregar o eliminar productos del carrito, el total se actualiza correctamente en CartItemsComponent.

5.22.8 Conclusión

Con este enfoque hemos logrado crear una aplicación modular utilizando Livewire, donde los componentes se comunican entre sí mediante eventos. Gracias a Livewire y su sistema de eventos, los datos se sincronizan en tiempo real sin recargar la página ni escribir código JavaScript.

Esta es una forma muy eficiente de manejar la interactividad en aplicaciones web modernas y complejas, lo que nos permite construir aplicaciones dinámicas y escalables.