О нас Руководства Проекты Контакты
Админка
пожалуйста подождите

Laravel Livewire позволяет создавать динамические, реактивные интерфейсы на PHP вместо JavaScript. Он обеспечивает реактивность уровня Vue или React, при этом сохраняя ваш код в привычных Blade-шаблонах. В этом руководстве рассматривается создание современных интерактивных UI с Livewire с точки зрения senior-разработчика.

Почему Livewire

Livewire предлагает убедительные преимущества:

  1. Только PHP: создавайте реактивные UI без написания JavaScript
  2. Интеграция с Blade: используйте знакомый шаблонизатор Laravel
  3. Логика на стороне сервера: держите бизнес-логику на сервере
  4. Обновления в реальном времени: автоматический diff DOM и обновления
  5. Экосистема Laravel: бесшовно работает с существующими приложениями Laravel

Установка

Установите Livewire через Composer:

composer require livewire/livewire

Assets Livewire автоматически подключаются. Для ручного управления добавьте в ваш layout:

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

Создание компонентов

Сгенерировать компонент

php artisan make:livewire DataTable

Будут созданы два файла:

  • app/Http/Livewire/DataTable.php — класс компонента
  • resources/views/livewire/data-table.blade.php — шаблон

Структура компонента

Класс компонента 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;
// Public properties автоматически являются реактивными
public $search = '';
public $status = 'all';
public $sortField = 'created_at';
public $sortDirection = 'desc';
// Сбрасывайте пагинацию при изменении фильтров
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,
]);
}
}

Шаблон 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>

Использование компонентов

В любом Blade-view:

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

Обработка форм

Создать компонент формы

<?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');
}
}

Шаблон:

<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>

Обмен событиями

Отправка событий

Компоненты могут взаимодействовать через события:

// В дочернем компоненте
public function selectItem($id)
{
$this->emit('itemSelected', $id);
}
// Отправлять событие только родителю
$this->emitUp('itemSelected', $id);
// Отправлять событие в конкретный компонент
$this->emitTo('shopping-cart', 'itemAdded', $productId);

Прослушивание событий

// В родительском компоненте
protected $listeners = [
'itemSelected' => 'handleItemSelected',
'refreshData' => '$refresh', // Специально: перерендерить компонент
];
public function handleItemSelected($id)
{
$this->selectedItem = Item::find($id);
}

JavaScript → Livewire

Отправляйте события из JavaScript:

// Отправлять событие во все компоненты Livewire
Livewire.emit('filterUpdated', filterValue);
// Отправлять событие в конкретный компонент
Livewire.emitTo('data-table', 'filterUpdated', filterValue);

Прослушивание в компоненте:

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

Состояния загрузки

Директива 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>

Компонент индикатора загрузки

Создайте переиспользуемый overlay загрузки:

{{-- 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>

Загрузка файлов

Включить загрузку файлов

<?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', // Максимум 1 MB
'photos.*' => 'image|max:1024',
];
public function updatedPhoto()
{
$this->validate([
'photo' => 'image|max:1024',
]);
}
public function save()
{
$this->validate();
// Сохранить один файл
$path = $this->photo->store('photos', 'public');
// Сохранить несколько файлов
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');
}
}

Шаблон:

<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>

Валидация в реальном времени

Мгновенная обратная связь

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

Input с 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 и автообновление

Автоматический polling

{{-- 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>

Интеграция с Alpine.js

Livewire отлично сочетается с Alpine.js для интерактивности на стороне клиента:

<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>

Полезные библиотеки

Расширьте возможности Livewire с помощью этих пакетов:

  • Filament: полноценный framework для admin panel, построенный на Livewire
  • WireUI: библиотека UI-компонентов для Livewire
  • Volt: single-file компоненты Livewire
# Установить Filament
composer require filament/filament
# Установить WireUI
composer require wireui/wireui

Ключевые выводы

  1. Public properties реактивны: любое изменение public property запускает повторный рендер
  2. Используйте wire:model осознанно: выбирайте между мгновенными, debounced или lazy обновлениями
  3. Используйте события: развязывайте компоненты с помощью event-driven взаимодействия
  4. Показывайте состояния загрузки: всегда обозначайте, когда операции выполняются
  5. Валидируйте в реальном времени: используйте validateOnly() для мгновенной обратной связи
  6. Комбинируйте с Alpine.js: используйте Alpine для чисто client-side взаимодействий

Livewire позволяет создавать сложные реактивные интерфейсы, сохраняя всю логику на PHP, что делает его идеальным выбором для разработчиков Laravel, которым нужны современные UI без JavaScript-фреймворков.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
Зетка Интерактив