Diferencia entre revisiones de «PHP Livewire»

De MediaWiki
Ir a la navegación Ir a la búsqueda
 
(No se muestran 54 ediciones intermedias del mismo usuario)
Línea 17: Línea 17:
  
 
* No directorio raíz do proxecto Laravel escribe: '''composer require livewire/livewire'''
 
* No directorio raíz do proxecto Laravel escribe: '''composer require livewire/livewire'''
 +
: '''ATENCIÓN:'''  A día de hoxe (2026) saiu a versión 4.0 de Livewire. No exame imos empregar a 4.0.
 +
: : Polo tanto empregar esta orden: <u>composer require livewire/livewire "^4.0"</u>  para descargar a versión 4.0 (non poñer sudo)
  
−
: Unha vez instalado en Laravel podedes consultar a versión instalada buscando a entrada livewire/livewire no arquivo '''composor.json''' no raíz do proxecto.
+
: Unha vez instalado en Laravel podedes consultar a versión instalada buscando a entrada livewire/livewire no arquivo '''composer.json''' no raíz do proxecto.
−
: Polo de agora (versión 10-12) estamos a traballar coa versión 3.x de Livewire.
+
: Polo de agora coa versión 13 de Laravel imos empregar a versión 4.0 de Livewire.
  
  
Línea 26: Línea 28:
  
 
==Validación de campos==
 
==Validación de campos==
 +
 +
* Creamos en mysql (se non o tema xa de exercicios anteriores) unha base de datos Mysql de nome '''perfumes'''.
 +
: Lembrar que o usuario configurado en Laravel para acceder á base de datos debería ter os permisos mínimos para que a aplicación funcione.
  
 
* Imos crear a táboa marcas e imos crear unha relación entre perfumes e marcas.
 
* Imos crear a táboa marcas e imos crear unha relación entre perfumes e marcas.
Línea 50: Línea 55:
 
: '''Táboa perfumes'''
 
: '''Táboa perfumes'''
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
−
CREATE TABLE perfumes.perfumes (
+
CREATE TABLE `perfumes.perfumes` (
−
   `id` int NOT NULL,
+
   `id` int NOT NULL AUTO_INCREMENT,
 
   `descripcion` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL,
 
   `descripcion` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL,
 
   `prezo` decimal(8,2) NOT NULL,
 
   `prezo` decimal(8,2) NOT NULL,
 
   `data_compra` date DEFAULT NULL,
 
   `data_compra` date DEFAULT NULL,
−
   `marca_id` int DEFAULT NULL
+
   `marca_id` int DEFAULT NULL,
−
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;
+
  PRIMARY KEY (`id`),
 +
  KEY `fk_PERFUMES_1_idx` (`id`),
 +
  KEY `fk_PERFUMES_MARCA_idx` (`marca_id`),
 +
  CONSTRAINT `fk_perfumes_1` FOREIGN KEY (`marca_id`) REFERENCES `marcas` (`id`) ON UPDATE CASCADE
 +
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;
  
−
ALTER TABLE perfumes.perfumes
 
−
  ADD PRIMARY KEY (`id`),
 
−
  ADD KEY `fk_PERFUMES_1_idx` (`id`),
 
−
  ADD KEY `fk_PERFUMES_MARCA_idx` (`marca_id`);
 
  
 
INSERT INTO perfumes.perfumes (`id`, `descripcion`, `prezo`, `data_compra`, `marca_id`) VALUES
 
INSERT INTO perfumes.perfumes (`id`, `descripcion`, `prezo`, `data_compra`, `marca_id`) VALUES
Línea 189: Línea 194:
 
@endif
 
@endif
  
−
@if(count($errors) > 0)
 
−
<div class="errors">
 
−
<ul>
 
−
@foreach($errors->all() as $error)
 
−
<li>{{ $error }}</li>
 
−
@endforeach
 
−
</ul>
 
−
</div>
 
−
@endif
 
  
  
Línea 257: Línea 253:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: <u<Nota:</u> Non separamos os campos nun arquivo diferente xa que en livewire as operacións de alta e modificación vanse facer sobre a mesma vista.
+
:<u>Nota:</u> Non separamos os campos nun arquivo diferente xa que en livewire as operacións de alta e modificación vanse facer sobre a mesma vista.
  
  
Línea 277: Línea 273:
 
................
 
................
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
 +
* <u>Nota:</u> Outra forma de añadir o livewire en la vista es: <b>@livewire(Marca:class)</b> o también <b>@livewire('marca')</b>
 +
 +
  
  
Línea 315: Línea 315:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
:* wire:model="descripcion": O que estamos a dicir con isto é que este input está vinculado á propiedade $descripcion definida no compoñente Livewire. Calquera modificación realizada polo usuario no cliente (na páxina web) actualizará automaticamente o valor da propiedade no servidor grazas á reactividade proporcionada por Livewire. Ademais, se a propiedade no servidor cambia, ese cambio tamén se reflectirá no cliente.
+
:* wire:model="descripcion": O que estamos a dicir con isto é que este input está vinculado á propiedade $descripcion definida no compoñente Livewire. Calquera modificación realizada polo usuario no cliente (na páxina web), <u>cando se faga unha chamada ao controlador livewire</u>, actualizará automaticamente o valor da propiedade no servidor grazas á reactividade proporcionada por Livewire. Ademais, se a propiedade no servidor cambia, ese cambio tamén se reflectirá no cliente.
 
:: Se vos dades conta non temos a propiedade value como tiñamos en perfumes.
 
:: Se vos dades conta non temos a propiedade value como tiñamos en perfumes.
 
:: Se agora cargades a páxina veredes como o valor inicial se carga no input.
 
:: Se agora cargades a páxina veredes como o valor inicial se carga no input.
Línea 380: Línea 380:
  
 
: Con isto <b>wire:submit.prevent="gardarMarca"</b> estamos a decir que cando faga o submit chame de forma asíncrona ao controlador do compoñente livewire ao método gardarMarca.
 
: Con isto <b>wire:submit.prevent="gardarMarca"</b> estamos a decir que cando faga o submit chame de forma asíncrona ao controlador do compoñente livewire ao método gardarMarca.
 +
: No caso de manexar formularios en livewire, empregar sempre prevent. Poderíamos facelo con <u>wire:submit</u> pero nese caso a páxina enteira se carga como se fora un submit 'tradicional'.
 +
 +
 +
 
: <u>Nota:</u> Podemos asociar calquera click de botón directamente ao un método no controlador do livewire, da forma: <button wire:click="gardarMarca">+</button>
 
: <u>Nota:</u> Podemos asociar calquera click de botón directamente ao un método no controlador do livewire, da forma: <button wire:click="gardarMarca">+</button>
  
Línea 416: Línea 420:
 
* Fixerase que neste caso a validación faise cando o usuario preme o botón de submit.
 
* Fixerase que neste caso a validación faise cando o usuario preme o botón de submit.
 
: Se queremos que se faga a validación cando o usuario cambio o contido da caixa de texto temos que:
 
: Se queremos que se faga a validación cando o usuario cambio o contido da caixa de texto temos que:
−
:* Empregar o método update($propiedade) que será chamado de forma automáticamente cando o campo teña unha modificación. Dito método fará a validación da propiedade. Temos que chamar ao método '''validatyOnly'''.
+
:* Empregar o método update($propiedade,$valor) que será chamado de forma automáticamente cando o campo teña unha modificación. Dito método fará a validación da propiedade. Temos que chamar ao método '''validatyOnly'''.
 
:* Cambiar a propiedade wire:model da caixa de texto por '''wire:model.live'''
 
:* Cambiar a propiedade wire:model da caixa de texto por '''wire:model.live'''
 
: Lembrar que estas chamadas fanse ao servidor polo que pode ser que non nos interese para aforrar recursos e comunicacións entre cliente e o servidor.
 
: Lembrar que estas chamadas fanse ao servidor polo que pode ser que non nos interese para aforrar recursos e comunicacións entre cliente e o servidor.
Línea 435: Línea 439:
  
 
     ..............
 
     ..............
−
     public function updated($propiedade){
+
     public function updated($propiedade,$valor){
 
         $this->validateOnly($propiedade);
 
         $this->validateOnly($propiedade);
 
     }
 
     }
Línea 442: Línea 446:
  
  
−
: Ao facer isto, cada vez que se preme no imput fará a validación.
+
: Ao facer isto, cada vez que se preme no input fará a validación.
 
: Lembrar que poñer isto non quita que teñades a chamada $this->validate() cando se vaia a dar de alta/modificar os datos
 
: Lembrar que poñer isto non quita que teñades a chamada $this->validate() cando se vaia a dar de alta/modificar os datos
  
Línea 451: Línea 455:
 
<br />
 
<br />
  
−
==Operacións de alta-baixa-modificación==
+
==Operacións de alta-baixa-modificación-listado==
  
 
===Alta===
 
===Alta===
  
−
* A alta faise como sempre pero agora está no controlador do compoñente livewire.
+
* A alta faise como sempre pero agora está no controlador do compoñente livewire.
−
: Hai un pequeno detalle xa que como chamamos Alta ao compoñente Livewire, se empregamos o modelo Marca teríamos o mesmo nome. Por iso empregaremos un alias para o modelo.
+
:Hai un pequeno detalle xa que como chamamos Alta ao compoñente Livewire, se empregamos o modelo Marca teríamos o mesmo nome. Por iso empregaremos un alias para o modelo.
  
  
−
: '''Arquivo: app/Livewire/Marca.php'''
+
:'''Arquivo: app/Livewire/Marca.php'''
 
::<syntaxhighlight lang="java" enclose="div" highlight="1,9-15" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="1,9-15" >
 
   use App\Models\Marca as MarcaModel;
 
   use App\Models\Marca as MarcaModel;
Línea 482: Línea 486:
  
  
−
: Fixarse que para enviar unha información á vista empregamos session()->flash que vai gardar nunha variable de sesión (message) o texto a amosar.
+
:Fixarse que para enviar unha información á vista empregamos session()->flash que vai gardar nunha variable de sesión (message) o texto a amosar.
−
: Dita variable non pode ser empregada pola páxina, soamente polo compoñente livewire xa que non se recarga a páxina enteira.
+
:Dita variable non pode ser empregada pola páxina, soamente polo compoñente livewire xa que non se recarga a páxina enteira.
−
: A chamada a reset vai poñer en branco os valor da variable $descripcion do compoñente. Pero como temos un valor por defecto na definición poñerá dito valor. Lembrar que non é obrigatorio e podemos quitalo.
+
:A chamada a reset vai poñer en branco os valor da variable $descripcion do compoñente. Pero como temos un valor por defecto na definición poñerá dito valor. Lembrar que non é obrigatorio e podemos quitalo.
  
−
: Tamén indicar que podemos facer o create da forma: '''MarcaModel::create($this->all())'''
+
:Tamén indicar que podemos facer o create da forma: '''MarcaModel::create($this->all())'''
  
  
Línea 495: Línea 499:
  
 
* Para amosar as mensaxes de que todo foi correcto, imos crear un arquivo na vista.
 
* Para amosar as mensaxes de que todo foi correcto, imos crear un arquivo na vista.
−
: Tamén imos poñer nese arquivo os posibles erros que enviemos dende o controlador.
+
:Tamén imos poñer nese arquivo os posibles erros que enviemos dende o controlador.
  
−
: '''Arquivo app/views/mensaxes/mensaxes.blade.php'''. Lembrar crear o cartafol mensaxes.
+
:'''Arquivo app/views/mensaxes/mensaxes.blade.php'''. Lembrar crear o cartafol mensaxes.
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
  
Línea 509: Línea 513:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
: Simplemente amosamos o contido da variable de sesión message.
+
:Simplemente amosamos o contido da variable de sesión message.
  
  
  
 
* Incluimos dito arquivo na vista do compoñente livewire:
 
* Incluimos dito arquivo na vista do compoñente livewire:
−
: '''Arquivo app/views/livewire/marca.blade.php'''.
+
:'''Arquivo app/views/livewire/marca.blade.php'''.
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
<div>
 
<div>
Línea 535: Línea 539:
 
     @include('mensaxes.mensaxes')
 
     @include('mensaxes.mensaxes')
 
</div>
 
</div>
 +
</syntaxhighlight>
  
−
: '''IMPORTANTE:''' O compoñente livewire soamente pode ter un elemento raíz, no exemplo posto <div>. Non podedes poñer o include fora do div.
+
:'''IMPORTANTE:''' O compoñente livewire soamente pode ter un elemento raíz, no exemplo posto <div>. Non podedes poñer o include fora do div.
  
−
 
−
</syntaxhighlight>
 
  
  
 
* E que pasa se queremos ir a outra pantalla ou que recargue toda a páxina e non soamente o compoñente livewire ? Debemos empregar o método redirectRoute() poñendo o nome da ruta á que queremos ir.
 
* E que pasa se queremos ir a outra pantalla ou que recargue toda a páxina e non soamente o compoñente livewire ? Debemos empregar o método redirectRoute() poñendo o nome da ruta á que queremos ir.
−
: : '''Arquivo: app/Livewire/Marca.php'''
+
:'''Arquivo: app/Livewire/Marca.php'''
 
::<syntaxhighlight lang="java" enclose="div" highlight="15" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="15" >
  
Línea 587: Línea 590:
  
 
<livewire:marca modo="actualizar" :id="$marca->id"/>    <!-- Se queres enviar unha cadea  ou numero poñemos clave="valor". Os dous puntos (:id) é para enviar unha variable de Laravel -->
 
<livewire:marca modo="actualizar" :id="$marca->id"/>    <!-- Se queres enviar unha cadea  ou numero poñemos clave="valor". Os dous puntos (:id) é para enviar unha variable de Laravel -->
−
 
−
@if(count($errors) > 0)
 
−
<div class="errors">
 
−
<ul>
 
−
@foreach($errors->all() as $error)
 
−
<li>{{ $error }}</li>
 
−
@endforeach
 
−
</ul>
 
−
</div>
 
−
@endif
 
−
 
  
 
@include('mensaxes.alertas_javascript')
 
@include('mensaxes.alertas_javascript')
Línea 699: Línea 691:
 
     }
 
     }
  
−
     public function updated($propiedade){
+
     public function updated($propiedade,$valor){
 
         $this->validateOnly($propiedade);
 
         $this->validateOnly($propiedade);
 
     }
 
     }
Línea 794: Línea 786:
 
         <tbody>
 
         <tbody>
 
             @forelse ($marcas as $marca)
 
             @forelse ($marcas as $marca)
−
                 <tr>
+
                 <tr wire:key="marca-{{ $marca->id }}">
 
                     <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
 
                     <td class="border border-gray-300 p-2">
 
                     <td class="border border-gray-300 p-2">
−
                         <button class="bg-blue-500 text-white px-4 py-1 rounded">Editar</button>
+
                         <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
 +
                            Editar
 +
                        </a>
 
                         <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 
                         <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 
                     </td>
 
                     </td>
Línea 811: Línea 805:
  
 
     @include('mensaxes.mensaxes')
 
     @include('mensaxes.mensaxes')
−
 
−
    @if (count($errors) > 0)
 
−
        <div class="errors">
 
−
            <ul>
 
−
                @foreach ($errors->all() as $error)
 
−
                    <li>{{ $error }}</li>
 
−
                @endforeach
 
−
            </ul>
 
−
        </div>
 
−
    @endif
 
−
 
  
 
</div>
 
</div>
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
'''NOTAS:'''
 +
:* Fixarse que cambiamos o botón de modificar por un enlace. Se podería empregar un botón cun formulario ou outra forma de que cargue unha url.
 +
:* Fixarse que no bucle o tr ten un wire:key. Isto é necesario sempre que teñades un bucle e empregedes:
 +
::* @foreach de elementos dinámicos
 +
::* Botóns wire:click dentro do loop
 +
::* Eliminación ou actualización de filas
 +
::* Inputs editables en lista
 +
::* Ordenaión ou filtros
 +
  
  
Línea 867: Línea 859:
 
====Paxinación====
 
====Paxinación====
  
 +
* Máis información en: [https://laravel.com/docs/10.x/pagination https://laravel.com/docs/10.x/pagination]
 
* Faise de forma parecido a como se fai en Laravel cando temos unha vista.
 
* Faise de forma parecido a como se fai en Laravel cando temos unha vista.
 
: No controlador do compoñente livewire imos indicar que empregaremos paxinación e cando obtemos a lista de marcas faremos o paxinate.
 
: No controlador do compoñente livewire imos indicar que empregaremos paxinación e cando obtemos a lista de marcas faremos o paxinate.
Línea 888: Línea 881:
 
     public function render()
 
     public function render()
 
     {
 
     {
−
         $marcas = Marca::orderBy('descripcion', 'asc')->paginate(3);
+
         $marcas = Marca::orderBy('descripcion', 'asc')->simplePaginate(3);
  
 
         return view('livewire.marca-listado', compact('marcas'));
 
         return view('livewire.marca-listado', compact('marcas'));
Línea 895: Línea 888:
  
 
</syntaxhighlight>
 
</syntaxhighlight>
 +
:<u>Nota:</u> Poño simplePaginate en vez de Paginate xa que por defecto engade dúas iconas para desprazarse á dereita/esqueda polas páxinas, pero ditas iconas se ven moi grandes, supoño a que por defecto emprega Tailwind CSS. Se consultades o enlace superior indica como empregar Bootstrap.
 +
  
  
 
* Agora imos indicarlle á vista que visualice os enlaces de seguinte e anterior:
 
* Agora imos indicarlle á vista que visualice os enlaces de seguinte e anterior:
 
* '''Arquivo resources/views/livewire/marca-listado.blade.php'''
 
* '''Arquivo resources/views/livewire/marca-listado.blade.php'''
−
::<syntaxhighlight lang="java" enclose="div" highlight="" >
+
::<syntaxhighlight lang="java" enclose="div" highlight="30-32" >
 
<div>
 
<div>
  
Línea 912: Línea 907:
 
         <tbody>
 
         <tbody>
 
             @forelse ($marcas as $marca)
 
             @forelse ($marcas as $marca)
−
                 <tr>
+
                 <tr wire:key="marca-{{ $marca->id }}">
 
                     <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
 
                     <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
 
                     <td class="border border-gray-300 p-2">
 
                     <td class="border border-gray-300 p-2">
−
                         <button class="bg-blue-500 text-white px-4 py-1 rounded">Editar</button>
+
                         <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
 +
                            Editar
 +
                        </a>
 
                         <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 
                         <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 
                     </td>
 
                     </td>
Línea 932: Línea 929:
  
 
     @include('mensaxes.mensaxes')
 
     @include('mensaxes.mensaxes')
−
 
−
    @if (count($errors) > 0)
 
−
        <div class="errors">
 
−
            <ul>
 
−
                @foreach ($errors->all() as $error)
 
−
                    <li>{{ $error }}</li>
 
−
                @endforeach
 
−
            </ul>
 
−
        </div>
 
−
    @endif
 
−
 
  
 
</div>
 
</div>
Línea 953: Línea 939:
  
 
<br />
 
<br />
 +
 
====Filtro====
 
====Filtro====
  
Línea 978: Línea 965:
  
 
* '''Arquivo app/Livewire/MarcaListado.php'''
 
* '''Arquivo app/Livewire/MarcaListado.php'''
−
::<syntaxhighlight lang="java" enclose="div" highlight="14,15,20" >
+
::<syntaxhighlight lang="java" enclose="div" highlight="14,15,21" >
 
<?php
 
<?php
 
namespace App\Livewire;
 
namespace App\Livewire;
Línea 1000: Línea 987:
 
     {
 
     {
 
         $marcas = Marca::where('descripcion', 'like', '%' . $this->filtro . '%')->orderBy('descripcion', 'asc')
 
         $marcas = Marca::where('descripcion', 'like', '%' . $this->filtro . '%')->orderBy('descripcion', 'asc')
−
             ->paginate(5);
+
             ->simplePaginate(5);
  
  
Línea 1033: Línea 1020:
  
 
* A baixa vai consistir en chamar a un método do controlador ao que lle mandaremos o id da marca a dar de baixa.
 
* A baixa vai consistir en chamar a un método do controlador ao que lle mandaremos o id da marca a dar de baixa.
−
: Polo tanto definimos  
+
:Polo tanto definimos o método no controlador.
  
 +
 +
'''Arquivo app/Livewire/MarcaListado.php'''
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
  ................
 +
    public function eliminarMarca($id){
 +
        $marca = Marca::find($id);  // Se poñemos findOrFail a aplicación pararía se non o atopa.
 +
 +
        if ($marca) {
 +
            $marca->delete();
 +
            session()->flash('message', 'Marca eliminada correctamente.');
 +
        } else {
 +
            $this->addError('error', 'Marca non atopada.');
 +
        }
 +
    }
 +
 +
</syntaxhighlight>
 +
 +
:Como vedes é moi sinxelo. Buscamos a marca baseada nese id e se a atopa a borra. Despois xa chama a render automaticamente. Se non atopa envía unha mensaxe de erro.
 +
: Empregamos $this->addError para gardar o erro na variable 'error'.
 +
 +
 +
* '''IMPORTANTE:''' Ao facer que o botón de Baixa se gestione con wire:click, se queremos pedir confirmación do borrado non podemos empregar confirm de javascripr, xa que vai realizar a llamada igual. Debemos empregar '''wire:confirm''' como está indicado no exemplo seguinte.
 +
 +
 +
'''Arquivo resources/views/livewire/marca-listado.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="39-43" >
 +
<div>
 +
    <input type="text" wire:model="filtro" placeholder="Buscar marcas..." class="mb-4 p-2 border rounded">
 +
    <input type="button" wire:click="$refresh" value="Buscar" />
 +
    <table class="table-auto w-full border-collapse border border-gray-300">
 +
        <thead>
 +
            <tr>
 +
                <th class="border border-gray-300 p-2">ID</th>
 +
                <th class="border border-gray-300 p-2">Descripción</th>
 +
                <th class="border border-gray-300 p-2">Acciones</th>
 +
            </tr>
 +
        </thead>
 +
        <tbody>
 +
            @forelse ($marcas as $marca)
 +
                <tr>
 +
                    <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
 +
                    <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
 +
                    <td class="border border-gray-300 p-2">
 +
                        <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
 +
                            Editar
 +
                        </a>
 +
                        <button wire:click="eliminarMarca({{ $marca->id }})"
 +
                            wire:confirm="Estás seguro de borrar la marca {{ $marca->descripcion }}? "
 +
                            class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 +
                    </td>
 +
                </tr>
 +
            @empty
 +
                <tr>
 +
                    <td colspan="3" class="border border-gray-300 p-2 text-center">No se encontraron marcas</td>
 +
                </tr>
 +
            @endforelse
 +
        </tbody>
 +
    </table>
 +
    <div class="mt-4">
 +
        {{ $marcas->links() }}
 +
    </div>
 +
 +
    @include('mensaxes.mensaxes')
 +
 +
    @error('error')
 +
        <div class="alert alert-danger">
 +
            {{ $message }}
 +
        </div>
 +
    @enderror
 +
 +
</div>
 +
 +
</syntaxhighlight>
 +
: Comprobamos se temos un erro na variable 'error' de livewire.
 +
 +
 +
<br />
 +
 +
===Axustes finais===
 +
 +
* Imos modificar a modificación e alta para que vain á pantalla de listado.
 +
:'''Arquivo app/Livewire/Marca.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="58,72" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Illuminate\Support\Facades\Log;
 +
 +
use Livewire\Attributes\Validate;
 +
use App\Models\Marca as MarcaModel;
 +
 +
use Livewire\Component;
 +
 +
class Marca extends Component
 +
{
 +
    public $descripcion = "Valor inicial";
 +
    public $id; // A empregamos na operación de modificación/baixa para atopar a marca a modificar ou borrar
 +
    public $modo = "engadir";
 +
 +
    protected $rules = [
 +
        'descripcion' => 'required|string|min:5|max:45',
 +
    ];
 +
 +
    protected $messages = [
 +
        'descripcion.required' => 'La descripción es obligatoria.',
 +
        'descripcion.string' => 'La descripción debe ser una cadena de texto.',
 +
        'descripcion.min' => 'La descripción debe tener al menos 5 caracteres.',
 +
        'descripcion.max' => 'La descripción no puede tener más de 45 caracteres.',
 +
    ];
 +
 +
    public function mount($id = null)
 +
    {
 +
        $this->id = $id;
 +
        if($id) {
 +
            $marca = MarcaModel::findOrFail($id);
 +
            $this->descripcion = $marca->descripcion;
 +
        }
 +
    }
 +
 +
    public function render()
 +
    {
 +
        return view('livewire.marca');
 +
    }
 +
 +
    public function gardarMarca()
 +
    {
 +
        $this->validate();
 +
 +
        Log::info('Pasa a validación');
 +
 +
        $marcaCreada = MarcaModel::create([
 +
            'descripcion' => $this->descripcion
 +
        ]);
 +
 +
        $this->id = $marcaCreada->id;
 +
 +
        session()->flash('message', "Marca creada correctamente con id $marcaCreada->id");
 +
        $this->reset(['descripcion']);
 +
 +
        return $this->redirectRoute('marcas.index');
 +
    }
 +
 +
    public function actualizarMarca()
 +
    {
 +
        $this->validate();
 +
 +
        $marca = MarcaModel::findOrFail($this->id);
 +
        $marca->update([
 +
            'descripcion' => $this->descripcion,
 +
        ]);
 +
 +
        session()->flash('message', 'Marca actualizada correctamente.');
  
 +
        return $this->redirectRoute('marcas.index');
 +
 +
    }
 +
 +
    public function updated($propiedade,$valor){
 +
        $this->validateOnly($propiedade);
 +
    }
 +
 +
}
 +
 +
</syntaxhighlight>
 +
 +
 +
* Engado a entrada de listar perfumes.
 +
:'''Arquivo resources/plantilla.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="25" >
 +
<!DOCTYPE html>
 +
<html>
 +
    <head>
 +
        <title>@yield('pageTitle')</title>
 +
 +
        <style>
 +
            .menu{
 +
                float: left;
 +
                width: 30%;
 +
                padding-left: 5px;
 +
            }
 +
            .menu a{
 +
                display: block;
 +
            }
 +
            .contido{
 +
                float: left;
 +
                width: 65%;
 +
            }
 +
 +
        </style>
 +
    </head>
 +
    <body>
 +
        <div class="menu">
 +
            <a href='{{route('perfumes.index')}}'>Operacións Perfumes</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
 +
            <a href='{{route('marcas.index')}}'>Operacións Marcas</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
 +
        </div>
 +
        <div class="contido">
 +
            @yield('content')
 +
        </div>
 +
    </body>
 +
</html>
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
<br />
 +
===Aclaracións varias===
 +
* Neste exercicio empregamos o controlador de Laravel para cargar o mesmo compoñente livewire na alta e modificación empregando un parámetro para informar ao compoñnente en que 'modo' se atopaba.
 +
 +
: <u>Normalmente isto non é necesario.</u>
 +
: Pódese facer unha única vista no controlador de Laravel que cargue o compoñente livewire e xestionar dende dentro do compoñente o listado, alta, baixa e modificación.
 +
: Dentro do compoñente podes definir unha variable pública estado que determine cando a vista ten que visualizar o listado, a alta-modificación.
 +
 +
 +
: Vos queda como exercicio para facer.
 +
: Tedes un exemplo de cómo facelo nos vídeos da plataforma a distancia.
 +
 +
 +
 +
<br />
 +
 +
==Exercicio proposto==
 +
 +
* Imos implementar a <u>xestión da perfumes</u> empregando compoñentes Livewire.
 +
: Dexiaremos [https://wiki.cifprodolfoucha.es/index.php?title=PHP_Modelo_-_Vista_-_Controlador#Exercicio_1:_Xesti.C3.B3n_dunha_.C3.BAnica_t.C3.A1boa o feito na UD6] e faremos unha nova versión de xestión dos perfumes (listado, alta, baixa e modificación) empregando compoñentes Livewire.
 +
 +
 +
 +
* Para cargar os compoñentes dentro da páxina master de Laravel crearemos o cartafol resources/views/'''perfumes_b'''/ e o controlador de Laravel chamarase '''PerfumeBController'''.
 +
: As rutas as teremos que definir nos (non podemos empregar resource xa que perfumes están definidas para empregar o controlador de Laravel).
 +
: Empregaremos:
 +
:* perfumesB/ => PerfumeBController@index
 +
 +
 +
* No listado (index) imos a poder:
 +
:* Crear un novo perfume
 +
:* Modificar un perfume
 +
:* Borrar un perfume
 +
 +
: Na creación/modificación do perfume imos engadir un campo novo, que vai a ser un select coa marca á que pertence o perfume.
 +
 +
* Imos empregar dous compoñentes Livewire:
 +
:* O encargado de listar os perfumes. Nome '''perfume-listar'''
 +
:* O encargado de engadir/modificar un perfume. Nome '''perfume'''
 +
 +
: No seguinte punto imos ver como podemos enviar 'mensaxes' de un compoñente a outro. Polo de agora ides amosar os dous compoñentes na mesma páxina e desta forma aparecerá o listado e os campos para dar de alta un novo perfume ao mesmo tempo. Despois solucionaremos o problema de como facer que non apareza ata que se pulse o botón de Novo Perfume e que cambie o contido/acción cando se prema o de modificar perfume. Polo de agora <u>non fagades o código de modificar perfume</u>. Soamente amosar o botón pero que non faga nada.
 +
 +
: É dicir, a páxina '''resources/views/perfumes_b/perfumes.blade.php''' terá este código:
 +
::<syntaxhighlight lang="java" enclose="div" highlight="9,11" >
 +
@extends('plantilla');
 +
 +
@section('pageTitle', 'Listado de perfumes con Livewire')
 +
 +
@section('content')
 +
<h2>Listado de Perfumes con Livewire</h2>
 +
 +
 +
@livewire("perfume-listado")
 +
 +
@livewire("perfume")
 +
 +
@include('mensaxes.alertas_javascript')
 +
 +
 +
@endsection
 +
 +
</syntaxhighlight>
 +
 +
: E o compoñente livewire perfume amosará os campos para dar de alta un novo perfume.
 +
[[Image:Ud_livewire_1.png|center|1000px]]
 +
 +
 +
 +
 +
* Lembrar, se non o fixéchedes antes, comprobar que a táboa perfumes ten un campo marca_id relacionada coa táboa marcas.
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
CREATE TABLE `perfumes` (
 +
  `id` int NOT NULL AUTO_INCREMENT,
 +
  `descripcion` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL,
 +
  `prezo` decimal(8,2) NOT NULL,
 +
  `data_compra` date DEFAULT NULL,
 +
  `marca_id` int DEFAULT NULL,
 +
  PRIMARY KEY (`id`),
 +
  KEY `fk_PERFUMES_1_idx` (`id`),
 +
  KEY `fk_PERFUMES_MARCA_idx` (`marca_id`),
 +
  CONSTRAINT `fk_perfumes_1` FOREIGN KEY (`marca_id`) REFERENCES `marcas` (`id`) ON UPDATE CASCADE
 +
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;
 +
</syntaxhighlight>
 +
 +
: E crear a relación en Laravel entre os dous modelos.
 +
<br />
 +
 +
 +
 +
* Se seguides a solución proposta para Marca, podedes intentar empregar [https://laravel.com/docs/10.x/eloquent#upserts o método Modelo::updateOrCreate] de Laravel que permite crear ou modificar unha fila da táboa. Podedes intentar crear un único método para as dúas operacións e non facer como en Marca, de ter un para a actualización e outro para a inserción.
 +
===Solución Exercicio proposto===
 +
 +
'''Arquivo app/Models/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="24,45-47" >
 +
<?php
 +
 +
namespace App\Models;
 +
 +
use Illuminate\Database\Eloquent\Model;
 +
use Illuminate\Database\Eloquent\Casts\Attribute;
 +
use Carbon\Carbon;
 +
 +
class Perfume extends Model
 +
{
 +
    protected $table = 'perfumes';  // Indicamos a táboa relacionada con este modelo.
 +
    protected $primaryKey = 'id';    // Indicamos a clave primaria da táboa
 +
 +
    public $timestamps = false;    // Por defecto laravel inclúe dous campos (created_at e updated_at) de tipo datetime cando se fai unha operación de inserción e actualización sobre unha fila. Con isto indicamos que a táboa non ten eses campos.
 +
 +
    protected $casts = [          // Cando accedamos a unha fila empregando este modelo, se poñemos $fila->data_compra teremos un string. Se poñemos o cast, obteremos unha instancia da [https://carbon.nesbot.com/docs/ clase Carbon] de Laravel que nos permite facer operacións e formatear a data.
 +
        'data_compra' => 'date',  // Tamén se pode empregar datetime. Devolve un Carbon
 +
    ];
 +
 +
    protected $fillable =  [                            // Isto xa o veremos. O que indica é que cando facemos unha operación masiva de inserción ou un insert da forma create(datos) soamente deixa enviar en datos (que é un array) as columnas indicadas nesta propiedade (as columnas indicadas son descripcion, prezo e data_compra). Faise por motivos de seguridade.
 +
        'descripcion',
 +
        'prezo',
 +
        'data_compra',
 +
        'marca_id'
 +
    ];
 +
 +
    protected function DataCompra(): Attribute
 +
    {
 +
        return Attribute::make(
 +
            get: fn($value) => $value ? Carbon::parse($value)->format('d/m/Y') : null,  // Cando empregamos a data da forma echo($perfume->data_compra); convirte o obxecto Carbon a unha cadea có formato correcto
 +
            set: fn($value) => $value ? Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d') : null  // Cando empregamos a data da forma: $perfume->data_compra = "10/01/2025" o gardará
 +
        );
 +
    }
 +
 +
  protected function Prezo(): Attribute
 +
    {
 +
        return Attribute::make(
 +
            // Cuando accedemos al valor del campo, convertimos el punto en coma
 +
            get: fn ($value) => number_format($value, 2, ',', '.'),
 +
            // Cuando se asigna el valor, reemplazamos la coma por un punto
 +
            set: fn ($value) => str_replace(',', '.', $value),
 +
        );
 +
    }
 +
 +
    public function marca() {
 +
        return $this->belongsTo(Marca::class);
 +
    }
 +
 +
}
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
'''Arquivo routes.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="11" >
 +
<?php
 +
 +
use Illuminate\Support\Facades\Route;
 +
 +
use App\Http\Controllers\PerfumeController;
 +
use App\Http\Controllers\MarcaController;
 +
use App\Http\Controllers\PerfumeBController;
 +
 +
Route::resource('perfumes', PerfumeController::class);
 +
Route::resource('marcas', MarcaController::class);
 +
Route::get('perfumesB', [PerfumeBController::class, 'index'])->name('perfumesB.index');
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo app/Htpp/Controllers/PerfumeBController.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<?php
 +
 +
namespace App\Http\Controllers;
 +
 +
use Illuminate\Http\Request;
 +
 +
class PerfumeBController extends Controller
 +
{
 +
    public function index()
 +
    {
 +
        return view('perfumes_b.perfumes');
 +
    }
 +
 +
}
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
'''Arquivo resources/views/perfumes_b/perfumes.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
@extends('plantilla');
 +
 +
@section('pageTitle', 'Listado de perfumes con Livewire')
 +
 +
@section('content')
 +
<h2>Listado de Perfumes con Livewire</h2>
 +
 +
 +
@livewire("perfume-listado")
 +
 +
@livewire("perfume")
 +
 +
@include('mensaxes.alertas_javascript')
 +
 +
 +
@endsection
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo app/Livewire/PerfumeListado.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Component;
 +
use App\Models\Perfume;
 +
 +
class PerfumeListado extends Component
 +
{
 +
    public $perfumes;
 +
 +
    public function cargarPerfumes()
 +
    {
 +
        $this->perfumes = Perfume::with('marca')->get();
 +
    }
 +
 +
    public function mount()
 +
    {
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
    public function eliminarPerfume($id)
 +
    {
 +
        $perfume = Perfume::find($id);
 +
 +
        if (!$perfume) {
 +
            $this->addError('error', 'Perfume non atopado.');
 +
            return;
 +
        }
 +
 +
        $perfume->delete();
 +
        session()->flash('message', 'Perfume eliminado correctamente.');
 +
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
    public function render()
 +
    {
 +
        return view('livewire.perfume-listado');
 +
    }
 +
}
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo resources/views/livewire/perfume-listado.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<div>
 +
    <button class="bg-green-500 text-white px-2 py-1 rounded mb-2">Novo perfume</button>
 +
 +
    <table class="table-auto w-full border">
 +
        <thead>
 +
            <tr>
 +
                <th>Descrición</th>
 +
                <th>Prezo</th>
 +
                <th>Data Compra</th>
 +
                <th>Marca</th>
 +
                <th>Accións</th>
 +
            </tr>
 +
        </thead>
 +
        <tbody>
 +
            @foreach($perfumes as $perfume)
 +
            <tr>
 +
                <td>{{ $perfume->descripcion }}</td>
 +
                <td>{{ $perfume->prezo }}</td>
 +
                <td>{{ $perfume->data_compra }}</td>
 +
                <td>{{ $perfume->marca->descripcion ?? 'Sen marca' }}</td>
 +
                <td>
 +
                    <button  class="bg-blue-500 text-white px-2 py-1 rounded">Editar</button>
 +
                    <button wire:click="eliminarPerfume({{ $perfume->id }})"
 +
                        wire:confirm="Estás seguro de borrar la marca {{ $perfume->descripcion }}? "
 +
                        class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 +
            </td>
 +
            </tr>
 +
            @endforeach
 +
        </tbody>
 +
    </table>
 +
 +
    @include('mensaxes.mensaxes')
 +
</div>
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
'''Arquivo app/Livewire/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Component;
 +
use App\Models\Perfume as PerfumeModel;
 +
use App\Models\Marca;
 +
 +
class Perfume extends Component
 +
{
 +
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
 +
    public $modo = 'engadir';
 +
 +
    protected $rules = [
 +
        'descripcion' => 'required|string|min:5|max:45',
 +
        'prezo' => 'required|numeric|min:0|max:999999.99',
 +
        'data_compra' => 'nullable|date|after_or_equal:today',
 +
        'marca_id' => 'nullable|exists:marcas,id',
 +
    ];
 +
 +
 +
    public function mount($id = null)
 +
    {
 +
        $this->id = $id;
 +
 +
        if ($id) {
 +
            $perfume = PerfumeModel::findOrFail($id);
 +
            $this->descripcion = $perfume->descripcion;
 +
            $this->prezo = $perfume->prezo;
 +
            $this->data_compra = $perfume->data_compra;
 +
            $this->marca_id = $perfume->marca_id;
 +
        }
 +
    }
 +
 +
    public function gardarPerfume()
 +
    {
 +
        $this->validate();
 +
 +
        if($this->modo=='engadir')  {
 +
            $this->id = -1;
 +
        }
 +
 +
        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
 +
        // Revisade o código e podedes ver que temos un cast con Date e un set para pasar a data que ven en formato español da caixa de texto a inglés que se vai gardar na base de datos mysql. 
 +
        // Nesta versión a data vaise recoller cun input de tipo 'date' que xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
 +
        $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
 +
        // Crear o actualizar el perfume
 +
        $perfumeCreado = PerfumeModel::updateOrCreate(
 +
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
 +
            [
 +
                'descripcion' => $this->descripcion,
 +
                'prezo' => $this->prezo,
 +
                'data_compra' => $this->data_compra,
 +
                'marca_id' => $this->marca_id
 +
            ]
 +
        );
 +
 +
        // Si se ha creado un nuevo perfume, asignar el ID
 +
        if (!$this->id) {
 +
            $this->id = $perfumeCreado->id;
 +
        }
 +
 +
        // Mensaje de éxito
 +
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");
 +
 +
        // Resetear los campos
 +
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
 +
 +
        // Redirigir a la lista de perfumes
 +
        return redirect()->route('perfumesB.index');
 +
    }
 +
 +
    public function render()
 +
    {
 +
        $marcas = Marca::orderBy('descripcion')->get();
 +
        return view('livewire.perfume',compact('marcas'));
 +
    }
 +
}
 +
 +
 +
</syntaxhighlight>
 +
 +
'''Arquivo resources/views/livewire/perfume.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<div class="p-4 border rounded bg-gray-100">
 +
    <form wire:submit.prevent="gardarPerfume">
 +
 +
        <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>
 +
 +
        <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
 +
        @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
 +
        @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <input type="date" wire:model="data_compra" class="block mb-2 w-full">
 +
        @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <select wire:model="marca_id" class="block mb-2 w-full">
 +
            <option value="">-- Selecciona unha marca --</option>
 +
            @foreach ($marcas as $marca)
 +
                <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
 +
            @endforeach
 +
        </select>
 +
        @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror
 +
 +
        <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
 +
            onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
 +
        <input type="reset" name="btnBorrar" value="Limpar" />
 +
 +
    </form>
 +
    @include('mensaxes.mensaxes')
 +
</div>
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo resources/views/plantilla.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="25" >
 +
<!DOCTYPE html>
 +
<html>
 +
    <head>
 +
        <title>@yield('pageTitle')</title>
 +
 +
        <style>
 +
            .menu{
 +
                float: left;
 +
                width: 30%;
 +
                padding-left: 5px;
 +
            }
 +
            .menu a{
 +
                display: block;
 +
            }
 +
            .contido{
 +
                float: left;
 +
                width: 65%;
 +
            }
 +
 +
        </style>
 +
    </head>
 +
    <body>
 +
        <div class="menu">
 +
            <a href='{{route('perfumes.index')}}'>Operacións Perfumes</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
 +
            <a href='{{route('perfumesB.index')}}'>Operacións Perfumes Livewire</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
 +
            <a href='{{route('marcas.index')}}'>Operacións Marcas</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
 +
        </div>
 +
        <div class="contido">
 +
            @yield('content')
 +
        </div>
 +
    </body>
 +
</html>
 +
 +
</syntaxhighlight>
 +
 +
'''Arquivo app/Models/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
</syntaxhighlight>
 +
 +
<br />
 +
 +
==Eventos entre compoñentes==
 +
 +
* Máis información en [https://livewire.laravel.com/docs/events https://livewire.laravel.com/docs/events]
 +
: Imos seguir co exemplo anterior de perfumes e imos solucionar as funcionalidades que nos quedan.
 +
 +
 +
* Os eventos poden ir dende un compoñente livewire a outro ou dende un compoñente livewire a un script dunha plantilla blade.
 +
 +
 +
* Imos aplicar isto á nosa pantalla de Perfumes.
 +
: Se vos fixades, cando creamos un novo perfume no compoñente livewire Perfume, recargamos a páxina:
 +
'''Arquivo app/Livewire/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
    ............
 +
    public function gardarPerfume()
 +
    {
 +
        $this->validate();
 +
        ............
 +
 +
        return redirect()->route('perfumesB.index');
 +
    }
 +
 +
</syntaxhighlight>
 +
 +
: O que imos facer é enviar un evento a todos os compoñentes livewire que estean cargados na páxina e que estén escoitando dito evento.
 +
: No noso caso:
 +
'''Arquivo app/Livewire/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="7" >
 +
    ............
 +
    public function gardarPerfume()
 +
    {
 +
        $this->validate();
 +
        ............
 +
 +
        $this->dispatch('refrescar-listado-perfumes');
 +
    }
 +
 +
</syntaxhighlight>
 +
 +
: O nome pode ser calquera. Agora temos que facer que o compoñente livewire de listado escoite dito evento e asocialo a un método.
 +
'''Arquivo app/Livewire/PerfumeListado.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="6,13-16" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Component;
 +
use Livewire\Attributes\On;
 +
 +
use App\Models\Perfume;
 +
 +
class PerfumeListado extends Component
 +
{
 +
  .................
 +
    #[On('refrescar-listado-perfumes')]
 +
    public function refrescarListado(){
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
</syntaxhighlight>
 +
 +
: O nome da liña On debe ser o mesmo que empregamos cando fixemos o dispatch.
 +
: Agora se probades a dar de alta un novo perfume se actualizará o listado pero no se carga a páxina novamente.
 +
 +
 +
 +
* Agora imos a facer que cando premamos no botón de alta fagamos unha alta no compoñente livewire de perfume e o mesmo na modificación.
 +
: Para iso necesitamos crear un método no controlador de PerfumeListado que sexa chamado polos botóns de alta e modificar enviando no caso de modificar o id do perfume.
 +
: Despois o que fará este método é lanzar un evento para que sexa capturado polo compoñente livewire Perfume e modificar a propiedade modo e cargar os datos do perfume en caso de modificación.
 +
 +
'''Arquivo resources/views/livewire/perfume-listado.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="2,22" >
 +
<div>
 +
    <button wire:click="operacionPerfume" class="bg-green-500 text-white px-2 py-1 rounded mb-2">Novo perfume</button>
 +
 +
    <table class="table-auto w-full border">
 +
        <thead>
 +
            <tr>
 +
                <th>Descrición</th>
 +
                <th>Prezo</th>
 +
                <th>Data Compra</th>
 +
                <th>Marca</th>
 +
                <th>Accións</th>
 +
            </tr>
 +
        </thead>
 +
        <tbody>
 +
            @foreach($perfumes as $perfume)
 +
            <tr>
 +
                <td>{{ $perfume->descripcion }}</td>
 +
                <td>{{ $perfume->prezo }}</td>
 +
                <td>{{ $perfume->data_compra }}</td>
 +
                <td>{{ $perfume->marca->descripcion ?? 'Sen marca' }}</td>
 +
                <td>
 +
                    <button wire:click="operacionPerfume({{$perfume->id}})" class="bg-blue-500 text-white px-2 py-1 rounded">Editar</button>
 +
                    <button wire:click="eliminarPerfume({{ $perfume->id }})"
 +
                        wire:confirm="Estás seguro de borrar la marca {{ $perfume->descripcion }}? "
 +
                        class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
 +
            </td>
 +
            </tr>
 +
            @endforeach
 +
        </tbody>
 +
    </table>
 +
 +
    @include('mensaxes.mensaxes')
 +
</div>
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
'''Arquivo app/Livewire/PerfumeListado.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="6,25-27" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Component;
 +
use Livewire\Attributes\On;
 +
use Illuminate\Support\Facades\Log;
 +
 +
use App\Models\Perfume;
 +
 +
class PerfumeListado extends Component
 +
{
 +
    public $perfumes;
 +
 +
    public function cargarPerfumes()
 +
    {
 +
        $this->perfumes = Perfume::with('marca')->get();
 +
    }
 +
 +
    public function mount()
 +
    {
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
    public function operacionPerfume($id=null){
 +
        $this->dispatch('operar-perfume',id:$id); // Chamamos ao método e enviamos o id do perfume. null => alta, numero => modificar
 +
    }
 +
 +
    #[On('refrescar-listado-perfumes')]
 +
    public function refrescarListado(){
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
    public function eliminarPerfume($id)
 +
    {
 +
        $perfume = Perfume::find($id);
 +
 +
        if (!$perfume) {
 +
            $this->addError('error', 'Perfume non atopado.');
 +
            return;
 +
        }
 +
 +
        $perfume->delete();
 +
        session()->flash('message', 'Perfume eliminado correctamente.');
 +
 +
        $this->cargarPerfumes();
 +
    }
 +
 +
    public function render()
 +
    {
 +
        return view('livewire.perfume-listado');
 +
    }
 +
}
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo app/Livewire/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="5,36-55" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Attributes\On;
 +
use Livewire\Component;
 +
 +
use App\Models\Perfume as PerfumeModel;
 +
use App\Models\Marca;
 +
 +
class Perfume extends Component
 +
{
 +
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
 +
    public $modo = 'engadir';
 +
 +
    protected $rules = [
 +
        'descripcion' => 'required|string|min:5|max:45',
 +
        'prezo' => 'required|numeric|min:0|max:999999.99',
 +
        'data_compra' => 'nullable|date|after_or_equal:today',
 +
        'marca_id' => 'nullable|exists:marcas,id',
 +
    ];
 +
 +
 +
    public function mount($id = null)
 +
    {
 +
        $this->id = $id;
 +
 +
        if ($id) {
 +
            $perfume = PerfumeModel::findOrFail($id);
 +
            $this->descripcion = $perfume->descripcion;
 +
            $this->prezo = $perfume->prezo;
 +
            $this->data_compra = $perfume->data_compra;
 +
            $this->marca_id = $perfume->marca_id;
 +
        }
 +
    }
 +
    #[On('operar-perfume')]
 +
    public function operarPerfume($id){
 +
 +
        if($id==null){
 +
            $this->modo = "engadir";
 +
            $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
 +
            $this->id = -1;
 +
        }
 +
        else{
 +
            $this->id = $id;
 +
            $perfume = PerfumeModel::findOrFail($id);
 +
            $this->descripcion = $perfume->descripcion;
 +
            $this->prezo = str_replace(',','.',$perfume->prezo);    // Prezo ten un mutator para poñer coma en vez de punto
 +
            if($this->data_compra){
 +
                $this->data_compra = \Carbon\Carbon::createFromFormat('d/m/Y', $perfume->data_compra)->format('Y-m-d');
 +
            }
 +
            $this->marca_id = $perfume->marca_id;
 +
            $this->modo ="actualizar";
 +
        }
 +
    }
 +
 +
    public function gardarPerfume()
 +
    {
 +
        $this->validate();
 +
 +
        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
 +
        // Nesta versión a data xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
 +
        if($this->data_compra) {
 +
          $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
 +
        }
 +
 +
        // Crear o actualizar el perfume
 +
        $perfumeCreado = PerfumeModel::updateOrCreate(
 +
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
 +
            [
 +
                'descripcion' => $this->descripcion,
 +
                'prezo' => $this->prezo,
 +
                'data_compra' => $this->data_compra,
 +
                'marca_id' => $this->marca_id
 +
            ]
 +
        );
 +
 +
        // Si se ha creado un nuevo perfume, asignar el ID
 +
        if (!$this->id) {
 +
            $this->id = $perfumeCreado->id;
 +
        }
 +
 +
        // Mensaje de éxito
 +
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");
 +
 +
        // Resetear los campos
 +
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
 +
 +
        // Redirigir a la lista de perfumes
 +
        //return redirect()->route('perfumesB.index');
 +
        $this->dispatch('refrescar-listado-perfumes');
 +
    }
 +
 +
    public function render()
 +
    {
 +
        $marcas = Marca::orderBy('descripcion')->get();
 +
        return view('livewire.perfume',compact('marcas'));
 +
    }
 +
}
 +
 +
 +
</syntaxhighlight>
 +
 +
 +
'''Arquivo resources/views/livewire/perfume.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
<div class="p-4 border rounded bg-gray-100">
 +
    <form wire:submit.prevent="gardarPerfume">
 +
 +
        <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>
 +
 +
        <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
 +
        @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
 +
        @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <input type="date" wire:model="data_compra" class="block mb-2 w-full">
 +
        @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
 +
        <select wire:model="marca_id" class="block mb-2 w-full">
 +
            <option value="">-- Selecciona unha marca --</option>
 +
            @foreach ($marcas as $marca)
 +
                <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
 +
            @endforeach
 +
        </select>
 +
        @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror
 +
 +
        <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
 +
            onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
 +
        <input type="reset" name="btnBorrar" value="Limpar" />
 +
 +
    </form>
 +
    @include('mensaxes.mensaxes')
 +
</div>
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
 +
 +
* Agora soamente queda por facer que a alta/modificación sexa invisible ata que se pulse o botón correspondente.
 +
: Iso o podemos facer na vista mirando que o modo sexa engadir/actualizar en en caso de que non o sexa, non amosar nada (algo hai que amosar, polo que envolvemos todo con un div)
 +
'''Arquivo resources/views/livewire/perfume.blade.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="2" >
 +
<div>
 +
@if($modo=='engadir' || $modo=='actualizar')
 +
    <div class="p-4 border rounded bg-gray-100">
 +
        <form wire:submit.prevent="gardarPerfume">
 +
 +
            <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>
 +
 +
            <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
 +
            @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
 +
            <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
 +
            @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
 +
            <input type="date" wire:model="data_compra" class="block mb-2 w-full">
 +
            @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
 +
            <select wire:model="marca_id" class="block mb-2 w-full">
 +
                <option value="">-- Selecciona unha marca --</option>
 +
                @foreach ($marcas as $marca)
 +
                    <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
 +
                @endforeach
 +
            </select>
 +
            @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror
 +
 +
            <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
 +
                onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
 +
            <input type="reset" name="btnBorrar" value="Limpar" />
 +
 +
        </form>
 +
        @include('mensaxes.mensaxes')
 +
    </div>
 +
@endif
 +
</div>
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
* Agora no controlador inicializamos o modo a "" e o cambiamos en función da operación a realizar.
 +
'''Arquivo app/Livewire/Perfume.php'''
 +
::<syntaxhighlight lang="java" enclose="div" highlight="14,40,53,88" >
 +
<?php
 +
 +
namespace App\Livewire;
 +
 +
use Livewire\Attributes\On;
 +
use Livewire\Component;
 +
 +
use App\Models\Perfume as PerfumeModel;
 +
use App\Models\Marca;
 +
 +
class Perfume extends Component
 +
{
 +
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
 +
    public $modo="";
 +
 +
    protected $rules = [
 +
        'descripcion' => 'required|string|min:5|max:45',
 +
        'prezo' => 'required|numeric|min:0|max:999999.99',
 +
        'data_compra' => 'nullable|date|after_or_equal:today',
 +
        'marca_id' => 'nullable|exists:marcas,id',
 +
    ];
 +
 +
 +
    public function mount($id = null)
 +
    {
 +
        $this->id = $id;
 +
 +
        if ($id) {
 +
            $perfume = PerfumeModel::findOrFail($id);
 +
            $this->descripcion = $perfume->descripcion;
 +
            $this->prezo = $perfume->prezo;
 +
            $this->data_compra = $perfume->data_compra;
 +
            $this->marca_id = $perfume->marca_id;
 +
        }
 +
    }
 +
    #[On('operar-perfume')]
 +
    public function operarPerfume($id){
 +
 +
        if($id==null){
 +
            $this->modo = "engadir";
 +
            $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
 +
            $this->id = -1;
 +
        }
 +
        else{
 +
            $this->id = $id;
 +
            $perfume = PerfumeModel::findOrFail($id);
 +
            $this->descripcion = $perfume->descripcion;
 +
            $this->prezo = str_replace(',','.',$perfume->prezo);    // Prezo ten un mutator para poñer coma en vez de punto
 +
            if($this->data_compra){
 +
                $this->data_compra = \Carbon\Carbon::createFromFormat('d/m/Y', $perfume->data_compra)->format('Y-m-d');
 +
            }
 +
            $this->marca_id = $perfume->marca_id;
 +
            $this->modo ="actualizar";
 +
        }
 +
    }
 +
 +
    public function gardarPerfume()
 +
    {
 +
        $this->validate();
 +
 +
        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
 +
        // Nesta versión a data xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
 +
        if($this->data_compra) {
 +
          $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
 +
        }
 +
        // Crear o actualizar el perfume
 +
        $perfumeCreado = PerfumeModel::updateOrCreate(
 +
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
 +
            [
 +
                'descripcion' => $this->descripcion,
 +
                'prezo' => $this->prezo,
 +
                'data_compra' => $this->data_compra,
 +
                'marca_id' => $this->marca_id
 +
            ]
 +
        );
 +
 +
        // Si se ha creado un nuevo perfume, asignar el ID
 +
        if (!$this->id) {
 +
            $this->id = $perfumeCreado->id;
 +
        }
 +
 +
        // Mensaje de éxito
 +
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");
 +
 +
        // Resetear los campos
 +
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
 +
 +
        $this->modo="";
 +
        // Redirigir a la lista de perfumes
 +
        //return redirect()->route('perfumesB.index');
 +
        $this->dispatch('refrescar-listado-perfumes');
 +
    }
 +
 +
    public function render()
 +
    {
 +
        $marcas = Marca::orderBy('descripcion')->get();
 +
        return view('livewire.perfume',compact('marcas'));
 +
    }
 +
}
 +
 +
 +
</syntaxhighlight>
 +
 +
 +
 +
 +
<br />
 +
* Neste caso non fixo falta, pero cando queremos refrescar o compoñente perfumes-listado, pode ser que en vez de gardar os datos nunha propiedade local ($perfumes), podemos envialos localmente no render, da forma:
 +
::<syntaxhighlight lang="java" enclose="div" highlight="" >
 +
 +
    public function render()
 +
    {
 +
        $perfumes = Perfume::with('marca')->get();
 +
        return view('livewire.perfume-listado',compact('perfumes');
 +
    }
 +
</syntaxhighlight>
 +
 +
: '''<u>NOTA IMPORTANTE:</u>''' Para que funcione desta forma tedes que eliminar a propiedade $perfumes do compoñente e xa non necesitades o mount nin o método cargarPerfumes.
 +
 +
: Nese caso, se queremos 'refrescar' o compoñente para que volva a facer o render, teríamos que poñer: <b>this->dispatch('render')</b> no método 'refrescar-perfumes'.
 +
 +
::<syntaxhighlight lang="java" enclose="div" highlight="3" >
 +
    #[On('refrescar-listado-perfumes')]
 +
    public function refrescarListado(){
 +
        $this->dispatch('refrescar');
 +
    }
 
</syntaxhighlight>
 
</syntaxhighlight>
  
−
==Funcionamento==
 
  
  

Revisión actual del 17:27 7 sep 2026

Introducción

  • Livewire permite o uso de compoñentes integrados en Laravel nos que se establece unha comunicación bidireccional (podemos enviar información desde o cliente (HTML) ao servidor (controlador) e viceversa sen necesidade de recargar completamente a páxina).
É o mesmo concepto que se empregaba en AJAX e que agora se usa con FETCH, pero integrado no framework de Laravel.



  • IMPORTANTE: Cando vexades a documentación mirade que version estades a consultar xa que dende a versión 2 á versión 3 houbo cambios.
Unha vez instalado en Laravel podedes consultar a versión instalada buscando a entrada livewire/livewire no arquivo composor.json no raíz do proxecto.
Polo de agora (versión 10-12) estamos a traballar coa versión 3.x de Livewire.


Instalación

  • No directorio raíz do proxecto Laravel escribe: composer require livewire/livewire
ATENCIÓN: A día de hoxe (2026) saiu a versión 4.0 de Livewire. No exame imos empregar a 4.0.
 : Polo tanto empregar esta orden: composer require livewire/livewire "^4.0" para descargar a versión 4.0 (non poñer sudo)
Unha vez instalado en Laravel podedes consultar a versión instalada buscando a entrada livewire/livewire no arquivo composer.json no raíz do proxecto.
Polo de agora coa versión 13 de Laravel imos empregar a versión 4.0 de Livewire.



Validación de campos

  • Creamos en mysql (se non o tema xa de exercicios anteriores) unha base de datos Mysql de nome perfumes.
Lembrar que o usuario configurado en Laravel para acceder á base de datos debería ter os permisos mínimos para que a aplicación funcione.
  • Imos crear a táboa marcas e imos crear unha relación entre perfumes e marcas.
A nivel de mysql:
CREATE TABLE perfumes.marcas (
  `id` int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  `descripcion` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;




INSERT INTO perfumes.marcas (`id`, `descripcion`) VALUES
(1, 'Descripción actualizada da marca con id=1'),
(2, 'ADIDAS'),
(3, 'ARAMIS');


Táboa perfumes
CREATE TABLE `perfumes.perfumes` (
  `id` int NOT NULL AUTO_INCREMENT,
  `descripcion` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL,
  `prezo` decimal(8,2) NOT NULL,
  `data_compra` date DEFAULT NULL,
  `marca_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_PERFUMES_1_idx` (`id`),
  KEY `fk_PERFUMES_MARCA_idx` (`marca_id`),
  CONSTRAINT `fk_perfumes_1` FOREIGN KEY (`marca_id`) REFERENCES `marcas` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;


INSERT INTO perfumes.perfumes (`id`, `descripcion`, `prezo`, `data_compra`, `marca_id`) VALUES
(18, 'Best Aramis', '54.32', '2024-01-18', 2),
(19, 'Novo perfume da marca con id 1', '100.22', '2024-01-22', 1);



  • Creamos o modelo, ruta, controlador e polo de agora imos facer a vista para crear unha nova marca.
Lembrar que esta parte ven ser a continuación do feito na UD6 - Modelo Vista Controlador de Laralve.
Dende o terminal escribimos: php artisan make:model Marca --resource


Arquivo: app/models/Marca.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Models\Perfume;

class Marca extends Model
{
    public $timestamps = false;

    protected $fillable =   [
        'descripcion',

    ];

    public function perfumes(){
        return $this->hasMany(Perfume::class, 'marca_id','id');     // Non necesitamos poñer xa que 'marca_id','id' cumpren as normas de nomeamento
    }

}


Arquivo: app/models/Perfume.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Carbon\Carbon;

class Perfume extends Model
{
    protected $table = 'perfumes';  // Indicamos a táboa relacionada con este modelo.
    protected $primaryKey = 'id';    // Indicamos a clave primaria da táboa

    public $timestamps = false;    // Por defecto laravel inclúe dous campos (created_at e updated_at) de tipo datetime cando se fai unha operación de inserción e actualización sobre unha fila. Con isto indicamos que a táboa non ten eses campos.

    protected $casts = [           // Cando accedamos a unha fila empregando este modelo, se poñemos $fila->data_compra teremos un string. Se poñemos o cast, obteremos unha instancia da [https://carbon.nesbot.com/docs/ clase Carbon] de Laravel que nos permite facer operacións e formatear a data.
        'data_compra' => 'date',   // Tamén se pode empregar datetime. Devolve un Carbon
    ];

    protected $fillable =   [                            // Isto xa o veremos. O que indica é que cando facemos unha operación masiva de inserción ou un insert da forma create(datos) soamente deixa enviar en datos (que é un array) as columnas indicadas nesta propiedade (as columnas indicadas son descripcion, prezo e data_compra). Faise por motivos de seguridade.
        'descripcion',
        'prezo',
        'data_compra'
    ];

    protected function DataCompra(): Attribute
    {
        return Attribute::make(
            get: fn($value) => $value ? Carbon::parse($value)->format('d/m/Y') : null,   // Cando empregamos a data da forma echo($perfume->data_compra); convirte o obxecto Carbon a unha cadea có formato correcto
            set: fn($value) => $value ? Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d') : null   // Cando empregamos a data da forma: $perfume->data_compra = "10/01/2025" o gardará
        );
    }

   protected function Prezo(): Attribute
    {
        return Attribute::make(
            // Cuando accedemos al valor del campo, convertimos el punto en coma
            get: fn ($value) => number_format($value, 2, ',', '.'),
            // Cuando se asigna el valor, reemplazamos la coma por un punto
            set: fn ($value) => str_replace(',', '.', $value),
        );
    }

}


Arquivo: routes/web.php
<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\PerfumeController;
use App\Http\Controllers\MarcaController;

Route::resource('perfumes', PerfumeController::class);
Route::resource('marcas', MarcaController::class);


O arquivo de MarcaController xa se crearia con todo o código necesario para a xestión.


Arquivo resources/views/marcas/form_alta_marcas.blade.php (creado o cartafol marcas dentro de view)
@extends('plantilla');

@section('pageTitle', 'Alta de marcas')

@section('content')
<h2>Alta de Marcas</h2>



@if (session()->has('idPerfume'))
    <h4>Creado un novo rexistro co id: {{session('idPerfume')}}</h4>
@endif



@include('mensaxes.alertas_javascript')


@endsection




Creamos o compoñente Livewire con esta orde: php artisan make:livewire marca
Isto creará dous arquivos en Laravel:
  • CLASS: app/Livewire/Marca.php => Ven a ser o controlador que vimos ata o de agora en Laravel.
  • VIEW: resources/views/livewire/marca.blade.php => Ven a ser a vista que vai visualizar Laravel no cliente.


Arquivo: app/Livewire/Marca.php
<?php

namespace App\Livewire;

use Livewire\Component;

class Marca extends Component
{
    public function render()
    {
        return view('livewire.marca');
    }
}
Básicamente indicamos en render a vista que vai enviar ao cliente.


Arquivo resource/views/Livewire/marca.blade.php
<form method="post" action="{{route('marcas.store')}}" name="formulario_marcas">

    @csrf
    <div>
            Descripción:<input type="text" maxlength="45" size="45" required name="descripcion" />
    </div>
    <div>
        <input type="submit" name="btnAlta" value="Alta" onClick="return confirm('Estás seguro de dar de alta a nova marca ');"/>
        <input type="reset" name="btnBorrar" value="Limpar" />

    </div>


</form>
Nota: Non separamos os campos nun arquivo diferente xa que en livewire as operacións de alta e modificación vanse facer sobre a mesma vista.


  • Agora para incluir o compoñente Livewire nunha view de Laravel temos que poñer:
Arquivo resouces/views/marcas/form_alta_marca.blade.php
@extends('plantilla');

@section('pageTitle', 'Alta de Marcas')

@section('content')
<h2>Alta de novos perfumes</h2>


<livewire:marca />


................
  • Nota: Outra forma de añadir o livewire en la vista es: @livewire(Marca:class) o también @livewire('marca')



  • Se agora poñemosm na url: http://localhost:8080/marcas/create veremos que aparecerá unha vista tal cual aparecía cando fixemos o de perfume, pero a diferenza de perfumes, esta vista (os campos e form) se xestionan cun compoñente Livewire.


  • Agora imos ver os cambios que temos que facer para xestionar os campos do formulario.
Primero imos definir no controlador do compoñente livewire os campos que imos xestionar. No noso caso está a descripcion.
<?php

namespace App\Livewire;

use Livewire\Component;

class Marca extends Component
{
    public $descripcion = "Valor inicial";   // O valor inicial non é necesasrio. Soamente para que vexades como dito valor se carga na vista.

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


Modificamos o campo descripcion.
Arquivo: resources/views/livewire/marca.blade.php
<div>
    Descripción:<input type="text" maxlength="45" required size="45" wire:model="descripcion" name="descripcion" />
</div>
  • wire:model="descripcion": O que estamos a dicir con isto é que este input está vinculado á propiedade $descripcion definida no compoñente Livewire. Calquera modificación realizada polo usuario no cliente (na páxina web), cando se faga unha chamada ao controlador livewire, actualizará automaticamente o valor da propiedade no servidor grazas á reactividade proporcionada por Livewire. Ademais, se a propiedade no servidor cambia, ese cambio tamén se reflectirá no cliente.
Se vos dades conta non temos a propiedade value como tiñamos en perfumes.
Se agora cargades a páxina veredes como o valor inicial se carga no input.



  • Agora imos a facer a validación no servidor. Para iso temos que definir unha propiedade $rules coas regras de validación que xa empregamos en Laravel.
Ademáis imos crear un método ao que vai chamar o formulario cando faga submit e que fará a validación dos campos. No exemplo imos chamar ao método gardarMarca.
Arquivo: app/Livewire/Marca.php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Log;

use Livewire\Component;

class Marca extends Component
{
    public $descripcion = "Valor inicial";

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
    ];

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

    public function gardarMarca(){
        $this->validate();

        Log::info('Pasa a validación');
    }
}
Para depurar o funcionamento dos compoñente livewire podemos empregar o facade Log::info e ver as mensaxes no arquivo /storage/logs/laravel.log (podedes borrar o contido antes de executar para ter todo limpo ao principio).


  • Para chamar a ese método temos que modificar a vista do compoñente livewire.
Arquivo: resources/views/livewire/marca.blade.php
<form wire:submit.prevent="gardarMarca">

    @csrf
    <div>
       Descripción:<input type="text" maxlength="45" size="45" required
              wire:model="descripcion" name="descripcion" />
    </div>
    <div>
        <input type="submit" name="btnAlta" value="Alta" onClick="return confirm('Estás seguro de dar de alta a nova marca ');"/>
        <input type="reset" name="btnBorrar" value="Limpar" />

    </div>


</form>
Con isto wire:submit.prevent="gardarMarca" estamos a decir que cando faga o submit chame de forma asíncrona ao controlador do compoñente livewire ao método gardarMarca.
No caso de manexar formularios en livewire, empregar sempre prevent. Poderíamos facelo con wire:submit pero nese caso a páxina enteira se carga como se fora un submit 'tradicional'.


Nota: Podemos asociar calquera click de botón directamente ao un método no controlador do livewire, da forma: <button wire:click="gardarMarca">+</button>


  • Tamén imos visualizar os erros de validación que poida enviar o controlador Livewire se non pasa a validación. Para iso temos que empregar a directiva @error sobre cada un dos campos que teñen validación. No exemplo se coloca ao lado do input, pero poderían ir en calquera parte.
Arquivo: resources/views/livewire/marca.blade.php
<div>
    Descripción:<input type="text" maxlength="45" required size="45" wire:model="descripcion" name="descripcion" />  
    @error('descripcion') <span class="error">{{ $message }}</span> @enderror
</div>


  • Se queremos personalizar as mensaxes de erro:
Arquivo: app/Livewire/Marca.php
<?php
...........

    protected $messages = [
        'descripcion.required' => 'La descripción es obligatoria.',
        'descripcion.string' => 'La descripción debe ser una cadena de texto.',
        'descripcion.min' => 'La descripción debe tener al menos 5 caracteres.',
        'descripcion.max' => 'La descripción no puede tener más de 45 caracteres.',
    ];
   ..............



  • Fixerase que neste caso a validación faise cando o usuario preme o botón de submit.
Se queremos que se faga a validación cando o usuario cambio o contido da caixa de texto temos que:
  • Empregar o método update($propiedade,$valor) que será chamado de forma automáticamente cando o campo teña unha modificación. Dito método fará a validación da propiedade. Temos que chamar ao método validatyOnly.
  • Cambiar a propiedade wire:model da caixa de texto por wire:model.live
Lembrar que estas chamadas fanse ao servidor polo que pode ser que non nos interese para aforrar recursos e comunicacións entre cliente e o servidor.


Arquivo: resources/views/livewire/marca.blade.php
MOI IMPORTANTE: Tedes que cambiar a propiedade wire:model por wire:model.live
<div>
    Descripción:<input type="text" maxlength="45" size="45" required
        wire:model.live="descripcion" name="descripcion" />
    @error('descripcion') <span class="error">{{ $message }}</span> @enderror
</div>
Arquivo: app/Livewire/Marca.php
    ..............
    public function updated($propiedade,$valor){
        $this->validateOnly($propiedade);
    }
    .............


Ao facer isto, cada vez que se preme no input fará a validación.
Lembrar que poñer isto non quita que teñades a chamada $this->validate() cando se vaia a dar de alta/modificar os datos



  • Mirade a documentación xa que a validación tamén se pode definir en cada un dos campos en vez de empregar a propiedade $rules: https://livewire.laravel.com/docs/validation (escoller versión 3 na parte esquerda da páxina)


Operacións de alta-baixa-modificación-listado

Alta

  • A alta faise como sempre pero agora está no controlador do compoñente livewire.
Hai un pequeno detalle xa que como chamamos Alta ao compoñente Livewire, se empregamos o modelo Marca teríamos o mesmo nome. Por iso empregaremos un alias para o modelo.


Arquivo: app/Livewire/Marca.php
   use App\Models\Marca as MarcaModel;

    .............
    public function gardarMarca(){
        $this->validate();

        Log::info('Pasa a validación');

        $marcaCreada = MarcaModel::create([
            'descripcion' => $this->descripcion
        ]);

        session()->flash('message', "Marca creada correctamente con id $marcaCreada->id");
        $this->reset(['descripcion']);   // Borra os datos da caixa de texto.

    }


Fixarse que para enviar unha información á vista empregamos session()->flash que vai gardar nunha variable de sesión (message) o texto a amosar.
Dita variable non pode ser empregada pola páxina, soamente polo compoñente livewire xa que non se recarga a páxina enteira.
A chamada a reset vai poñer en branco os valor da variable $descripcion do compoñente. Pero como temos un valor por defecto na definición poñerá dito valor. Lembrar que non é obrigatorio e podemos quitalo.
Tamén indicar que podemos facer o create da forma: MarcaModel::create($this->all())



  • Neste caso quedaríamos na pantalla de alta pero dita pantalla non se refresca. Soamente refresca o compoñente livewire.


  • Para amosar as mensaxes de que todo foi correcto, imos crear un arquivo na vista.
Tamén imos poñer nese arquivo os posibles erros que enviemos dende o controlador.
Arquivo app/views/mensaxes/mensaxes.blade.php. Lembrar crear o cartafol mensaxes.
@if (session()->has('message'))
    <div class="alert alert-success">
        {{ session('message') }}
    </div>
@endif
Simplemente amosamos o contido da variable de sesión message.


  • Incluimos dito arquivo na vista do compoñente livewire:
Arquivo app/views/livewire/marca.blade.php.
<div>
    <form wire:submit.prevent="gardarMarca">

        @csrf
        <div>
          Descripción:<input type="text" maxlength="45" size="45" required
               wire:model.live="descripcion" name="descripcion" />
          @error('descripcion') <span class="error">{{ $message }}</span> @enderror
        </div>
        <div>
            <input type="submit" name="btnAlta" value="Alta" onClick="return confirm('Estás seguro de dar de alta a nova marca ');"/>
            <input type="reset" name="btnBorrar" value="Limpar" />
        </div>


    </form>

    @include('mensaxes.mensaxes')
</div>
IMPORTANTE: O compoñente livewire soamente pode ter un elemento raíz, no exemplo posto
. Non podedes poñer o include fora do div.


  • E que pasa se queremos ir a outra pantalla ou que recargue toda a páxina e non soamente o compoñente livewire ? Debemos empregar o método redirectRoute() poñendo o nome da ruta á que queremos ir.
Arquivo: app/Livewire/Marca.php
    ...................
    public function gardarMarca()
    {
        $this->validate();

        Log::info('Pasa a validación');

        $marcaCreada = MarcaModel::create([
            'descripcion' => $this->descripcion
        ]);

        session()->flash('message', "Marca creada correctamente con id $marcaCreada->id");
        $this->reset(['descripcion']);

        return $this->redirectRoute('marcas.create');
    }



Modificar

  • Agora, ao engadir unha nova operación temos un problema.
Se non empregáramos livewire teríamos dúas vistas diferentes, unha para alta e outra para modificación, pero ao estar nun compoñente livewire temos que engadir a lóxica para que a vista faga unha cousa ou outra.


  • Creamos a vista en Laravel para actualizar a marca:
Arquivo resources/views/marcas/form_modificar_marcas.blade.php
@extends('plantilla');

@section('pageTitle', 'Modificar Marcas')

@section('content')
<h2>Modificar Marcas</h2>


<livewire:marca modo="actualizar" :id="$marca->id"/>    <!-- Se queres enviar unha cadea  ou numero poñemos clave="valor". Os dous puntos (:id) é para enviar unha variable de Laravel -->

@include('mensaxes.alertas_javascript')


@endsection
Fixarse como estamos a pasar o id da marca que queremos modificar ao compoñente Livewire. Dita marca ven do controlador de Laravel (posto a continuación)


  • Cargamos dita vista no controlador de Laravel no método edit.
Arquivo app/http/Controllers/MarcaController.php
    public function edit(Marca $marca)
    {
        return view('marcas.form_modificar_marcas', compact('marca'));
    }


  • Agora temos que modificar o controlador do compoñente livewire
Polo tanto imos engadir ao controlador:
  • Unha variable que vai indicar se estamos en modo edición ou en modo alta. En función desa variable a vista amosará un texto diferente no botón e fará un actión diferente.
  • Unha variable $id que vai gardar o id da marca que imos querer modificar. Dita id será o que enviemos dende a vista de Laravel cando premamos na modificación.
Tamén teremos un método actualizarMarca() que actualizará os campos necesarios (no noso exemplo descripcion) cando o usuario preme no botón modificar da vista do compoñente livewire.
Arquivo app/http/livewire/Marcas.php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Log;

use Livewire\Attributes\Validate;
use App\Models\Marca as MarcaModel;

use Livewire\Component;

class Marca extends Component
{
    public $descripcion = "Valor inicial";
    public $id; // A empregamos na operación de modificación/baixa para atopar a marca a modificar ou borrar
    public $modo = "engadir";

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
    ];

    protected $messages = [
        'descripcion.required' => 'La descripción es obligatoria.',
        'descripcion.string' => 'La descripción debe ser una cadena de texto.',
        'descripcion.min' => 'La descripción debe tener al menos 5 caracteres.',
        'descripcion.max' => 'La descripción no puede tener más de 45 caracteres.',
    ];

    public function mount($id = null)
    {
        $this->id = $id;
        if($id) {
            $marca = MarcaModel::findOrFail($id);
            $this->descripcion = $marca->descripcion;
        }
    }

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

    public function gardarMarca()
    {
        $this->validate();

        Log::info('Pasa a validación');

        $marcaCreada = MarcaModel::create([
            'descripcion' => $this->descripcion
        ]);

        $this->id = $marcaCreada->id;

        session()->flash('message', "Marca creada correctamente con id $marcaCreada->id");
        $this->reset(['descripcion']);

        return $this->redirectRoute('marcas.create');
    }

    public function actualizarMarca()
    {
        $this->validate();

        $marca = MarcaModel::findOrFail($this->id);
        $marca->update([
            'descripcion' => $this->descripcion,
        ]);

        session()->flash('message', 'Marca actualizada correctamente.');
    }

    public function updated($propiedade,$valor){
        $this->validateOnly($propiedade);
    }

}
Fixarse no método mount. En principio non é necesario poñelo xa que automaticamente vai gardar o id pasado dende a vista de Laravel á propiedade id do compoñente livewire, pero como queremos atopar a descripción, o poñemos para atopar o modelo asociado a dita id e obter a descripción.


  • Agora na vista do compoñente temos que cambiar a chamada á función dependendo do modo no que estemos (e os textos do botón de alta e mensaxe confirm).

Arquivo resources/views/livewire/marca.blade.php

<div>
    <form wire:submit.prevent="{{$modo=="engadir" ? 'gardarMarca' : 'actualizarMarca' }}">

        @csrf
        <div>
            Descripción:<input type="text" maxlength="45" size="45" required
                wire:model.live="descripcion" name="descripcion" />
            @error('descripcion') <span class="error">{{ $message }}</span> @enderror
        </div>

        <div>
            <input type="submit" name="btnAlta" value="{{$modo=='engadir' ? "Alta" : "Modificar"}}"
               onClick="return confirm('{{$modo=="engadir" ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');"/>
            <input type="reset" name="btnBorrar" value="Limpar" />

        </div>


    </form>

    @include('mensaxes.mensaxes')
</div>



Listado

  • O listado o imos implementar empregando un compoñente livewire.
Ademáis imos poder empregar un campo de filtro e paxinación sen necesidade de recargar toda a páxina ao empregar o compoñente.
Executamos a orde dende terminal: php artisan make:livewire marca-listado
Creará:
Class: app/Livewire/MarcaListado.php
View: resources/views/livewire/marca-listado.blade.php


  • Arquivo app/Livewire/MarcaListado.php
Polo de agora imos amosar o listado de marcas.
<?php
namespace App\Livewire;

use Livewire\Component;
use App\Models\Marca;

use Illuminate\Support\Facades\Log;

class MarcaListado extends Component
{

    public function render()
    {
        $marcas = Marca::orderBy('descripcion', 'asc')->get();

        return view('livewire.marca-listado', compact('marcas'));
    }
}


  • Arquivo resources/views/livewire/marca-listado.php
<div>

    <table class="table-auto w-full border-collapse border border-gray-300">
        <thead>
            <tr>
                <th class="border border-gray-300 p-2">ID</th>
                <th class="border border-gray-300 p-2">Descripción</th>
                <th class="border border-gray-300 p-2">Acciones</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($marcas as $marca)
                <tr wire:key="marca-{{ $marca->id }}">
                    <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
                    <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
                    <td class="border border-gray-300 p-2">
                        <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
                            Editar
                        </a>
                        <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="3" class="border border-gray-300 p-2 text-center">No se encontraron marcas</td>
                </tr>
            @endforelse
        </tbody>
    </table>

    @include('mensaxes.mensaxes')

</div>

NOTAS:

  • Fixarse que cambiamos o botón de modificar por un enlace. Se podería empregar un botón cun formulario ou outra forma de que cargue unha url.
  • Fixarse que no bucle o tr ten un wire:key. Isto é necesario sempre que teñades un bucle e empregedes:
  • @foreach de elementos dinámicos
  • Botóns wire:click dentro do loop
  • Eliminación ou actualización de filas
  • Inputs editables en lista
  • Ordenaión ou filtros


  • Indicamos no controlador da marca que cargue unha vista cando chamemos a index
Arquivo app/Http/Controllers/MarcaController.php
   ...............
    public function index()
    {
        return view('marcas.marcas');
    }


  • Arquivo resources/views/marcas/marcas.blade.php
Cargamos o livewire que amosa o listado. En principio non necesitamos enviarlle ningún dato xa que é o propio compoñente livewire quen obtén o listado de marcas.
@extends('plantilla');

@section('pageTitle', 'Listado de marcas')

@section('content')
<h2>Listado de Marcas</h2>


@livewire("marca-listado")

@include('mensaxes.alertas_javascript')


@endsection


Se agora probades a poñer http://localhost:8000/marcas vos amosara o listado de marcas.


Paxinación

No controlador do compoñente livewire imos indicar que empregaremos paxinación e cando obtemos a lista de marcas faremos o paxinate.
  • Arquivo app/Livewire/MarcaListado.php
<?php
namespace App\Livewire;

use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Marca;

use Illuminate\Support\Facades\Log;


class MarcaListado extends Component
{
    use WithPagination;

    public function render()
    {
        $marcas = Marca::orderBy('descripcion', 'asc')->simplePaginate(3);

        return view('livewire.marca-listado', compact('marcas'));
    }
}
Nota: Poño simplePaginate en vez de Paginate xa que por defecto engade dúas iconas para desprazarse á dereita/esqueda polas páxinas, pero ditas iconas se ven moi grandes, supoño a que por defecto emprega Tailwind CSS. Se consultades o enlace superior indica como empregar Bootstrap.


  • Agora imos indicarlle á vista que visualice os enlaces de seguinte e anterior:
  • Arquivo resources/views/livewire/marca-listado.blade.php
<div>

    <table class="table-auto w-full border-collapse border border-gray-300">
        <thead>
            <tr>
                <th class="border border-gray-300 p-2">ID</th>
                <th class="border border-gray-300 p-2">Descripción</th>
                <th class="border border-gray-300 p-2">Acciones</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($marcas as $marca)
                <tr wire:key="marca-{{ $marca->id }}">
                    <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
                    <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
                    <td class="border border-gray-300 p-2">
                        <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
                            Editar
                        </a>
                        <button class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="3" class="border border-gray-300 p-2 text-center">No se encontraron marcas</td>
                </tr>
            @endforelse
        </tbody>
    </table>
    <div class="mt-4">
        {{ $marcas->links() }}
    </div>

    @include('mensaxes.mensaxes')

</div>
Cada 3 marcas teredes unha páxina.



Filtro

  • Para filtrar imos necesitar na vista un campo input no que imos poder poñer unha palabra e amosará as marcas que teñan dita palabra.
Iso o fará se necesidade de cargar novamente a páxina enteira.
  • Arquivo resources/views/livewire/marca-listado.blade.php
<div>
    <input type="text" wire:model.live="filtro" placeholder="Buscar marcas..." class="mb-4 p-2 border rounded">

    <table class="table-auto w-full border-collapse border border-gray-300">
        <thead>
            <tr>
                <th class="border border-gray-300 p-2">ID</th>
                <th class="border border-gray-300 p-2">Descripción</th>
                <th class="border border-gray-300 p-2">Acciones</th>
            </tr>
        </thead>

       ................
Fixarse que o campo input vai estar ligado a unha propiedade de nome filtro que se atopará no controlador do livewire. Ao poñer wire:model.live facemos que cando premamos unha tecla vai actualizar dita propiedade. Se non queremos facelo ao premer na tecla teríamos que ter un botón que ao premer fixera dita o proceso de actualizar a vista.


  • Arquivo app/Livewire/MarcaListado.php
<?php
namespace App\Livewire;

use Livewire\Component;
use Livewire\WithPagination;
use App\Models\Marca;

use Illuminate\Support\Facades\Log;


class MarcaListado extends Component
{
    use WithPagination;
    public $filtro;
    protected $queryString = ['filtro']; // Sincronizar búsqueda con la URL



    public function render()
    {
        $marcas = Marca::where('descripcion', 'like', '%' . $this->filtro . '%')->orderBy('descripcion', 'asc')
            ->simplePaginate(5);


        return view('livewire.marca-listado', compact('marcas'));
    }
}
Nota: A liña 15 non é obrigatoria. O que fai a propiedade $queryString é actualizar a URL có contido da campo de busca no parámetro filtro. Sería necesario se queremos empregar a url para algún proceso posterior.


Filtro: Variante con botón

  • Imos facer unha variante do anterior para que soamente busque cando premamos un botón.
Para isto temos que cambiar wire.model.live por wire.model e engadir un botón.
  • Arquivo resources/views/livewire/marca-listado.blade.php
<div>
    <input type="text" wire:model="filtro" placeholder="Buscar marcas..." class="mb-4 p-2 border rounded">
    <input type="button" wire:click="$refresh" value="Buscar" />
    .............
Fixarse que o botón o que fai é chamar a un método 'refresh' que xa ten definido internamento o controlador do livewire.
O que fai dito método e chamar ao método render do controlador.
Por outra banda, ao empregar wire:model, o campo non se actualiza no controlador ata que alguén o chame (como fai refresh).
NOTA: Se queremos chamar a un método do controlador o podemos facer poñendo: wire:click="nomeMetodo" e definindo dito método no controlador.

Baixa

  • A baixa vai consistir en chamar a un método do controlador ao que lle mandaremos o id da marca a dar de baixa.
Polo tanto definimos o método no controlador.


Arquivo app/Livewire/MarcaListado.php

   ................
    public function eliminarMarca($id){
        $marca = Marca::find($id);  // Se poñemos findOrFail a aplicación pararía se non o atopa.

        if ($marca) {
            $marca->delete();
            session()->flash('message', 'Marca eliminada correctamente.');
        } else {
            $this->addError('error', 'Marca non atopada.');
        }
    }
Como vedes é moi sinxelo. Buscamos a marca baseada nese id e se a atopa a borra. Despois xa chama a render automaticamente. Se non atopa envía unha mensaxe de erro.
Empregamos $this->addError para gardar o erro na variable 'error'.


  • IMPORTANTE: Ao facer que o botón de Baixa se gestione con wire:click, se queremos pedir confirmación do borrado non podemos empregar confirm de javascripr, xa que vai realizar a llamada igual. Debemos empregar wire:confirm como está indicado no exemplo seguinte.


Arquivo resources/views/livewire/marca-listado.blade.php

<div>
    <input type="text" wire:model="filtro" placeholder="Buscar marcas..." class="mb-4 p-2 border rounded">
    <input type="button" wire:click="$refresh" value="Buscar" />
    <table class="table-auto w-full border-collapse border border-gray-300">
        <thead>
            <tr>
                <th class="border border-gray-300 p-2">ID</th>
                <th class="border border-gray-300 p-2">Descripción</th>
                <th class="border border-gray-300 p-2">Acciones</th>
            </tr>
        </thead>
        <tbody>
            @forelse ($marcas as $marca)
                <tr>
                    <td class="border border-gray-300 p-2">{{ $marca->id }}</td>
                    <td class="border border-gray-300 p-2">{{ $marca->descripcion }}</td>
                    <td class="border border-gray-300 p-2">
                        <a href="{{ route('marcas.edit',['marca' => $marca->id]) }}" class="bg-blue-500 text-white px-4 py-1 rounded">
                            Editar
                        </a>
                        <button wire:click="eliminarMarca({{ $marca->id }})"
                            wire:confirm="Estás seguro de borrar la marca {{ $marca->descripcion }}? "
                            class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
                    </td>
                </tr>
            @empty
                <tr>
                    <td colspan="3" class="border border-gray-300 p-2 text-center">No se encontraron marcas</td>
                </tr>
            @endforelse
        </tbody>
    </table>
    <div class="mt-4">
        {{ $marcas->links() }}
    </div>

    @include('mensaxes.mensaxes')

    @error('error')
        <div class="alert alert-danger">
            {{ $message }}
        </div>
    @enderror

</div>
Comprobamos se temos un erro na variable 'error' de livewire.



Axustes finais

  • Imos modificar a modificación e alta para que vain á pantalla de listado.
Arquivo app/Livewire/Marca.php
<?php

namespace App\Livewire;

use Illuminate\Support\Facades\Log;

use Livewire\Attributes\Validate;
use App\Models\Marca as MarcaModel;

use Livewire\Component;

class Marca extends Component
{
    public $descripcion = "Valor inicial";
    public $id; // A empregamos na operación de modificación/baixa para atopar a marca a modificar ou borrar
    public $modo = "engadir";

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
    ];

    protected $messages = [
        'descripcion.required' => 'La descripción es obligatoria.',
        'descripcion.string' => 'La descripción debe ser una cadena de texto.',
        'descripcion.min' => 'La descripción debe tener al menos 5 caracteres.',
        'descripcion.max' => 'La descripción no puede tener más de 45 caracteres.',
    ];

    public function mount($id = null)
    {
        $this->id = $id;
        if($id) {
            $marca = MarcaModel::findOrFail($id);
            $this->descripcion = $marca->descripcion;
        }
    }

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

    public function gardarMarca()
    {
        $this->validate();

        Log::info('Pasa a validación');

        $marcaCreada = MarcaModel::create([
            'descripcion' => $this->descripcion
        ]);

        $this->id = $marcaCreada->id;

        session()->flash('message', "Marca creada correctamente con id $marcaCreada->id");
        $this->reset(['descripcion']);

        return $this->redirectRoute('marcas.index');
    }

    public function actualizarMarca()
    {
        $this->validate();

        $marca = MarcaModel::findOrFail($this->id);
        $marca->update([
            'descripcion' => $this->descripcion,
        ]);

        session()->flash('message', 'Marca actualizada correctamente.');

        return $this->redirectRoute('marcas.index');

    }

    public function updated($propiedade,$valor){
        $this->validateOnly($propiedade);
    }

}


  • Engado a entrada de listar perfumes.
Arquivo resources/plantilla.blade.php
<!DOCTYPE html>
<html>
    <head>
        <title>@yield('pageTitle')</title>

        <style>
            .menu{
                float: left;
                width: 30%;
                padding-left: 5px;
            }
            .menu a{
                display: block;
            }
            .contido{
                float: left;
                width: 65%;
            }

        </style>
    </head>
    <body>
        <div class="menu">
            <a href='{{route('perfumes.index')}}'>Operacións Perfumes</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
            <a href='{{route('marcas.index')}}'>Operacións Marcas</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
        </div>
         <div class="contido">
            @yield('content')
        </div>
    </body>
</html>



Aclaracións varias

  • Neste exercicio empregamos o controlador de Laravel para cargar o mesmo compoñente livewire na alta e modificación empregando un parámetro para informar ao compoñnente en que 'modo' se atopaba.
Normalmente isto non é necesario.
Pódese facer unha única vista no controlador de Laravel que cargue o compoñente livewire e xestionar dende dentro do compoñente o listado, alta, baixa e modificación.
Dentro do compoñente podes definir unha variable pública estado que determine cando a vista ten que visualizar o listado, a alta-modificación.


Vos queda como exercicio para facer.
Tedes un exemplo de cómo facelo nos vídeos da plataforma a distancia.



Exercicio proposto

  • Imos implementar a xestión da perfumes empregando compoñentes Livewire.
Dexiaremos o feito na UD6 e faremos unha nova versión de xestión dos perfumes (listado, alta, baixa e modificación) empregando compoñentes Livewire.


  • Para cargar os compoñentes dentro da páxina master de Laravel crearemos o cartafol resources/views/perfumes_b/ e o controlador de Laravel chamarase PerfumeBController.
As rutas as teremos que definir nos (non podemos empregar resource xa que perfumes están definidas para empregar o controlador de Laravel).
Empregaremos:
  • perfumesB/ => PerfumeBController@index


  • No listado (index) imos a poder:
  • Crear un novo perfume
  • Modificar un perfume
  • Borrar un perfume
Na creación/modificación do perfume imos engadir un campo novo, que vai a ser un select coa marca á que pertence o perfume.
  • Imos empregar dous compoñentes Livewire:
  • O encargado de listar os perfumes. Nome perfume-listar
  • O encargado de engadir/modificar un perfume. Nome perfume
No seguinte punto imos ver como podemos enviar 'mensaxes' de un compoñente a outro. Polo de agora ides amosar os dous compoñentes na mesma páxina e desta forma aparecerá o listado e os campos para dar de alta un novo perfume ao mesmo tempo. Despois solucionaremos o problema de como facer que non apareza ata que se pulse o botón de Novo Perfume e que cambie o contido/acción cando se prema o de modificar perfume. Polo de agora non fagades o código de modificar perfume. Soamente amosar o botón pero que non faga nada.
É dicir, a páxina resources/views/perfumes_b/perfumes.blade.php terá este código:
@extends('plantilla');

@section('pageTitle', 'Listado de perfumes con Livewire')

@section('content')
<h2>Listado de Perfumes con Livewire</h2>


@livewire("perfume-listado")

@livewire("perfume")

@include('mensaxes.alertas_javascript')


@endsection
E o compoñente livewire perfume amosará os campos para dar de alta un novo perfume.
Ud livewire 1.png



  • Lembrar, se non o fixéchedes antes, comprobar que a táboa perfumes ten un campo marca_id relacionada coa táboa marcas.
CREATE TABLE `perfumes` (
  `id` int NOT NULL AUTO_INCREMENT,
  `descripcion` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_spanish2_ci NOT NULL,
  `prezo` decimal(8,2) NOT NULL,
  `data_compra` date DEFAULT NULL,
  `marca_id` int DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_PERFUMES_1_idx` (`id`),
  KEY `fk_PERFUMES_MARCA_idx` (`marca_id`),
  CONSTRAINT `fk_perfumes_1` FOREIGN KEY (`marca_id`) REFERENCES `marcas` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_spanish2_ci;
E crear a relación en Laravel entre os dous modelos.



  • Se seguides a solución proposta para Marca, podedes intentar empregar o método Modelo::updateOrCreate de Laravel que permite crear ou modificar unha fila da táboa. Podedes intentar crear un único método para as dúas operacións e non facer como en Marca, de ter un para a actualización e outro para a inserción.

Solución Exercicio proposto

Arquivo app/Models/Perfume.php

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Carbon\Carbon;

class Perfume extends Model
{
    protected $table = 'perfumes';  // Indicamos a táboa relacionada con este modelo.
    protected $primaryKey = 'id';    // Indicamos a clave primaria da táboa

    public $timestamps = false;    // Por defecto laravel inclúe dous campos (created_at e updated_at) de tipo datetime cando se fai unha operación de inserción e actualización sobre unha fila. Con isto indicamos que a táboa non ten eses campos.

    protected $casts = [           // Cando accedamos a unha fila empregando este modelo, se poñemos $fila->data_compra teremos un string. Se poñemos o cast, obteremos unha instancia da [https://carbon.nesbot.com/docs/ clase Carbon] de Laravel que nos permite facer operacións e formatear a data.
        'data_compra' => 'date',   // Tamén se pode empregar datetime. Devolve un Carbon
    ];

    protected $fillable =   [                            // Isto xa o veremos. O que indica é que cando facemos unha operación masiva de inserción ou un insert da forma create(datos) soamente deixa enviar en datos (que é un array) as columnas indicadas nesta propiedade (as columnas indicadas son descripcion, prezo e data_compra). Faise por motivos de seguridade.
        'descripcion',
        'prezo',
        'data_compra',
        'marca_id'
    ];

    protected function DataCompra(): Attribute
    {
        return Attribute::make(
            get: fn($value) => $value ? Carbon::parse($value)->format('d/m/Y') : null,   // Cando empregamos a data da forma echo($perfume->data_compra); convirte o obxecto Carbon a unha cadea có formato correcto
            set: fn($value) => $value ? Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d') : null   // Cando empregamos a data da forma: $perfume->data_compra = "10/01/2025" o gardará
        );
    }

   protected function Prezo(): Attribute
    {
        return Attribute::make(
            // Cuando accedemos al valor del campo, convertimos el punto en coma
            get: fn ($value) => number_format($value, 2, ',', '.'),
            // Cuando se asigna el valor, reemplazamos la coma por un punto
            set: fn ($value) => str_replace(',', '.', $value),
        );
    }

    public function marca() {
        return $this->belongsTo(Marca::class);
    }

}


Arquivo routes.php

<?php

use Illuminate\Support\Facades\Route;

use App\Http\Controllers\PerfumeController;
use App\Http\Controllers\MarcaController;
use App\Http\Controllers\PerfumeBController;

Route::resource('perfumes', PerfumeController::class);
Route::resource('marcas', MarcaController::class);
Route::get('perfumesB', [PerfumeBController::class, 'index'])->name('perfumesB.index');


Arquivo app/Htpp/Controllers/PerfumeBController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PerfumeBController extends Controller
{
    public function index()
    {
        return view('perfumes_b.perfumes');
    }

}


Arquivo resources/views/perfumes_b/perfumes.blade.php

@extends('plantilla');

@section('pageTitle', 'Listado de perfumes con Livewire')

@section('content')
<h2>Listado de Perfumes con Livewire</h2>


@livewire("perfume-listado")

@livewire("perfume")

@include('mensaxes.alertas_javascript')


@endsection


Arquivo app/Livewire/PerfumeListado.php

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Perfume;

class PerfumeListado extends Component
{
    public $perfumes;

    public function cargarPerfumes()
    {
        $this->perfumes = Perfume::with('marca')->get();
    }

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

    public function eliminarPerfume($id)
    {
        $perfume = Perfume::find($id);

        if (!$perfume) {
            $this->addError('error', 'Perfume non atopado.');
            return;
        }

        $perfume->delete();
        session()->flash('message', 'Perfume eliminado correctamente.');

        $this->cargarPerfumes();
    }

    public function render()
    {
        return view('livewire.perfume-listado');
    }
}


Arquivo resources/views/livewire/perfume-listado.blade.php

<div>
    <button class="bg-green-500 text-white px-2 py-1 rounded mb-2">Novo perfume</button>

    <table class="table-auto w-full border">
        <thead>
            <tr>
                <th>Descrición</th>
                <th>Prezo</th>
                <th>Data Compra</th>
                <th>Marca</th>
                <th>Accións</th>
            </tr>
        </thead>
        <tbody>
            @foreach($perfumes as $perfume)
            <tr>
                <td>{{ $perfume->descripcion }}</td>
                <td>{{ $perfume->prezo }}</td>
                <td>{{ $perfume->data_compra }}</td>
                <td>{{ $perfume->marca->descripcion ?? 'Sen marca' }}</td>
                <td>
                    <button  class="bg-blue-500 text-white px-2 py-1 rounded">Editar</button>
                    <button wire:click="eliminarPerfume({{ $perfume->id }})"
                        wire:confirm="Estás seguro de borrar la marca {{ $perfume->descripcion }}? "
                        class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
            </td>
            </tr>
            @endforeach
        </tbody>
    </table>

    @include('mensaxes.mensaxes')
</div>


Arquivo app/Livewire/Perfume.php

<?php

namespace App\Livewire;

use Livewire\Component;
use App\Models\Perfume as PerfumeModel;
use App\Models\Marca;

class Perfume extends Component
{
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
    public $modo = 'engadir';

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
        'prezo' => 'required|numeric|min:0|max:999999.99',
        'data_compra' => 'nullable|date|after_or_equal:today',
        'marca_id' => 'nullable|exists:marcas,id',
    ];


    public function mount($id = null)
    {
        $this->id = $id;

        if ($id) {
            $perfume = PerfumeModel::findOrFail($id);
            $this->descripcion = $perfume->descripcion;
            $this->prezo = $perfume->prezo;
            $this->data_compra = $perfume->data_compra;
            $this->marca_id = $perfume->marca_id;
        }
    }

    public function gardarPerfume()
    {
        $this->validate();

        if($this->modo=='engadir')  {
            $this->id = -1;
        }

        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
        // Revisade o código e podedes ver que temos un cast con Date e un set para pasar a data que ven en formato español da caixa de texto a inglés que se vai gardar na base de datos mysql.   
        // Nesta versión a data vaise recoller cun input de tipo 'date' que xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
        $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
        // Crear o actualizar el perfume
        $perfumeCreado = PerfumeModel::updateOrCreate(
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
            [
                'descripcion' => $this->descripcion,
                'prezo' => $this->prezo,
                'data_compra' => $this->data_compra,
                'marca_id' => $this->marca_id
            ]
        );

        // Si se ha creado un nuevo perfume, asignar el ID
        if (!$this->id) {
            $this->id = $perfumeCreado->id;
        }

        // Mensaje de éxito
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");

        // Resetear los campos
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);

        // Redirigir a la lista de perfumes
        return redirect()->route('perfumesB.index');
    }

    public function render()
    {
        $marcas = Marca::orderBy('descripcion')->get();
        return view('livewire.perfume',compact('marcas'));
    }
}

Arquivo resources/views/livewire/perfume.blade.php

<div class="p-4 border rounded bg-gray-100">
    <form wire:submit.prevent="gardarPerfume">

        <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>

        <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
        @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
        <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
        @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
        <input type="date" wire:model="data_compra" class="block mb-2 w-full">
        @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
        <select wire:model="marca_id" class="block mb-2 w-full">
            <option value="">-- Selecciona unha marca --</option>
            @foreach ($marcas as $marca)
                <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
            @endforeach
        </select>
        @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror

        <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
            onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
        <input type="reset" name="btnBorrar" value="Limpar" />

    </form>
    @include('mensaxes.mensaxes')
</div>


Arquivo resources/views/plantilla.blade.php

<!DOCTYPE html>
<html>
    <head>
        <title>@yield('pageTitle')</title>

        <style>
            .menu{
                float: left;
                width: 30%;
                padding-left: 5px;
            }
            .menu a{
                display: block;
            }
            .contido{
                float: left;
                width: 65%;
            }

        </style>
    </head>
    <body>
        <div class="menu">
            <a href='{{route('perfumes.index')}}'>Operacións Perfumes</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
            <a href='{{route('perfumesB.index')}}'>Operacións Perfumes Livewire</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
            <a href='{{route('marcas.index')}}'>Operacións Marcas</a>  <!-- Se podería poñer /perfumes/listar pero mellor empregar os nomes definidos no arquivo de routes.php-->
        </div>
         <div class="contido">
            @yield('content')
        </div>
    </body>
</html>

Arquivo app/Models/Perfume.php


Eventos entre compoñentes

Imos seguir co exemplo anterior de perfumes e imos solucionar as funcionalidades que nos quedan.


  • Os eventos poden ir dende un compoñente livewire a outro ou dende un compoñente livewire a un script dunha plantilla blade.


  • Imos aplicar isto á nosa pantalla de Perfumes.
Se vos fixades, cando creamos un novo perfume no compoñente livewire Perfume, recargamos a páxina:

Arquivo app/Livewire/Perfume.php

    ............
    public function gardarPerfume()
    {
        $this->validate();
        ............

        return redirect()->route('perfumesB.index');
    }
O que imos facer é enviar un evento a todos os compoñentes livewire que estean cargados na páxina e que estén escoitando dito evento.
No noso caso:

Arquivo app/Livewire/Perfume.php

    ............
    public function gardarPerfume()
    {
        $this->validate();
        ............

        $this->dispatch('refrescar-listado-perfumes');
    }
O nome pode ser calquera. Agora temos que facer que o compoñente livewire de listado escoite dito evento e asocialo a un método.

Arquivo app/Livewire/PerfumeListado.php

<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\Attributes\On;

use App\Models\Perfume;

class PerfumeListado extends Component
{
  .................
    #[On('refrescar-listado-perfumes')]
    public function refrescarListado(){
        $this->cargarPerfumes();
    }
O nome da liña On debe ser o mesmo que empregamos cando fixemos o dispatch.
Agora se probades a dar de alta un novo perfume se actualizará o listado pero no se carga a páxina novamente.


  • Agora imos a facer que cando premamos no botón de alta fagamos unha alta no compoñente livewire de perfume e o mesmo na modificación.
Para iso necesitamos crear un método no controlador de PerfumeListado que sexa chamado polos botóns de alta e modificar enviando no caso de modificar o id do perfume.
Despois o que fará este método é lanzar un evento para que sexa capturado polo compoñente livewire Perfume e modificar a propiedade modo e cargar os datos do perfume en caso de modificación.

Arquivo resources/views/livewire/perfume-listado.blade.php

<div>
    <button wire:click="operacionPerfume" class="bg-green-500 text-white px-2 py-1 rounded mb-2">Novo perfume</button>

    <table class="table-auto w-full border">
        <thead>
            <tr>
                <th>Descrición</th>
                <th>Prezo</th>
                <th>Data Compra</th>
                <th>Marca</th>
                <th>Accións</th>
            </tr>
        </thead>
        <tbody>
            @foreach($perfumes as $perfume)
            <tr>
                <td>{{ $perfume->descripcion }}</td>
                <td>{{ $perfume->prezo }}</td>
                <td>{{ $perfume->data_compra }}</td>
                <td>{{ $perfume->marca->descripcion ?? 'Sen marca' }}</td>
                <td>
                    <button wire:click="operacionPerfume({{$perfume->id}})" class="bg-blue-500 text-white px-2 py-1 rounded">Editar</button>
                    <button wire:click="eliminarPerfume({{ $perfume->id }})"
                        wire:confirm="Estás seguro de borrar la marca {{ $perfume->descripcion }}? "
                        class="bg-red-500 text-white px-4 py-1 rounded">Eliminar</button>
            </td>
            </tr>
            @endforeach
        </tbody>
    </table>

    @include('mensaxes.mensaxes')
</div>


Arquivo app/Livewire/PerfumeListado.php

<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\Attributes\On;
use Illuminate\Support\Facades\Log;

use App\Models\Perfume;

class PerfumeListado extends Component
{
    public $perfumes;

    public function cargarPerfumes()
    {
        $this->perfumes = Perfume::with('marca')->get();
    }

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

    public function operacionPerfume($id=null){
        $this->dispatch('operar-perfume',id:$id); // Chamamos ao método e enviamos o id do perfume. null => alta, numero => modificar
    }

    #[On('refrescar-listado-perfumes')]
    public function refrescarListado(){
        $this->cargarPerfumes();
    }

    public function eliminarPerfume($id)
    {
        $perfume = Perfume::find($id);

        if (!$perfume) {
            $this->addError('error', 'Perfume non atopado.');
            return;
        }

        $perfume->delete();
        session()->flash('message', 'Perfume eliminado correctamente.');

        $this->cargarPerfumes();
    }

    public function render()
    {
        return view('livewire.perfume-listado');
    }
}


Arquivo app/Livewire/Perfume.php

<?php

namespace App\Livewire;

use Livewire\Attributes\On;
use Livewire\Component;

use App\Models\Perfume as PerfumeModel;
use App\Models\Marca;

class Perfume extends Component
{
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
    public $modo = 'engadir';

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
        'prezo' => 'required|numeric|min:0|max:999999.99',
        'data_compra' => 'nullable|date|after_or_equal:today',
        'marca_id' => 'nullable|exists:marcas,id',
    ];


    public function mount($id = null)
    {
        $this->id = $id;

        if ($id) {
            $perfume = PerfumeModel::findOrFail($id);
            $this->descripcion = $perfume->descripcion;
            $this->prezo = $perfume->prezo;
            $this->data_compra = $perfume->data_compra;
            $this->marca_id = $perfume->marca_id;
        }
    }
    #[On('operar-perfume')]
    public function operarPerfume($id){

        if($id==null){
            $this->modo = "engadir";
            $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
            $this->id = -1;
        }
        else{
            $this->id = $id;
            $perfume = PerfumeModel::findOrFail($id);
            $this->descripcion = $perfume->descripcion;
            $this->prezo = str_replace(',','.',$perfume->prezo);    // Prezo ten un mutator para poñer coma en vez de punto
            if($this->data_compra){
                $this->data_compra = \Carbon\Carbon::createFromFormat('d/m/Y', $perfume->data_compra)->format('Y-m-d');
            }
            $this->marca_id = $perfume->marca_id;
            $this->modo ="actualizar";
        }
    }

    public function gardarPerfume()
    {
        $this->validate();

        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
        // Nesta versión a data xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
        if($this->data_compra) {
          $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
        }

        // Crear o actualizar el perfume
        $perfumeCreado = PerfumeModel::updateOrCreate(
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
            [
                'descripcion' => $this->descripcion,
                'prezo' => $this->prezo,
                'data_compra' => $this->data_compra,
                'marca_id' => $this->marca_id
            ]
        );

        // Si se ha creado un nuevo perfume, asignar el ID
        if (!$this->id) {
            $this->id = $perfumeCreado->id;
        }

        // Mensaje de éxito
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");

        // Resetear los campos
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);

        // Redirigir a la lista de perfumes
        //return redirect()->route('perfumesB.index');
        $this->dispatch('refrescar-listado-perfumes');
    }

    public function render()
    {
        $marcas = Marca::orderBy('descripcion')->get();
        return view('livewire.perfume',compact('marcas'));
    }
}


Arquivo resources/views/livewire/perfume.blade.php

<div class="p-4 border rounded bg-gray-100">
    <form wire:submit.prevent="gardarPerfume">

        <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>

        <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
        @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
        <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
        @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
        <input type="date" wire:model="data_compra" class="block mb-2 w-full">
        @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
        <select wire:model="marca_id" class="block mb-2 w-full">
            <option value="">-- Selecciona unha marca --</option>
            @foreach ($marcas as $marca)
                <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
            @endforeach
        </select>
        @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror

        <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
            onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
        <input type="reset" name="btnBorrar" value="Limpar" />

    </form>
    @include('mensaxes.mensaxes')
</div>



  • Agora soamente queda por facer que a alta/modificación sexa invisible ata que se pulse o botón correspondente.
Iso o podemos facer na vista mirando que o modo sexa engadir/actualizar en en caso de que non o sexa, non amosar nada (algo hai que amosar, polo que envolvemos todo con un div)

Arquivo resources/views/livewire/perfume.blade.php

<div>
@if($modo=='engadir' || $modo=='actualizar')
    <div class="p-4 border rounded bg-gray-100">
        <form wire:submit.prevent="gardarPerfume">

            <h2 class="text-lg font-bold mb-2">{{ $modo == 'engadir' ? 'Novo Perfume' : 'Editar Perfume' }}</h2>

            <input type="text" wire:model="descripcion" placeholder="Descrición" class="block mb-2 w-full">
            @error('descripcion') <span class="error">{{ $message }}</span><br /> @enderror
            <input type="number" step="0.01" wire:model="prezo" placeholder="Prezo" class="block mb-2 w-full">
            @error('prezo') <span class="error">{{ $message }}</span><br /> @enderror
            <input type="date" wire:model="data_compra" class="block mb-2 w-full">
            @error('data_compra') <span class="error">{{ $message }}</span><br /> @enderror
            <select wire:model="marca_id" class="block mb-2 w-full">
                <option value="">-- Selecciona unha marca --</option>
                @foreach ($marcas as $marca)
                    <option value="{{ $marca->id }}">{{ $marca->descripcion }}</option>
                @endforeach
            </select>
            @error('marca_id') <span class="error">{{ $message }}</span><br /> @enderror

            <input type="submit" name="btnAlta" value="{{ $modo == 'engadir' ? 'Alta' : 'Modificar' }}"
                onClick="return confirm('{{ $modo == 'engadir' ? 'Estás seguro de dar de alta a nova marca' : "Estás seguro de dar modificar a marca $descripcion ?" }}');" />
            <input type="reset" name="btnBorrar" value="Limpar" />

        </form>
        @include('mensaxes.mensaxes')
    </div>
@endif
</div>


  • Agora no controlador inicializamos o modo a "" e o cambiamos en función da operación a realizar.

Arquivo app/Livewire/Perfume.php

<?php

namespace App\Livewire;

use Livewire\Attributes\On;
use Livewire\Component;

use App\Models\Perfume as PerfumeModel;
use App\Models\Marca;

class Perfume extends Component
{
    public $id, $descripcion, $prezo, $data_compra, $marca_id;
    public $modo="";

    protected $rules = [
        'descripcion' => 'required|string|min:5|max:45',
        'prezo' => 'required|numeric|min:0|max:999999.99',
        'data_compra' => 'nullable|date|after_or_equal:today',
        'marca_id' => 'nullable|exists:marcas,id',
    ];


    public function mount($id = null)
    {
        $this->id = $id;

        if ($id) {
            $perfume = PerfumeModel::findOrFail($id);
            $this->descripcion = $perfume->descripcion;
            $this->prezo = $perfume->prezo;
            $this->data_compra = $perfume->data_compra;
            $this->marca_id = $perfume->marca_id;
        }
    }
    #[On('operar-perfume')]
    public function operarPerfume($id){

        if($id==null){
            $this->modo = "engadir";
            $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);
            $this->id = -1;
        }
        else{
            $this->id = $id;
            $perfume = PerfumeModel::findOrFail($id);
            $this->descripcion = $perfume->descripcion;
            $this->prezo = str_replace(',','.',$perfume->prezo);    // Prezo ten un mutator para poñer coma en vez de punto
            if($this->data_compra){
                $this->data_compra = \Carbon\Carbon::createFromFormat('d/m/Y', $perfume->data_compra)->format('Y-m-d');
            }
            $this->marca_id = $perfume->marca_id;
            $this->modo ="actualizar";
        }
    }

    public function gardarPerfume()
    {
        $this->validate();

        // Na versión anterior de xestión de perfumes, temos posto no modelo un set para pasar automaticamente da data en formato español a data en formato inglés.
        // Nesta versión a data xa ven en formato inglés polo que daría un erro. Por iso a pasamos antes a formato español
        if($this->data_compra) {
          $this->data_compra = \Carbon\Carbon::createFromFormat('Y-m-d', $this->data_compra)->format('d/m/Y');
        }
        // Crear o actualizar el perfume
        $perfumeCreado = PerfumeModel::updateOrCreate(
            ['id' => $this->id],  // Si existe un ID, lo actualiza. Si no, lo crea.
            [
                'descripcion' => $this->descripcion,
                'prezo' => $this->prezo,
                'data_compra' => $this->data_compra,
                'marca_id' => $this->marca_id
            ]
        );

        // Si se ha creado un nuevo perfume, asignar el ID
        if (!$this->id) {
            $this->id = $perfumeCreado->id;
        }

        // Mensaje de éxito
        session()->flash('message', "Perfume guardado correctamente con id $perfumeCreado->id");

        // Resetear los campos
        $this->reset(['descripcion', 'prezo', 'data_compra', 'marca_id']);

        $this->modo="";
        // Redirigir a la lista de perfumes
        //return redirect()->route('perfumesB.index');
        $this->dispatch('refrescar-listado-perfumes');
    }

    public function render()
    {
        $marcas = Marca::orderBy('descripcion')->get();
        return view('livewire.perfume',compact('marcas'));
    }
}




  • Neste caso non fixo falta, pero cando queremos refrescar o compoñente perfumes-listado, pode ser que en vez de gardar os datos nunha propiedade local ($perfumes), podemos envialos localmente no render, da forma:
    public function render()
    {
        $perfumes = Perfume::with('marca')->get();
        return view('livewire.perfume-listado',compact('perfumes');
    }
NOTA IMPORTANTE: Para que funcione desta forma tedes que eliminar a propiedade $perfumes do compoñente e xa non necesitades o mount nin o método cargarPerfumes.
Nese caso, se queremos 'refrescar' o compoñente para que volva a facer o render, teríamos que poñer: this->dispatch('render') no método 'refrescar-perfumes'.
    #[On('refrescar-listado-perfumes')]
    public function refrescarListado(){
        $this->dispatch('refrescar');
    }





Volver á UD7


-- Ángel D. Fernández González -- (2025).