Livewire a realtime vyhledání
V tomto článku si na jednoduchém příkladu ukážeme sílu Laravel Livewire. Náš projekt bude obsahovat realtime vyhledávání článků podle jejich názvu.
Než začneme, tak se pokusím vysvětlit co vlastně Livewire je a k čemu slouží. Livewire je fullstackový framework pro Laravel, který založil Američan Caleb Porzio. Jedná se o knihovnu, díky které je možné vytvářet moderní, reaktivní a dynamické aplikace a zároveň stále používáte šablony Blade a Laravel jako backend. Vue a React jsou extrémně silní hráči, nicméně pro fullstack vývojáře může být jejich komplexita a workflow velmi náročná. Livewire tak slouží k vytváření dynamických a reaktivních aplikací, aniž by jste museli napsat jediný řádek JavaScript kódu.
Příprava dat
Jako obvykle si prvně musíme připravit nějaké dummy data. Budu předpokládat, že již máte připravené připojení k databázi a vytváření seedu vám není cizí. Proto to vezmu trochu rychleji bez většího okomentování.
Úplně na začátku si vytvoříme nový projekt:
laravel new livewire-example
A v něm si vytvoříme naráz model, factory, seed a controller pomocí artisan flagu -a
:
php artisan make:model Post -a
Model created successfully.
Factory created successfully.
Created Migration: 2020_10_16_140818_create_posts_table
Seeder created successfully.
Controller created successfully.
Upravíme si migraci s články:
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->string('author');
$table->timestamps();
});
}
Připravíme si faktorku:
<?php
namespace Database\Factories;
use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Post::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'title' => $this->faker->sentence,
'content' => $this->faker->paragraph(),
'author' => $this->faker->name
];
}
}
V seedu si vytvoříme třeba 100 článků:
<?php
namespace Database\Seeders;
use App\Models\Post;
use Illuminate\Database\Seeder;
class PostSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
Post::factory(100)->create();
}
}
A seeder zavoláme v hlavním seedu:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(PostSeeder::class);
}
}
A nakonec zavoláme migrace i se seedem. Tím máme data připravena.
php artisan migrate:fresh --seed
Dropped all tables successfully.
Migration table created successfully.
Migrating: 2014_10_12_000000_create_users_table
Migrated: 2014_10_12_000000_create_users_table (66.26ms)
Migrating: 2014_10_12_100000_create_password_resets_table
Migrated: 2014_10_12_100000_create_password_resets_table (60.74ms)
Migrating: 2019_08_19_000000_create_failed_jobs_table
Migrated: 2019_08_19_000000_create_failed_jobs_table (62.94ms)
Migrating: 2020_10_16_140818_create_posts_table
Migrated: 2020_10_16_140818_create_posts_table (17.37ms)
Seeding: Database\Seeders\PostSeeder
Seeded: Database\Seeders\PostSeeder (135.00ms)
Database seeding completed successfully.
Serverové vyhledávání
Jako první si ukážeme, jak bychom to řešili klasicky s požadavkem na server a následném obnovení stránky s výsledky. Následně si to upravíme pro Livewire.
Na začátku si definujeme dvě routy, jedna bude výchozí a zobrazovat všechny články, druhá bude zobrazovat výsledky vyhledávání. Upravíme si tedy soubor routes/web.php
:
<?php
use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;
Route::get('/', [PostController::class, 'index']);
Route::get('/hledej', [PostController::class, 'search'])->name('search');
Dříve jsme si již vytvořili controller PostController
, který ale obsahuje všechny resource metody. Upravíme si tedy třídu aby obsahovala pouze dvě metody, které budeme potřebovat:
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index(): View
{
return view('search', [
'posts' => Post::all()
]);
}
public function search(Request $request): View
{
return view('search', [
'posts' => Post::query()->where('title', 'like', '%' . $request->input('search') . '%')->get()
]);
}
}
Obě metody vracejí Blade pohled s články. Vytvoříme si tedy novou šablonu resources/search.blade.php
a v ní zobrazíme jednoduchý výpis článků:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
<div class="w-full max-w-screen-xl mx-auto px-6">
<div class="flex justify-center p-4 px-3 py-10">
<div class="w-full max-w-md">
<div class="bg-white shadow-md rounded-lg px-3 py-2 mb-4">
<div class="block text-gray-700 text-lg font-semibold py-2 px-2">
Seznam článků
</div>
<form method="get" action="{{ route('search') }}">
<div class="flex items-center bg-gray-200 rounded-md">
<div class="pl-2">
<svg class="fill-current text-gray-500 w-6 h-6" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path class="heroicon-ui"
d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/>
</svg>
</div>
<input
class="w-full rounded-md bg-gray-200 text-gray-700 leading-tight focus:outline-none py-2 px-2"
id="search" name="search" type="text" placeholder="Hledej..." value="{{ request()->input('search') }}">
</div>
</form>
<div class="overflow-x-scroll py-3 text-sm" style="height: 40rem">
@foreach($posts as $post)
<div class="flex justify-start cursor-pointer text-gray-700 hover:text-blue-400 hover:bg-blue-100 rounded-md px-2 py-2 my-2">
<div class="w-4/6 font-medium px-2">{{ $post->title }}</div>
<div class="w-2/6 text-sm font-normal text-gray-500 tracking-wide">{{ $post->author
}}</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Pro zjednodušení jsem nepřidal žádné tlačítko, pro vyhledávání se tak bude používat enter
(alespoň do té doby než přidáme Livewire).
Tím bychom měli serverové vyhledávání hotové. Není v tom žádná magie. Nyní přichází ta zajímavější část, a to, že si to upravíme pro Livewire.
Livewire realtime vyhledávání
Jako první je potřeba Livewire pomocí composeru nainstalovat:
composer require livewire/livewire
Livewire je založen na komponentách, kde každá komponenta má svůj vlastní životní cyklus a je nezávislá na ostatních komponentách. Proto si vytvoříme novou komponentu pro naše vyhledávání pomocí speciálního artisan příkazu, který Livewire používá:
php artisan make:livewire search
COMPONENT CREATED 🤙
CLASS: app/Http/Livewire/Search.php
VIEW: resources/views/livewire/search.blade.php
Artisan vytvořil dva soubory - třídu s komponentou a blade, který bude využívat. Jako první si upravíme komponentu:
<?php
namespace App\Http\Livewire;
use App\Models\Post;
use Illuminate\Database\Eloquent\Builder;
use Livewire\Component;
class Search extends Component
{
public string $search = '';
public function render()
{
return view('livewire.search', [
'posts' => Post::query()->when($this->search, function (Builder $query) {
$query->where('title', 'like', '%' . $this->search . '%');
})->get(),
]);
}
}
Livewire vychází z podobného schématu jako je Vue.js. Tedy definuje si určité modely, které si drží aktuální stav. Jako první model jsme si nadefinovali proměnnou search
. Dále třída obsahuje metodu render()
, která zodpovídá za vygenerování šablony. Celý proces je stejný jako bychom využívali klasicky jenom Laravel, změna je pouze v tom, že namísto requestu využíváme náš model (resp. proměnnou třídy). Tedy vrátíme šablonu a s ní data, které hledáme (pokud máme vůbec něco hledat).
Dále se vrhneme na samotnou šablonu v resources/livewire/search.blade
. Zde zkopírujeme obsah z původní search.blade
šablony a trochu si ji upravíme:
<div>
<div class="w-full max-w-screen-xl mx-auto px-6">
<div class="flex justify-center p-4 px-3 py-10">
<div class="w-full max-w-md">
<div class="bg-white shadow-md rounded-lg px-3 py-2 mb-4">
<div class="block text-gray-700 text-lg font-semibold py-2 px-2">
Seznam článků
</div>
<div class="flex items-center bg-gray-200 rounded-md">
<div class="pl-2">
<svg class="fill-current text-gray-500 w-6 h-6" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24">
<path class="heroicon-ui"
d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/>
</svg>
</div>
<input
class="w-full rounded-md bg-gray-200 text-gray-700 leading-tight focus:outline-none py-2 px-2"
wire:model="search" id="search" name="search" type="text" placeholder="Hledej...">
</div>
<div class="overflow-x-scroll py-3 text-sm" style="height: 40rem">
@foreach($posts as $post)
<div class="flex justify-start cursor-pointer text-gray-700
hover:text-blue-400 hover:bg-blue-100 rounded-md px-2 py-2 my-2">
<div class="w-4/6 font-medium px-2">{{ $post->title }}</div>
<div class="w-2/6 text-sm font-normal text-gray-500 tracking-wide">{{ $post->author
}}</div>
</div>
@endforeach
</div>
</div>
</div>
</div>
</div>
</div>
Na první pohled se může zdát úplně stejná, nicméně je tam několik rozdílů:
- Jako první si všimněte, že samotná šablona neobsahuje žádné tagy
html
,header
,body
atd. To proto, že za chvíli si upravíme původnísearch.blade.php
, aby zavolala naší komponentu. Z toho důvodu není potřeba nic dalšího doplňovat, protože zde funguje klasická dědičnost jak jsme u Blade šablon zvyklí. Je potřeba si dát ale pozor, že i když zde dědičnost funguje, tak komponenta není schopna jednoduše převzít data z rodiče a museli by se v rodiči definovat. Více o tom ale zjistíte v oficiální dokumentaci Livewire. - Dále je potřeba mít na paměti, že každá Livewire komponenta musí mít pouze jeden hlavní kořen. Z toho důvodu jsem pro jistotu celý obsah ještě obalil do samostatného divu.
- Protože už vyhledáváme realtime, tak nepotřebujeme žádný formulář
- Všimněte si u vyhledávacího inputu nejdůležitějšího atributu
wire:model="search"
. Právě tento atribut je odpovědný za to, že se v komponentně naváže na model$search
.
Nakonec aby to celé fungovalo, tak je potřeba někde zavolat tuto komponentu. To provedeme v původní resources/search.blade.php
komponentě, kterou si upravíme takto:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
@livewireStyles
</head>
<body>
<livewire:search />
@livewireScripts
</body>
</html>
Aby mohl Livewire fungovat, je potřeba vždy přidat styly a javascripty, které bude Livewire používat. A to pomocí helperů @livewireStyles
a @livewireScripts
. A nakonec zavoláme Livewire komponentu search
.
Jak to celé funguje
Prakticky to funguje velmi jednoduše. Cokoliv napíšeme do inputu, automaticky se pošle AJAX na server, konkretně do komponenty Search. Ta do modelu komponenty definovaného v šabloně (a poté v requestu) propíše obsah a vrátí zase odpověď z render()
metody. Vlastně se tak pregenruje celá komponenta, ale pouze ona, zbytek stránek zůstane nedotčen.
Nakonec bych rád zmínil ještě jednu z mnoha věcí, které Laravel Livewire nabízí. Mějme příklad, kdy máme tisíce (či miliony) záznamů. V tom případě posílat požadavek na server při každém zadání znaku do vyhledávání nebude zrovna ideální. Možností je upravit input takto:
<input class="w-full rounded-md bg-gray-200 text-gray-700 leading-tight focus:outline-none py-2 px-2" wire:model.debounce.500ms="search" id="search" name="search" type="text" placeholder="Hledej...">
Díky metodě debounce
je možné odpověď "zpomalit", konkrétně se požadavek nepošle hned, ale až 500ms po skončení psaní.
A nakonec zde je demo na Laravel Playground. Kód jsem trochu upravil, aby data bral z pole namísto z databáze.
Máte dotazy, něco vám není jasné nebo mám někde chybu? Napište mi do komentářů!