Sobre nós Guias Projetos Contactos
Админка
please wait

O Laravel Livewire permite criar interfaces dinâmicas e reativas usando PHP em vez de JavaScript. Oferece a reatividade do Vue ou do React, mantendo o seu código em templates Blade familiares. Este guia aborda a construção de UIs modernas e interativas com Livewire, na perspetiva de um programador sénior.

Porquê Livewire

O Livewire oferece vantagens convincentes:

  1. Apenas PHP: Crie UIs reativas sem escrever JavaScript
  2. Integração com Blade: Use a templating do Laravel que já conhece
  3. Lógica no Lado do Servidor: Mantenha a lógica de negócio no servidor
  4. Atualizações em Tempo Real: Diffing e atualizações automáticas do DOM
  5. Ecossistema Laravel: Funciona de forma integrada com aplicações Laravel existentes

Instalação

Instale o Livewire via Composer:

composer require livewire/livewire

Os assets do Livewire são injetados automaticamente. Para controlo manual, adicione ao seu layout:

<html>
<head>
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>

Criação de Componentes

Gerar um Componente

php artisan make:livewire DataTable

Isto cria dois ficheiros:

  • app/Http/Livewire/DataTable.php - Classe do componente
  • resources/views/livewire/data-table.blade.php - Template

Estrutura do Componente

Classe do componente app/Http/Livewire/DataTable.php:

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithPagination;
use App\Models\User;
class DataTable extends Component
{
use WithPagination;
// As propriedades públicas são automaticamente reativas.
public $search = '';
public $status = 'all';
public $sortField = 'created_at';
public $sortDirection = 'desc';
// Repor a paginação quando os filtros mudam.
protected $queryString = [
'search' => ['except' => ''],
'status' => ['except' => 'all'],
];
public function updatingSearch()
{
$this->resetPage();
}
public function updatingStatus()
{
$this->resetPage();
}
public function sortBy($field)
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
}
public function render()
{
$users = User::query()
->when($this->search, function ($query) {
$query->where(function ($q) {
$q->where('name', 'like', "%{$this->search}%")
->orWhere('email', 'like', "%{$this->search}%");
});
})
->when($this->status !== 'all', function ($query) {
$query->where('status', $this->status);
})
->orderBy($this->sortField, $this->sortDirection)
->paginate(10);
return view('livewire.data-table', [
'users' => $users,
]);
}
}

Template resources/views/livewire/data-table.blade.php:

<div>
<div class="filters mb-4 flex gap-4">
<input
type="text"
wire:model.debounce.300ms="search"
placeholder="Search users..."
class="form-control"
>
<select wire:model="status" class="form-control">
<option value="all">All Statuses</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
</div>
<table class="table">
<thead>
<tr>
<th wire:click="sortBy('name')" class="cursor-pointer">
Name
@if($sortField === 'name')
@if($sortDirection === 'asc') ↑ @else ↓ @endif
@endif
</th>
<th wire:click="sortBy('email')" class="cursor-pointer">
Email
</th>
<th wire:click="sortBy('created_at')" class="cursor-pointer">
Created
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->email }}</td>
<td>{{ $user->created_at->format('M d, Y') }}</td>
<td>
<button wire:click="edit({{ $user->id }})" class="btn btn-sm btn-primary">
Edit
</button>
<button
wire:click="delete({{ $user->id }})"
wire:confirm="Are you sure you want to delete this user?"
class="btn btn-sm btn-danger"
>
Delete
</button>
</td>
</tr>
@empty
<tr>
<td colspan="4" class="text-center">No users found.</td>
</tr>
@endforelse
</tbody>
</table>
{{ $users->links() }}
</div>

Utilizar Componentes

Em qualquer view Blade:

<livewire:data-table />
{{-- Or with parameters --}}
<livewire:data-table :status="'active'" />

Tratamento de Formulários

Criar Componente de Formulário

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Models\Post;
class PostForm extends Component
{
public Post $post;
protected $rules = [
'post.title' => 'required|min:3|max:255',
'post.body' => 'required|min:10',
'post.published' => 'boolean',
];
protected $messages = [
'post.title.required' => 'The post title is required.',
'post.title.min' => 'The title must be at least 3 characters.',
];
public function mount(Post $post = null)
{
$this->post = $post ?? new Post();
}
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function save()
{
$this->validate();
$isNew = !$this->post->exists;
$this->post->user_id = auth()->id();
$this->post->save();
session()->flash('message', $isNew
? 'Post created successfully!'
: 'Post updated successfully!'
);
return redirect()->route('posts.index');
}
public function render()
{
return view('livewire.post-form');
}
}

Template:

<form wire:submit.prevent="save">
@if (session()->has('message'))
<div class="alert alert-success">
{{ session('message') }}
</div>
@endif
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
id="title"
wire:model="post.title"
class="form-control @error('post.title') is-invalid @enderror"
>
@error('post.title')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="body">Content</label>
<textarea
id="body"
wire:model.lazy="post.body"
rows="10"
class="form-control @error('post.body') is-invalid @enderror"
></textarea>
@error('post.body')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-check">
<input
type="checkbox"
id="published"
wire:model="post.published"
class="form-check-input"
>
<label for="published" class="form-check-label">Published</label>
</div>
<button type="submit" class="btn btn-primary" wire:loading.attr="disabled">
<span wire:loading.remove>Save Post</span>
<span wire:loading>Saving...</span>
</button>
</form>

Comunicação por Eventos

Emitir Eventos

Os componentes podem comunicar através de eventos:

// No componente filho.
public function selectItem($id)
{
$this->emit('itemSelected', $id);
}
// Emitir apenas para o componente pai.
$this->emitUp('itemSelected', $id);
// Emitir para um componente específico.
$this->emitTo('shopping-cart', 'itemAdded', $productId);

Escutar Eventos

// No componente pai.
protected $listeners = [
'itemSelected' => 'handleItemSelected',
'refreshData' => '$refresh', // Especial: re-renderizar o componente.
];
public function handleItemSelected($id)
{
$this->selectedItem = Item::find($id);
}

JavaScript para Livewire

Emita eventos a partir de JavaScript:

// Emitir para todos os componentes Livewire.
Livewire.emit('filterUpdated', filterValue);
// Emitir para um componente específico.
Livewire.emitTo('data-table', 'filterUpdated', filterValue);

Escute no componente:

protected $listeners = [
'filterUpdated' => 'setFilter',
];
public function setFilter($value)
{
$this->filter = $value;
}

Estados de Carregamento

Diretiva wire:loading

{{-- Show/hide elements --}}
<span wire:loading>Processing...</span>
<span wire:loading.remove>Ready</span>
{{-- Target specific actions --}}
<span wire:loading wire:target="save">Saving post...</span>
<span wire:loading wire:target="delete">Deleting...</span>
{{-- Target multiple actions --}}
<div wire:loading wire:target="save, update, delete">
Working...
</div>
{{-- CSS classes --}}
<button wire:loading.class="opacity-50" wire:target="save">
Save
</button>
{{-- Disable elements --}}
<button wire:loading.attr="disabled">Submit</button>

Componente de Indicador de Carregamento

Crie um overlay de carregamento reutilizável:

{{-- resources/views/livewire/partials/loading.blade.php --}}
<div
wire:loading.flex
class="fixed inset-0 bg-black bg-opacity-25 items-center justify-center z-50"
>
<div class="bg-white p-4 rounded-lg shadow-lg">
<svg class="animate-spin h-8 w-8 text-blue-500" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" fill="none" />
</svg>
</div>
</div>

Uploads de Ficheiros

Ativar Uploads de Ficheiros

<?php
namespace App\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class ImageUpload extends Component
{
use WithFileUploads;
public $photo;
public $photos = [];
protected $rules = [
'photo' => 'image|max:1024', // Máximo de 1 MB.
'photos.*' => 'image|max:1024',
];
public function updatedPhoto()
{
$this->validate([
'photo' => 'image|max:1024',
]);
}
public function save()
{
$this->validate();
// Guardar um único ficheiro.
$path = $this->photo->store('photos', 'public');
// Guardar vários ficheiros.
foreach ($this->photos as $photo) {
$photo->store('photos', 'public');
}
session()->flash('message', 'Photos uploaded!');
$this->reset(['photo', 'photos']);
}
public function render()
{
return view('livewire.image-upload');
}
}

Template:

<form wire:submit.prevent="save">
{{-- Single file --}}
<div class="form-group">
<label>Single Photo</label>
<input type="file" wire:model="photo">
@error('photo')
<span class="text-red-500">{{ $message }}</span>
@enderror
{{-- Preview --}}
@if ($photo)
<img src="{{ $photo->temporaryUrl() }}" class="mt-2 max-w-xs">
@endif
<div wire:loading wire:target="photo">Uploading...</div>
</div>
{{-- Multiple files --}}
<div class="form-group">
<label>Multiple Photos</label>
<input type="file" wire:model="photos" multiple>
@if ($photos)
<div class="flex gap-2 mt-2">
@foreach($photos as $photo)
<img src="{{ $photo->temporaryUrl() }}" class="w-20 h-20 object-cover">
@endforeach
</div>
@endif
</div>
<button type="submit" wire:loading.attr="disabled">
Upload Photos
</button>
</form>

Validação em Tempo Real

Feedback Imediato

public function updated($propertyName)
{
$this->validateOnly($propertyName);
}

Input com Debounce

{{-- Validate 500ms after user stops typing --}}
<input type="text" wire:model.debounce.500ms="email">
{{-- Validate only on blur --}}
<input type="text" wire:model.lazy="email">
{{-- Immediate validation (default) --}}
<input type="text" wire:model="email">

Polling e Atualização Automática

Polling Automático

{{-- Refresh every 2 seconds --}}
<div wire:poll.2s>
Current time: {{ now() }}
</div>
{{-- Poll only when tab is visible --}}
<div wire:poll.visible.5s>
{{ $notifications->count() }} notifications
</div>
{{-- Poll a specific method --}}
<div wire:poll.10s="refreshData">
{{ $data }}
</div>

Integração com Alpine.js

O Livewire combina de forma excelente com Alpine.js para interatividade no lado do cliente:

<div
x-data="{ open: false }"
@item-selected.window="open = false"
>
<button @click="open = !open">Toggle Menu</button>
<div x-show="open" x-cloak>
<ul>
@foreach($items as $item)
<li>
<button wire:click="selectItem({{ $item->id }})">
{{ $item->name }}
</button>
</li>
@endforeach
</ul>
</div>
</div>

Bibliotecas Úteis

Melhore o Livewire com estes packages:

  • Filament: Framework completo de painel de administração construído sobre Livewire
  • WireUI: Biblioteca de componentes de UI para Livewire
  • Volt: Componentes Livewire de ficheiro único
# Instalar o Filament.
composer require filament/filament
# Instalar o WireUI.
composer require wireui/wireui

Principais Conclusões

  1. As propriedades públicas são reativas: Qualquer alteração a uma propriedade pública desencadeia um re-render
  2. Use wire:model com critério: Escolha entre atualizações imediatas, com debounce, ou lazy
  3. Tire partido de eventos: Desacople componentes com comunicação orientada a eventos
  4. Mostre estados de carregamento: Indique sempre quando as operações estão em curso
  5. Valide em tempo real: Use validateOnly() para feedback imediato
  6. Combine com Alpine.js: Use Alpine para interações puramente no lado do cliente

O Livewire permite criar interfaces sofisticadas e reativas, mantendo toda a lógica em PHP, o que o torna perfeito para programadores Laravel que querem UIs modernas sem uma framework JavaScript.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive