Saltearse al contenido

Formularios

Dado que los formularios son la columna vertebral de la mayoría de las aplicaciones web, Livewire proporciona un montón de utilidades útiles para construirlos. Desde el manejo de simples elementos de entrada a cosas complejas como la validación en tiempo real o la carga de archivos, Livewire tiene herramientas simples y bien documentadas para hacer su vida más fácil y deleitar a sus usuarios.

Vamos a sumergirnos.

Enviando e un formulario

Empecemos por ver un formulario muy simple en un componente CrearPublicacion. Este formulario tendrá dos simples entradas de texto y un botón de envío, así como algo de código en el backend para gestionar el estado del formulario y su envío:

<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Publicacion;
class CrearPublicacion extends Component
{
public $titulo = '';
public $contenido = '';
public function guardar()
{
Publicacion::create(
$this->only(['titulo', 'contenido'])
);
session()->flash('status', 'Publicación almacenada');
return $this->redirect('/publicaciones');
}
public function render()
{
return view('livewire.crear-publicacion');
}
}
<form wire:submit="guardar">
<input type="text" wire:model="titulo">
<input type="text" wire:model="contenido">
<button type="submit">Guardar</button>
</form>

Como puedes ver, estamos «enlazando»(binding) las propiedades públicas $title y $content en el formulario de arriba usando wire:model. Esta es una de las características más utilizadas y potentes de Livewire.

Además de enlazar $titulo y $contenido, estamos usando wire:submit para capturar el evento submit cuando se pulsa el botón «Guardar» e invocar la acción guardar(). Esta acción mantendrá la entrada del formulario en la base de datos.

Una vez creado el nuevo post en la base de datos, redirigimos al usuario a la página del componente MostrarPublicaciones y le mostramos un mensaje «flash» de que se ha creado el nuevo post.

Agregando validación

Para evitar almacenar datos incompletos o peligrosos, la mayoría de los formularios necesitan algún tipo de validación.

Livewire hace que validar tus formularios sea tan sencillo como añadir atributos #[Validate] sobre las propiedades que quieres que sean validadas.

Una vez que una propiedad tiene un atributo #[Validate] adjunto, la regla de validación se aplicará al valor de la propiedad cada vez que se actualice en el servidor.

Añadamos algunas reglas de validación básicas a las propiedades $titulo y $contenido de nuestro componente CrearPublicacion:

<?php
namespace App\Livewire;
use Livewire\Component;
use App\Models\Publicacion;
class CrearPublicacion extends Component
{
#[Validate('required')]
public $titulo = '';
#[Validate('required')]
public $contenido = '';
public function guardar()
{
Publicacion::create(
$this->only(['titulo', 'contenido'])
);
session()->flash('status', 'Publicación almacenada');
return $this->redirect('/publicaciones');
}
public function render()
{
return view('livewire.crear-publicacion');
}
}

También modificaremos nuestra plantilla Blade para mostrar cualquier error de validación en la página.

<form wire:submit="guardar">
<input type="text" wire:model="titulo">
<div>
@error('titulo') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model="contenido">
<div>
@error('contenido') <span class="error">{{ $message }}</span> @enderror
</div>
<button type="submit">Guardar</button>
</form>

Ahora, si el usuario intenta enviar el formulario sin rellenar ninguno de los campos, verá mensajes de validación indicándole qué campos son obligatorios antes de guardar la entrada.

Livewire ofrece muchas más funciones de validación. Para más información, visite nuestra página de documentación dedicada a la Validación.

Extrayendo un objeto formulario

Si está trabajando con un formulario grande y prefiere extraer todas sus propiedades, lógica de validación, etc., en una clase separada, Livewire ofrece objetos de formulario.

Los objetos de formulario le permiten reutilizar la lógica del formulario a través de los componentes y proporcionan una buena manera de mantener su clase componente más limpia agrupando todo el código relacionado con el formulario en una clase separada.

Puede crear una clase de formulario a mano o utilizar el práctico comando artisan:

Ventana de terminal
php artisan livewire:form PublicacionForm

El comando anterior creará un archivo llamado app/Livewire/Forms/PublicacionForm.php

Vamos a reescribir el componente CrearPublicacion para utilizar una clase PublicacionForm:

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PublicacionForm extends Form
{
#[Validate('required|min:5')]
public $titulo = '';
#[Validate('required|min:5')]
public $contenido = '';
}
<?php
namespace App\Livewire;
use App\Livewire\Forms\PublicacionForm;
use Livewire\Component;
use App\Models\Publicacion;
class CrearPublicacion extends Component
{
public PublicacionForm $formulario;
public function save()
{
$this->validate();
Publicacion::create(
$this->formulario->all()
);
return $this->redirect('/publicacion');
}
public function render()
{
return view('livewire.crear-publicacion');
}
}
<form wire:submit="guardar">
<input type="text" wire:model="titulo">
<div>
@error('formulario.titulo') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model="contenido">
<div>
@error('formulario.contenido') <span class="error">{{ $message }}</span> @enderror
</div>
<button type="submit">Guardar</button>
</form>

Si lo desea, también puede extraer la lógica de creación de la entrada en el objeto de formulario de esta manera:

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use App\Models\Publicacion;
use Livewire\Form;
class PublicacionForm extends Form
{
#[Validate('required|min:5')]
public $titulo = '';
#[Validate('required|min:5')]
public $contenido = '';
public function almacenar()
{
$this->validate();
Publicacion::create($this->all());
}
}

Ahora puede llamar a $this->formulario->almacenar() desde el componente:

<?php
namespace App\Livewire;
use App\Livewire\Forms\PublicacionForm;
use Livewire\Component;
use App\Models\Publicacion;
class CrearPublicacion extends Component
{
public PublicacionForm $formulario;
public function save()
{
$this->form->store();
return $this->redirect('/publicacion');
}
public function render()
{
return view('livewire.crear-publicacion');
}
}

Si desea utilizar este objeto de formulario tanto para un formulario de creación como para uno de actualización, puede adaptarlo fácilmente para manejar ambos casos de uso.

Este es el aspecto que tendría utilizar este mismo objeto de formulario para un componente ActualizarPublicacion y rellenarlo con datos iniciales:

<?php
namespace App\Livewire;
use App\Livewire\Forms\PublicacionForm;
use Livewire\Component;
use App\Models\Publicacion;
class ActualizarPublicacion extends Component
{
public PublicacionForm $formulario;
public function mount(Publicacion $publicacion)
{
$this->formulario->setPublicacion($publicacion);
}
public function save()
{
$this->formulario->update();
return $this->redirect('/publicaciones');
}
public function render()
{
return view('livewire.actualizar-publicacion');
}
}
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Publicacion;
class PublicacionForm extends Form
{
public ?Publicacion $publicacion;
#[Validate('required|min:5')]
public $titulo = '';
#[Validate('required|min:5')]
public $contenido = '';
public function setPublicacion(Publicacion $publicacion)
{
$this->publicacion = $publicacion;
$this->titulo = $publicacion->titulo;
$this->contenido = $publicacion->contenido;
}
public function almacenar()
{
$this->validate();
Publicacion::create($this->only(['titulo', 'contenido']));
}
public function actualizar()
{
$this->validate();
$this->publicacion->update(
$this->all()
);
}
}

Como puedes ver, hemos añadido un método setPublicacion() al objeto PublicacionForm para permitir opcionalmente rellenar el formulario con datos existentes así como almacenar la entrada en el objeto formulario para su uso posterior. También hemos añadido un método actualizar() para actualizar la entrada existente.

Los objetos formulario no son necesarios cuando se trabaja con Livewire, pero ofrecen una buena abstracción para mantener sus componentes libres de repeticiones.

Restableciendo los campos del formulario

Si está utilizando un objeto de formulario, es posible que desee restablecer el formulario después de que se haya enviado. Esto se puede hacer llamando al método reset():

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Publicacion;
class PublicacionForm extends Form
{
public ?Publicacion $publicacion;
#[Validate('required|min:5')]
public $titulo = '';
#[Validate('required|min:5')]
public $contenido = '';
// ...
public function almacenar()
{
$this->validate();
Publicacion::create($this->all());
$this->reset();
}
}

También puede restablecer propiedades específicas pasando los nombres de las propiedades al método reset():

$this->reset('title');
// O varios a la vez...
$this->reset(['title', 'content']);

Extracción de campos de formulario

Alternativamente, puedes utilizar el método pull() para recuperar las propiedades de un formulario y restablecerlas en una sola operación.

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Publicacion;
class PublicacionForm extends Form
{
public ?Publicacion $publicacion;
#[Validate('required|min:5')]
public $titulo = '';
#[Validate('required|min:5')]
public $contenido = '';
// ...
public function almacenar()
{
$this->validate();
Publicacion::create($this->pull());
}
}

También puede extraer propiedades específicas pasando los nombres de las propiedades al método pull():

// Devuelve un valor antes de reiniciar...
$this->pull('title');
// Devuelve un array clave-valor de propiedades antes de reiniciar...
$this->pull(['title', 'content']);

Usando objetos Rule

Si tienes escenarios de validación más sofisticados donde los objetos Rule de Laravel son necesarios, puedes alternativamente definir un método rules() para declarar tus reglas de validación así:

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Publicacion;
class PublicacionForm extends Form
{
public ?Publicacion $publicacion;
public $titulo = '';
public $contenido = '';
protected function rules()
{
return [
'titulo' => [
'required',
Rule::unique('publicaciones')->ignore($this->publicacion),
],
'contenido' => 'required|min:5',
];
}
// ...
public function actualizar()
{
$this->validate();
$this->publicacion->update(
$this->all()
);
$this->reset();
}
}

Al utilizar un método rules() en lugar de #[Validate], Livewire sólo ejecutará las reglas de validación cuando llame a $this->validate(), en lugar de cada vez que se actualice una propiedad.

Si está utilizando validación en tiempo real o cualquier otro escenario en el que desee que Livewire valide campos específicos después de cada solicitud, puede utilizar #[Validate] sin ninguna regla proporcionada de esta manera:

<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Publicacion;
class PublicacionForm extends Form
{
public ?Publicacion $publicacion;
#[Validate]
public $titulo = '';
public $contenido = '';
protected function rules()
{
return [
'titulo' => [
'required',
Rule::unique('publicaciones')->ignore($this->publicacion),
],
'contenido' => 'required|min:5',
];
}
// ...
public function actualizar()
{
$this->validate();
$this->publicacion->update(
$this->all()
);
$this->reset();
}
}

Ahora si la propiedad $titulo se actualiza antes de que el formulario sea enviado-como cuando se usa wire:model.blur se ejecutará la validación para $titulo.

Mostrar un indicador de carga

Por defecto, Livewire deshabilitará automáticamente los botones de envío y marcará las entradas como readonly mientras se envía un formulario, evitando que el usuario envíe el formulario de nuevo mientras se gestiona el primer envío.

Sin embargo, puede ser difícil que los usuarios detecten este estado de «carga» sin elementos adicionales en la interfaz de usuario de su aplicación.

Aquí tienes un ejemplo de cómo añadir un pequeño spinner de carga al botón «Guardar» mediante wire:loading para que el usuario entienda que el formulario se está enviando:

<button type="submit">
Guardar
<div wire:loading>
<svg>...</svg> <!-- Spinner SVG de cargando... -->
</div>
</button>

Ahora, cuando un usuario pulse «Guardar», aparecerá un pequeño spinner en línea.

La función wire:loading de Livewire tiene mucho más que ofrecer. Visite la documentación de carga para obtener más información.

Campos actualizables en tiempo real

Por defecto, Livewire sólo envía una petición de red cuando se envía el formulario (o se llama a cualquier otra acción), no mientras se está rellenando el formulario.

Tome el componente CrearPublicacion, por ejemplo. Si quieres asegurarte de que el campo de entrada «titulo» se sincroniza con la propiedad $titulo en el backend a medida que el usuario escribe, puedes añadir el modificador .live a wire:model de esta manera:

<input type="text" wire:model.live="titulo">

Ahora, cuando un usuario escriba en este campo, se enviarán peticiones de red al servidor para actualizar $titulo. Esto es útil para cosas como una búsqueda en tiempo real, donde un conjunto de datos se filtra a medida que un usuario escribe en un cuadro de búsqueda.

Actualizar campos sólo al desenfocar(blur)

En la mayoría de los casos, wire:model.live está bien para actualizar campos de formulario en tiempo real; sin embargo, puede consumir demasiados recursos de red en las entradas de texto.

Si en lugar de enviar solicitudes de red a medida que el usuario escribe, quieres enviar la solicitud sólo cuando el usuario «salta» de la entrada de texto (también conocido como «difuminar (blurring)» una entrada), puedes utilizar el modificador .blur en su lugar:

<input type="text" wire:model.blur="titulo">

Ahora la clase del componente en el servidor no se actualizará hasta que el usuario pulse tabulador o haga clic fuera de la entrada de texto.

Validación en tiempo real

A veces, es posible que desee mostrar los errores de validación a medida que el usuario rellena el formulario. De esta forma, se les avisa antes de que algo va mal en lugar de tener que esperar hasta que se haya rellenado todo el formulario.

Livewire maneja este tipo de cosas automáticamente. Usando .live o .blur en wire:model, Livewire enviará peticiones de red a medida que el usuario rellene el formulario. Cada una de esas peticiones de red ejecutará las reglas de validación apropiadas antes de actualizar cada propiedad. Si la validación falla, la propiedad no se actualizará en el servidor y se mostrará un mensaje de validación al usuario:

<input type="text" wire:model.blur="titulo">
<div>
@error('titulo') <span class="error">{{ $message }}</span> @enderror
</div>
#[Validate('required|min:5')]
public $titulo = '';

Ahora, si el usuario sólo escribe tres caracteres en la entrada «título» y luego hace clic en la siguiente entrada del formulario, se le mostrará un mensaje de validación indicándole que hay un mínimo de cinco caracteres para ese campo.

Para más información, consulte la página de documentación sobre validación.

Guardar formularios en tiempo real

Si desea guardar automáticamente un formulario a medida que el usuario lo rellena en lugar de esperar hasta que el usuario haga clic en «enviar», puede hacerlo utilizando el hook updated() de Livewire:

<?php
namespace App\Livewire;
use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Models\Publicacion;
class ActualizarPublicacion extends Component
{
public Publicacion $publicacion;
#[Validate('required')]
public $titulo = '';
#[Validate('required')]
public $contenido = '';
public function mount(Publicacion $publicacion)
{
$this->publicacion = $publicacion;
$this->titulo = $publicacion->titulo;
$this->contenido = $publicacion->contenido;
}
public function updated($nombre, $valor)
{
$this->publicacion->update([
$nombre => $valor,
]);
}
public function render()
{
return view('livewire.actualizar-publicacion');
}
}
<form wire:submit>
<input type="text" wire:model.blur="titulo">
<div>
@error('titulo') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model.blur="contenido">
<div>
@error('contenido') <span class="error">{{ $message }}</span> @enderror
</div>
</form>

En el ejemplo anterior, cuando un usuario completa un campo (haciendo clic o tabulando al siguiente campo), se envía una petición de red para actualizar esa propiedad en el componente. Inmediatamente después de que la propiedad es actualizada en la clase, el hook updated() es llamado para ese nombre de propiedad específico y su nuevo valor.

Podemos usar este hook para actualizar sólo ese campo específico en la base de datos.

Además, como tenemos los atributos #[Validate] adjuntos a esas propiedades, las reglas de validación se ejecutarán antes de que se actualice la propiedad y se llame al hook updated().

Para saber más sobre el gancho de ciclo de vida «updated» y otros hooks, visita la documentación de hooks de ciclo de vida.

Indicadores de suciedad

En el escenario de guardado en tiempo real comentado anteriormente, puede ser útil indicar a los usuarios cuándo un campo aún no se ha guardado en la base de datos.

Por ejemplo, si un usuario visita una página ActualizarPublicacion y empieza a modificar el título de la entrada en un campo de texto, puede que no tenga claro cuándo se está actualizando el título en la base de datos, especialmente si no hay un botón «Guardar» al final del formulario.

Livewire proporciona la directiva wire:dirty para permitirle alternar elementos o modificar clases cuando el valor de una entrada difiere del componente del lado del servidor:

<input type="text" wire:model="titulo">
<div wire:dirty wire:target="titulo">Sin guardar...</div>

Descontar la entrada

Al utilizar .live en una entrada de texto, es posible que desee un control más preciso sobre la frecuencia con la que se envía una solicitud de red. Por defecto, se aplica un «debounce» de «250ms» a la entrada; sin embargo, puede personalizarlo utilizando el modificador .debounce:

<input type="text" wire:model.live.debounce.150ms="titulo" >

Ahora que se ha añadido .debounce.150ms al campo, se utilizará un debounce más corto de «150ms» cuando se manejen actualizaciones de entrada para este campo. En otras palabras, cuando un usuario escriba, sólo se enviará una solicitud de red si el usuario deja de escribir durante al menos 150 milisegundos.

Limitación de la entrada

Como se ha indicado anteriormente, cuando se aplica una limitación de entrada a un campo, no se enviará una solicitud de red hasta que el usuario haya dejado de escribir durante un cierto tiempo. Esto significa que si el usuario continúa escribiendo un mensaje largo, no se enviará una petición de red hasta que el usuario termine.

A veces este no es el comportamiento deseado, y preferirías enviar una petición mientras el usuario escribe, no cuando ha terminado o se ha tomado un descanso.

En estos casos, puedes usar .throttle para indicar un intervalo de tiempo para enviar peticiones de red:

<input type="text" wire:model.live.throttle.150ms="titulo" >

En el ejemplo anterior, como el usuario está escribiendo continuamente en el campo «título», se enviará una petición de red cada 150 milisegundos hasta que el usuario termine.

Extracción de campos de entrada a componentes Blade

Incluso en un componente pequeño como el ejemplo de CrearPublicacion que hemos estado comentando, acabamos duplicando un montón de repeticiones de campos de formulario, como mensajes de validación y etiquetas.

Puede ser útil extraer elementos de interfaz de usuario repetitivos como estos en componentes Blade dedicados para compartirlos en toda la aplicación.

Por ejemplo, a continuación se muestra la plantilla Blade original del componente CrearPublicacion. Extraeremos las siguientes dos entradas de texto en componentes Blade dedicados:

<form wire:submit="guardar">
<input type="text" wire:model="titulo">
<div>
@error('titulo') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model="contenido">
<div>
@error('contenido') <span class="error">{{ $message }}</span> @enderror
</div>
<button type="submit">Guardar</button>
</form>

Este es el aspecto que tendrá la plantilla después de extraer un componente reutilizable de Blade llamado <x-input-text>:

<form wire:submit="guardar">
<x-input-text name="titulo" wire:model="titulo" />
<x-input-text name="contenido" wire:model="contenido" />
<button type="submit">Guardar</button>
</form>

A continuación, aquí está la fuente para el componente x-input-text:

resources/views/components/input-text.blade.php
@props(['nombre'])
<input type="text" name="{{ $nombre }}" {{ $atributos }}>
<div>
@error($nombre) <span class="error">{{ $message }}</span> @enderror
</div>

Como puede ver, tomamos el HTML repetitivo y lo colocamos dentro de un componente Blade dedicado.

En su mayor parte, el componente Blade contiene sólo el HTML extraído del componente original. Sin embargo, hemos añadido dos cosas:

  • La directiva @props
  • La sentencia {{ $atributos }} en la entrada

Vamos a comentar cada una de estas adiciones:

Al especificar nombre como una «prop» usando @props(['nombre']) le estamos diciendo a Blade: si un atributo llamado «nombre» está establecido en este componente, toma su valor y hazlo disponible dentro de este componente como $nombre.

Para otros atributos que no tienen un propósito explícito, utilizamos la sentencia {{ $atributos }}. Se utiliza para el «reenvío de atributos» o, en otras palabras, para tomar cualquier atributo HTML escrito en el componente Blade y reenviarlo a un elemento dentro del componente.

Esto asegura que wire:model="titulo" y cualquier otro atributo extra como disabled, class="...", o required se envíen al elemento <input>.

Controles de formulario personalizados

En el ejemplo anterior, hemos «envuelto» un elemento de entrada en un componente Blade reutilizable que podemos utilizar como si fuera un elemento de entrada HTML nativo.

Este patrón es muy útil; sin embargo, puede haber algunos casos en los que desee crear un componente de entrada completo desde cero (sin un elemento de entrada nativo subyacente), pero aún así ser capaz de vincular su valor a las propiedades Livewire utilizando wire:model.

Por ejemplo, imaginemos que desea crear un componente <x-input-counter /> que sea un simple «contador» de entrada escrito en Alpine.

Antes de crear un componente Blade, veamos primero un componente «contador» simple, puramente Alpine, como referencia:

<div x-data="{ contar: 0 }">
<button x-on:click="contar--">-</button>
<span x-text="contar"></span>
<button x-on:click="contar++">+</button>
</div>

Como puede ver, el componente anterior muestra un número junto a dos botones para incrementar y decrementar ese número.

Ahora, imaginemos que queremos extraer este componente en un componente Blade llamado <x-input-counter /> que usaríamos dentro de un componente como este:

<x-input-counter wire:model="cantidad" />

Crear este componente es muy sencillo. Tomamos el HTML del contador y lo colocamos dentro de una plantilla de componente Blade como resources/views/components/input-counter.blade.php.

Sin embargo, hacer que funcione con wire:model="cantidad" para que pueda vincular fácilmente los datos de su componente Livewire a la «cuenta» dentro de este componente Alpine necesita un paso extra.

Aquí está la fuente para el componente:

resources/view/components/input-counter.blade.php
<div x-data="{ contador: 0 }" x-modelable="contador" {{ $atributos}}>
<button x-on:click="contador--">-</button>
<span x-text="contador"></span>
<button x-on:click="contador++">+</button>
</div>

Como puedes ver, lo único diferente de este HTML es el x-modelable=contador y {{ $atributos }}.

x-modelable es una utilidad de Alpine que le dice a Alpine que haga que un determinado dato esté disponible para ser enlazado desde fuera. La documentación de Alpine tiene más información sobre esta directiva.

{{ $atributos }}, como hemos explorado antes, reenvía cualquier atributo pasado al componente Blade desde fuera. En este caso, se reenviará la directiva wire:model.

Debido a {{ $atributos }}, cuando el HTML se renderice en el navegador, wire:model="contador" se renderizará junto a x-modelable="contador" en la raíz <div> del componente Alpine de esta forma:

<div x-data="{ contador: 0 }" x-modelable="contador" wire:model="cantidad">

x-modelable="contador" indica a Alpine que busque cualquier sentencia x-model o wire:model y utilice “contador” como dato para enlazarlas.

Dado que x-modelable funciona tanto para wire:model como para x-model, también puede utilizar este componente Blade indistintamente con Livewire y Alpine. Por ejemplo, aquí hay un ejemplo de uso de este componente Blade en un contexto puramente Alpine:

<x-input-counter x-model="cantidad" />

La creación de elementos de entrada personalizados en su aplicación es extremadamente potente, pero requiere una comprensión más profunda de las utilidades que Livewire y Alpine proporcionan y cómo interactúan entre sí.