Skip to content

5.18 Patrón MVVM (Model-View-ViewModel) en Laravel

5.18.1 Introducción al patrón MVVM

El patrón MVVM (Model-View-ViewModel) es una evolución del patrón tradicional MVC (Model-View-Controller) que tiene como principal objetivo mejorar la interactividad y la vinculación entre los datos del modelo y la interfaz de usuario. En aplicaciones modernas, especialmente aquellas que son altamente interactivas, como aplicaciones de una sola página (SPA), la eficiencia en el manejo de la interfaz y la lógica de negocio es crucial. El patrón MVVM se enfoca en desacoplar aún más la lógica de presentación de la lógica de negocio, lo que permite que la Vista y el Modelo se comuniquen de manera más eficiente.

¿Por qué utilizar MVVM en lugar de MVC?

El patrón MVC tiene algunas limitaciones en aplicaciones altamente interactivas, ya que cada cambio en el modelo puede requerir una recarga completa de la vista, lo que resulta en una mala experiencia de usuario. Con MVVM, el ViewModel sirve como un intermediario entre el Modelo y la Vista, vinculando datos de manera reactiva sin necesidad de recargar la vista por completo. MVVM resuelve este problema al permitir la actualización en tiempo real de la interfaz de usuario cuando los datos cambian, sin necesidad de recargar toda la página.

Esquema patrones MVC y MVVM


5.18.2 Concepto de Componentes y Composición de Componentes

¿Qué son los Componentes?

En Laravel, un componente es una unidad de funcionalidad reutilizable que incluye tanto la lógica como la presentación (la vista). Los componentes permiten que las vistas se dividan en bloques más pequeños, lo que facilita la reutilización de código y la mantenibilidad de la aplicación. Un componente puede tener su propio Modelo y Vista, y puede ser utilizado en diferentes partes de la aplicación.

Ventajas de Utilizar Componentes:

  • Reutilización de código: Un componente que maneja una funcionalidad puede ser utilizado en varias vistas, evitando duplicación de código.
  • Mantenibilidad: Los cambios realizados a un componente se reflejan en todas las instancias donde se utiliza, lo que mejora la coherencia y facilita el mantenimiento del código.
  • Escalabilidad: Los componentes permiten estructurar una aplicación de manera modular, lo que facilita el crecimiento de la misma sin que la complejidad aumente desmesuradamente.

Composición de Componentes:

La composición de componentes es un enfoque que permite crear interfaces de usuario complejas a partir de componentes más simples. En lugar de tener una única vista que controle toda la interfaz, podemos dividir la interfaz en partes más pequeñas, donde cada parte es un componente. Esto permite gestionar pantallas complejas de una manera más ordenada y eficiente.


5.18.3 Desventajas de Utilizar Componentes

Aunque los componentes ofrecen muchas ventajas, también tienen algunas desventajas que deben ser consideradas:

  • SEO: Los motores de búsqueda no pueden leer directamente el contenido generado por componentes JavaScript, lo que puede afectar el posicionamiento web si no se toman las medidas adecuadas.
  • Accesibilidad: El uso excesivo de JavaScript o de componentes sin una buena estructura semántica puede dificultar el acceso a la aplicación para personas con discapacidades, especialmente aquellas que utilizan lectores de pantalla.
  • Complejidad: Si los componentes no se gestionan adecuadamente, la aplicación puede volverse difícil de mantener y escalar. Los componentes deben estar bien definidos y la comunicación entre ellos debe ser clara.

5.18.4 La Importancia de las API REST en MVVM

En el patrón MVVM, las API REST juegan un papel crucial en la separación de la lógica de negocio y la interfaz de usuario. Las APIs permiten que los Modelos y las Vistas se comuniquen de manera eficiente sin necesidad de recargar toda la página. Las API REST son interfaces que permiten que la lógica del servidor y la presentación en el cliente se comuniquen a través de peticiones HTTP, lo que resulta en un desacoplamiento efectivo de las capas de presentación y lógica de negocio.

Esquema conexión Vista y Modelo a través de API REST


5.18.5 Necesidad de Livewire e Inertia.js para Implementar MVVM

Livewire:

  • Livewire es un framework de Laravel que permite crear interfaces de usuario dinámicas y reactivas sin escribir JavaScript. En lugar de utilizar JavaScript puro para gestionar la reactividad de la interfaz, Livewire permite usar PHP en el backend para manejar la lógica de los componentes, lo que facilita la comunicación entre el modelo y la vista en el patrón MVVM.

Ventajas de Livewire:

  • Permite escribir interfaces interactivas sin necesidad de aprender o utilizar JavaScript.
  • Integra perfectamente con Laravel y Blade.
  • Facilita la vinculación de datos reactiva entre el backend y el frontend.

Inertia.js:

  • Inertia.js es una biblioteca que facilita la creación de SPA (Single Page Applications) utilizando Laravel y frameworks como Vue.js o React. Inertia.js permite mantener la estructura tradicional de Laravel, pero al mismo tiempo habilita una experiencia de usuario más dinámica, similar a las aplicaciones de una sola página.

Ventajas de Inertia.js:

  • No requiere recargar toda la página, como ocurre en las aplicaciones tradicionales de Laravel.
  • Utiliza Vue.js o React para una experiencia de usuario más fluida.
  • Mantiene la estructura de Laravel, lo que facilita el trabajo con rutas y controladores tradicionales.

Diferencias entre Livewire e Inertia.js:

  • Livewire: Usa PHP para gestionar la reactividad y no requiere conocimientos de JavaScript.
  • Inertia.js: Requiere conocimientos de Vue.js o React para gestionar la interactividad, pero ofrece más control sobre la interfaz de usuario.

5.18.6 Livewire

Livewire es un framework full-stack para Laravel que permite construir interfaces interactivas sin necesidad de escribir código JavaScript. Funciona de forma fluida con Laravel y proporciona una forma sencilla de crear componentes dinámicos y reactivamente actualizados en una aplicación web.

Características clave de Livewire:

  1. Interactividad sin JavaScript: Puedes crear interfaces reactivas usando PHP, evitando la complejidad de escribir JavaScript o integrar librerías frontend como Vue.js o React.

  2. Integración con Laravel: Livewire se integra estrechamente con Laravel, lo que permite aprovechar todas las características de Laravel, como la validación de formularios, controladores y bases de datos, directamente en los componentes.

  3. Componentes Dinámicos: Los componentes en Livewire pueden ser actualizados dinámicamente cuando el usuario interactúa con la página, sin tener que recargarla. Livewire maneja la sincronización de datos entre el frontend y el backend automáticamente.

  4. Facilidad de uso: Al usar Livewire, los desarrolladores pueden trabajar principalmente en el backend, lo que simplifica la lógica de las aplicaciones interactivas.

  5. Actualización de la interfaz: Cuando se realiza una acción (como hacer clic en un botón o enviar un formulario), Livewire automáticamente envía la solicitud al servidor, actualiza el estado del componente y lo refleja en la interfaz sin necesidad de recargar la página.

En resumen, Livewire permite construir interfaces interactivas de manera sencilla en Laravel, sin tener que recurrir a JavaScript o frameworks frontend complejos, haciendo que el desarrollo de aplicaciones web modernas sea más accesible y eficiente.

Instalar Livewire:

composer require livewire/livewire
  • Este comando usa Composer, que es el gestor de dependencias para PHP, para instalar el paquete de Livewire en tu proyecto.
  • Al ejecutar este comando, Composer buscará la última versión de Livewire y la descargará e instalará en tu proyecto, actualizando el archivo composer.json para reflejar la nueva dependencia.
  • Livewire es una librería de Laravel que facilita la creación de interfaces interactivas sin tener que escribir mucho JavaScript. Este comando simplemente agrega Livewire a tu proyecto para que puedas usar sus funcionalidades.

5.18.7 Crear Layout y Componente Básico con Livewire

Paso 1: Crear Layout Básico

Crear Layout Básico

En livewire, necesitamos crear un layout básico que sirva como plantilla para nuestras vistas. Este layout contendrá los estilos y scripts necesarios para Livewire.

Primero, en resources/views/_layouts, crea un archivo llamado index.blade.php con una estructura básica de HTML5 y añade los estilos y scripts de Livewire:

<html>
    <head>
        <title>@yield('title')</title>
        @livewireStyles
    </head>
    <body>
        @yield('content')
        @livewireScripts
    </body>
</html>

Como puedes ver, hemos añadido @livewireStyles y @livewireScripts, que son necesarios para que Livewire funcione correctamente. El @yield('content') es donde se insertará el contenido de las vistas que extiendan este layout.

Paso 2: Crear un Componente Livewire

A continuación, crea un componente de Livewire llamado HelloWorld:

php artisan make:livewire HelloWorld

En el archivo HelloWorld.php, añade el siguiente código:

public function render()
{
    return view('livewire.hello-world');
}

Modificamos la vista hello-world.blade.php para que contenga un saludo simple:

<div>
    <h1>Hello World!!</h1>
    <p>my first live wire component</p>
</div>

Paso 3: Modificar welcome.blade.php para Usar el Layout y el Componente

Modifica welcome.blade.php para utilizar el layout creado y el componente HelloWorld:

@extends('_layouts.index')
@section('content')
    <livewire:hello-world />
@endsection

Paso 4: Probar el Componente

Ahora si nos dirigimos a http://localhost:8080/, deberíamos ver el saludo "Hello World!!" en la página. Esto significa que hemos creado correctamente nuestro primer componente de Livewire y lo hemos integrado en nuestra aplicación.


5.18.8 Documentación Oficial de Livewire

Visita la documentación oficial de Livewire para aprender más sobre cómo trabajar con este framework y cómo utilizar sus características avanzadas:


5.18.9 Crear Componente Contador con Livewire

Vamos a dar un paso más y crear un componente de Livewire que funcione como un contador. Este componente tendrá un botón que incrementará el valor del contador cada vez que se haga clic en él. De esta maner podmos observar la reactividad de Livewire.

Paso 1: Crear el Componente Contador

Usa el siguiente comando para crear un nuevo componente de Livewire llamado Contador:

php artisan make:livewire Contador

En el archivo Contador.php, define una propiedad pública llamada contador y un método público llamado incrementar:

public $contador = 0;

public function incrementar()
{
    $this->contador++;
}

public function render()
{
    return view('livewire.contador');
}

Paso 2: Crear la Vista para el Componente Contador

En contador.blade.php, crea un botón para incrementar el contador y una etiqueta para mostrar su valor:

<div>
    <button wire:click="incrementar">Incrementar</button>
    <h1>{{ $contador }}</h1>
</div>

En los componentes podemos controlar los eventos utilizando wire:[nombre_evento], en este caso wire:click para el evento de clic en el botón. Por tanto al igual que hacemos en JS tendremos que implementar la función incrementar en el componente para que responda al evento.

Ahora modificamos la clase livewire contador para que el contador se incremente cada vez que se haga clic en el botón.

public function incrementar()
{
    $this->contador++;
}

De esta manera, cada vez que se haga clic en el botón, el contador se incrementará y la vista se actualizará automáticamente sin necesidad de recargar la página. Esto es lo que llamamos vinculación de datos reactiva.

5.18.12 Ejercicio.

Modifica el componente anteior y añade los botones de decrementar y resetear el contador. El botón decrementar no puede pasar de 0 y el botón resetear debe poner el contador a 0.


5.18.11 Ciclo de Vida de un Componente Livewire

Los componentes de Livewire tienen un ciclo de vida que incluye los métodos:

  • mount: Se ejecuta cuando el componente se monta en la página.
  • render: Devuelve la vista del componente.
  • updating: Se llama cada vez que el estado del componente cambia.
  • updated: Se llama después de que el estado del componente ha cambiado.
  • destroy: Se llama cuando el componente se destruye.

En este link puedes ver todos los métodos del ciclo de vida de un componente Livewire.


5.18.13 Ejemplo Guiado: CRUD con Livewire - Notas

Paso 1: Crear la migración para la tabla notes

Primero, necesitamos crear la tabla y el modelo que almacenará las notas. Ejecuta el siguiente comando para crear una migración:

php artisan make:model Note -m

En la migración generada, en el archivo database/migrations/YYYY_MM_DD_create_notes_table.php, define los campos de la tabla:

public function up()
{
    Schema::create('notes', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('description');
        $table->timestamps();
    });
}

En el modelo Note.php, define los campos que se pueden llenar:

namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Note extends Model
{
    use HasFactory;

    protected $guarded = [];
}

Después, ejecuta la migración para crear la tabla en la base de datos:

php artisan migrate

Paso 2: Crear el componente Livewire NoteComponent

Ahora creamos el componente de Livewire para gestionar las notas:

php artisan make:livewire NoteComponent

En el archivo generado app/Http/Livewire/NoteComponent.php, añade la lógica para gestionar las notas. Definimos las propiedades necesarias y los métodos para manejar el CRUD.

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Note;

class NoteComponent extends Component
{
    public $title, $description, $noteId;

    public function render()
    {
        $notes = Note::all();
        return view('livewire.notas', compact('notes'));
    }

    public function store()
    {
        Note::create([
            'title' => $this->title,
            'description' => $this->description
        ]);

        $this->resetInputFields();
    }

    public function edit($id)
    {
        $note = Note::find($id);
        $this->noteId = $id;
        $this->title = $note->title;
        $this->description = $note->description;
    }

    public function update()
    {
        $note = Note::find($this->noteId);
        $note->update([
            'title' => $this->title,
            'description' => $this->description
        ]);

        $this->resetInputFields();
    }

    public function delete($id)
    {
        Note::find($id)->delete();
    }

    private function resetInputFields()
    {
        $this->title = '';
        $this->description = '';
        $this->noteId = null;
    }
}

Paso 3: Crear la vista para el componente note-component

En resources/views/livewire/note-component.blade.php, crea la vista que gestionará el formulario y mostrará las notas:

<div>
    <form wire:submit.prevent="{{ $noteId ? 'update' : 'store' }}">
        <input type="text" wire:model="title" placeholder="Title" />
        <textarea wire:model="description" placeholder="Description"></textarea>
        <button type="submit">{{ $noteId ? 'Update' : 'Create' }}</button>
    </form>

    <ul>
        @foreach($notes as $note)
            <li>
                {{ $note->title }} - {{ $note->description }}
                <button wire:click="edit({{ $note->id }})">Edit</button>
                <button wire:click="delete({{ $note->id }})">Delete</button>
            </li>
        @endforeach
    </ul>
</div>

Paso 4: Crear las rutas y controladores para Notes

En routes/web.php, define la ruta para mostrar el componente NotesComponent:

use App\Http\Livewire\NoteComponent;

Route::view('/notes', 'notes.index')->name('notes.index');

Por último creamos una vista que cargue el layout y el nuevo componente views/notes/index.blade.php:

@extends('_layouts.index')
@section('content')
    <livewire:note-component />
@endsection

En este caso, el componente NoteComponent se encargará de gestionar la lógica del CRUD y la vista mostrará el formulario y la lista de notas.


Paso 5: Probar la funcionalidad

  • Visita http://localhost/notes en tu navegador.
  • Crea, edita y elimina notas para verificar que el CRUD está funcionando correctamente.

Ejercicio:

Añade una propiedad $message al componente Notas y muestra un mensaje de éxito o error después de cada acción (crear, editar, eliminar).

Paso 6: Añadir validación al formulario de Notas

Para añadir validación al formulario de Notas, podemos utilizar las reglas de validación de Laravel en el método store y update del componente NoteComponent. Modificamos el componente para incluir la validación:

protected $rules = [
    'title' => 'required|string|max:255',
    'description' => 'required|string|max:1000',
];
public function store()
{
    $this->validate();

    Note::create([
        'title' => $this->title,
        'description' => $this->description
    ]);

    $this->resetInputFields();
}
public function update()
{
    $this->validate();

    $note = Note::find($this->noteId);
    $note->update([
        'title' => $this->title,
        'description' => $this->description
    ]);

    $this->resetInputFields();
}

De esta manera, antes de crear o actualizar una nota, se validarán los campos title y description. Si la validación falla, Livewire enviará automáticamente los mensajes de error en la vista.

El siguiente paso es mostrar los mensajes de error en la vista. En notes/index.blade.php, añade el siguiente código para mostrar los errores de validación:

@if ($errors->any())
    <div class="alert alert-danger">
        <ul>
            @foreach ($errors->all() as $error)
                <li>{{ $error }}</li>
            @endforeach
        </ul>
    </div>
@endif
Esto mostrará una lista de errores si hay alguno al enviar el formulario. Puedes personalizar el estilo de los mensajes de error según tus necesidades.

5.18.14 Ejemplo Guiado: CRUD con Livewire - Productos

Paso 1: Crear la migración para la tabla products

Primero, necesitamos crear la tabla que almacenará los productos y el modelo. Ejecuta el siguiente comando para crear una migración:

php artisan make:model Products -m

En la migración generada, en el archivo database/migrations/YYYY_MM_DD_create_products_table.php, define los campos de la tabla:

public function up()
{
    Schema::create('products', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->text('short_description');
        $table->text('description');
        $table->decimal('price', 8, 2)->default(20.00);
        $table->timestamps();
    });
}

Después, ejecuta la migración para crear la tabla en la base de datos:

php artisan migrate

Paso 2: Crear el componente Livewire ProductsCompoenet

Ahora creamos el componente de Livewire para gestionar los productos:

php artisan make:livewire ProductsComponent

En el archivo generado app/Http/Livewire/ProductsComponent.php, añade la lógica para gestionar el CRUD:

namespace App\Http\Livewire;

use Livewire\Component;
use App\Models\Product;

class ProductsComponent extends Component
{
    public $name, $short_description, $description, $price, $productId;

    public function render()
    {
        $products = Product::all();
        return view('livewire.products-component', compact('products'));
    }

    public function store()
    {
        Product::create([
            'name' => $this->name,
            'short_description' => $this->short_description,
            'description' => $this->description,
            'price' => $this->price
        ]);

        $this->resetInputFields();
    }

    public function edit($id)
    {
        $product = Product::find($id);
        $this->productId = $id;
        $this->name = $product->name;
        $this->short_description = $product->short_description;
        $this->description = $product->description;
        $this->price = $product->price;
    }

    public function update()
    {
        $product = Product::find($this->productId);
        $product->update([
            'name' => $this->name,
            'short_description' => $this->short_description,
            'description' => $this->description,
            'price' => $this->price
        ]);

        $this->resetInputFields();
    }

    public function delete($id)
    {
        Product::find($id)->delete();
    }

    private function resetInputFields()
    {
        $this->name = '';
        $this->short_description = '';
        $this->description = '';
        $this->price = '';
        $this->productId = null;
    }
}

Paso 3: Crear la vista para el componente ProductsComponent

En resources/views/livewire/products-component.blade.php, crea la vista que gestionará el formulario y mostrará los productos:

<div>
    <form wire:submit.prevent="{{ $productId ? 'update' : 'store' }}">
        <input type="text" wire:model="name" placeholder="Product Name" />
        <input type="text" wire:model="short_description" placeholder="Short Description" />
        <textarea wire:model="description" placeholder="Description"></textarea>
        <input type="number" wire:model="price" placeholder="Price" />
        <button type="submit">{{ $productId ? 'Update' : 'Create' }}</button>
    </form>

    <ul>
        @foreach($products as $product)
            <li>
                {{ $product->name }} - ${{ $product->price }}
                <button wire:click="edit({{ $product->id }})">Edit</button>
                <button wire:click="delete({{ $product->id }})">Delete</button>
            </li>
        @endforeach
    </ul>
</div>

Paso 4: Crear las rutas para mostrar los productos

En routes/web.php, define la ruta para mostrar los productos:

Route::view('/productos', 'products.index')->name('products.index');

Paso 5: Crear la vista products.index para cargar el componente de productos

blade title="resources/views/products/index.blade.php" {linenums="1"} @extends('layouts.index') @section('content') <livewire:products-index /> @endsection

  • Visita http://localhost/products en tu navegador.
  • Crea, edita y elimina productos para verificar que el CRUD está funcionando correctamente.

Resumen de la Aplicación

  • En la página principal de tu aplicación, tendrás dos secciones: Notas y Productos, ambas gestionadas a través de Livewire.
  • Ambas funcionalidades CRUD se manejarán con sus respectivos componentes, y se integrarán en un mismo menú en la vista principal.