1. מה זה Livewire
Livewire הוא framework מסוג full-stack ל-Laravel שמאפשר לבנות ממשקים דינמיים ב-PHP וב-Blade בלי לצאת מה-backend. לא צריך לכתוב SPA נפרד, לא צריך להקים שכבת REST או GraphQL, ולא צריך לתחזק מודל נתונים שני ב-JavaScript.
איך זה עובד
קומפוננטת Livewire היא מחלקת PHP ועוד view של Blade. בטעינה הראשונה של העמוד הקומפוננטה מרונדרת בצד השרת כ-markup רגיל. מכאן והלאה, runtime קטן ב-JS מיירט את פעולות המשתמש (click, input, submit) ושולח בקשת AJAX עם ה-state הנוכחי של הקומפוננטה; השרת מרנדר מחדש את הקומפוננטה ומחזיר markup עדכני. ה-runtime משווה בין עץ ה-DOM הישן לחדש ומעדכן רק את מה שהשתנה — העמוד לא נטען מחדש אף פעם.
Browser Server
│ │
│ click on wire:click="save" │
├───────── POST /livewire/update ┤
│ { snapshot, calls, updates } │
│ ├── restore component from snapshot
│ ├── apply property updates
│ ├── call save()
│ ├── run render()
│ { snapshot, html, effects } │
├◄───────────────────────────────┤
├── morph the DOM (diffs only) │
▼ ▼מתי Livewire הוא הבחירה הנכונה
- פאנלי ניהול, מערכות CRM, דשבורדים, כלים פנימיים — הרבה טפסים וטבלאות, מעט אנימציה מורכבת.
- טפסים עם שדות תלויים, אשפים מרובי שלבים, ולידציה בזמן אמת.
- טבלאות עם חיפוש, סינון, מיון ועימוד.
- צוות חזק ב-PHP שלא רוצה לתחזק stack נפרד ל-frontend.
- שחרור פיצ'רים מהר בלי לשכפל לוגיקה בין PHP ל-JS.
מתי לבחור במשהו אחר
- ממשקים שמתעדכנים ללא הפסקה: עורכים אונליין, canvas, בוני drag-and-drop, משחקים.
- אפליקציות offline-first ו-PWA עם סנכרון מקומי מורכב.
- אפליקציות מובייל — הן צריכות API, לא markup שעובר ברשת.
- חיבורים חלשים או עם latency גבוה: כל מחזור הוא הלוך-חזור ברשת.
גרסאות
התיעוד הזה מכסה את Livewire 3.x. דרישות: PHP 8.1+, Laravel 10 ומעלה.
גרסה 3 כוללת את Alpine.js מובנה, שינתה את שמות ה-API של האירועים (dispatch במקום
emit), הוסיפה PHP attributes (#[Computed], #[Validate],
#[Url]), והפכה את wire:model לדחוי כברירת מחדל.
2. התקנה והגדרה
התקנה בפרויקט Laravel קיים היא פקודה אחת.
composer require livewire/livewire
ב-Livewire 3 אין צורך לחבר את ה-assets בעצמכם: החבילה מזריקה את
@livewireStyles ואת @livewireScripts ל-layout. עם layout לא סטנדרטי
או CSP קפדני אפשר למקם את ה-directives ידנית.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ $title ?? 'Application' }}</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
@livewireStyles
</head>
<body class="antialiased">
{{ $slot }}
@livewireScripts
</body>
</html>@livewireScripts — או שהוא ממוקם לפני ה-build של Alpine.js שלכם.
Livewire 3 כבר כולל את Alpine; מופע שני של Alpine שובר את הריאקטיביות.
פרסום קובץ ה-config
php artisan livewire:publish --configההגדרות החשובות ב-config/livewire.php:
| הגדרה | תפקיד |
|---|---|
class_namespace | ה-namespace של מחלקות הקומפוננטות. ברירת המחדל: App\Livewire. |
view_path | התיקייה שמכילה את ה-views של הקומפוננטות ב-Blade. |
layout | ה-layout שמשמש קומפוננטות עמוד מלא. |
temporary_file_upload | דיסק, משך חיים וכללים להעלאות זמניות. |
inject_assets | הזרקה אוטומטית של CSS/JS. כבו אם אתם ממקמים את ה-directives בעצמכם. |
navigate.show_progress_bar | פס התקדמות עבור wire:navigate. |
בדיקת ההתקנה
php artisan livewire:make Counter
# CLASS: app/Livewire/Counter.php
# VIEW: resources/views/livewire/counter.blade.php3. הקומפוננטה הראשונה
קומפוננטה היא שני קבצים: מחלקה ו-view.
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function decrement(): void
{
$this->count--;
}
public function reset(): void
{
$this->count = 0;
}
public function render()
{
return view('livewire.counter');
}
}<div class="flex items-center gap-3">
<button type="button" wire:click="decrement" class="btn">−</button>
<span class="text-2xl font-bold">{{ $count }}</span>
<button type="button" wire:click="increment" class="btn">+</button>
<button type="button" wire:click="reset" class="btn btn-ghost">Reset</button>
</div><div>.
שלוש דרכים לרנדר קומפוננטה
{{-- 1. תחביר תגית (מומלץ) --}}
<livewire:counter />
{{-- 2. Directive --}}
@livewire('counter')
{{-- 3. עם פרמטרים --}}
<livewire:counter :start="10" title="Order counter" />קומפוננטות עמוד מלא
אפשר לחבר קומפוננטה ישירות ל-route ולרנדר אותה כעמוד עצמאי.
use App\Livewire\Dashboard;
Route::get('/dashboard', Dashboard::class)->middleware('auth')->name('dashboard');use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
class Dashboard extends Component
{
#[Layout('layouts.app')]
#[Title('Dashboard')]
public function render()
{
return view('livewire.dashboard');
}
}קומפוננטות inline
בקומפוננטות קטנות מאוד אפשר להחזיר את ה-view כמחרוזת ולוותר לגמרי על קובץ Blade.
public function render()
{
return <<<'BLADE'
<div>
<button wire:click="$refresh">Refresh</button>
</div>
BLADE;
}4. מאפייני קומפוננטה
מאפיינים ציבוריים של המחלקה זמינים אוטומטית ב-view של Blade ונשמרים בין בקשות. הם ה-state של הקומפוננטה.
class UserProfile extends Component
{
public string $name = '';
public ?int $age = null;
public array $tags = [];
public bool $isPublic = false;
public function render()
{
return view('livewire.user-profile');
}
}מה אפשר לשמור
ה-state עובר סריאליזציה ל-JSON בין בקשות, ולכן מגוון הטיפוסים הנתמכים מוגבל.
| מותר | אסור |
|---|---|
string, int, float, bool, null |
Closures |
array של ערכים סקלריים |
Resources, streams |
| מודלי Eloquent ו-collections של מודלים | אובייקטים שרירותיים ללא תמיכת Wireable/Synth |
Carbon, DateTime, Stringable, enums |
אובייקטים שמחזיקים חיבורים, PDO handles וכדומה |
אתחול: mount()
mount() הוא ה-constructor של הקומפוננטה. הוא רץ פעם אחת, ברינדור הראשון, ומקבל
את הפרמטרים שהועברו מ-Blade.
use App\Models\Order;
class OrderCard extends Component
{
public Order $order;
public string $mode;
public function mount(Order $order, string $mode = 'compact'): void
{
$this->order = $order;
$this->mode = $mode;
}
}<livewire:order-card :order="$order" mode="full" :key="$order->id" />הגנה על מאפיינים: #[Locked]
כברירת מחדל הלקוח יכול לשנות את הערך של כל מאפיין ציבורי. עבור מזהים וכל דבר שמשפיע על
בקרת גישה זו פרצה. ה-attribute #[Locked] אוסר שינויים מצד
ה-frontend.
use Livewire\Attributes\Locked;
class InvoiceEditor extends Component
{
#[Locked]
public int $invoiceId; // הלקוח לא יכול להחליף למזהה של מישהו אחר
public string $comment = '';
}מאפיינים שמוסתרים מ-JSON
מאפייני protected ו-private לא עוברים סריאליזציה ומתאפסים
בין בקשות. השתמשו בהם רק לערכים שמחושבים מחדש בכל מחזור.
class Report extends Component
{
public string $period = 'month';
// מתאפס אחרי כל בקשה — אל תשמרו כאן state.
protected array $cache = [];
}5. wire:model וקישור נתונים
wire:model מקשר שדה טופס למאפיין של הקומפוננטה. ב-Livewire 3 הקישור
דחוי כברירת מחדל: הערך לא נשלח לשרת בכל הקשה, אלא
יחד עם הפעולה הבאה (לחיצה, submit).
{{-- דחוי: הערך נשלח עם הבקשה הבאה --}}
<input type="text" wire:model="name">
{{-- מיידי: בקשה בכל שינוי --}}
<input type="text" wire:model.live="search">
{{-- 500 ms אחרי שההקלדה נעצרת --}}
<input type="text" wire:model.live.debounce.500ms="search">
{{-- לכל היותר פעם ב-2 s בזמן ההקלדה --}}
<input type="text" wire:model.live.throttle.2s="search">
{{-- רק כשהשדה מאבד פוקוס --}}
<input type="text" wire:model.blur="email">
{{-- מתעדכן מיד בצד הלקוח, השרת מתעדכן מאוחר יותר --}}
<input type="text" wire:model.lazy="draft">Modifiers
| Modifier | התנהגות | מתי להשתמש |
|---|---|---|
| — | שליחה דחויה | שדות טופס רגילים |
.live | בקשה בכל שינוי | חיפוש חי, select-ים תלויים |
.blur | בקשה באיבוד פוקוס | ולידציה של שדה ביציאה ממנו |
.debounce.Xms | המתנה להפסקה בהקלדה | חיפוש תוך כדי הקלדה |
.throttle.Xs | לכל היותר פעם אחת בכל פרק זמן | queries יקרים |
.number | המרה למספר | שדות מספריים |
.boolean | המרה לבוליאני | select-ים של כן/לא |
.fill | לקיחת הערך ההתחלתי מה-markup | טפסים ממולאים מראש |
כל סוגי השדות
{{-- טקסט, textarea --}}
<input type="text" wire:model="title">
<textarea wire:model="body"></textarea>
{{-- checkbox בודד: bool --}}
<input type="checkbox" wire:model="agreed">
{{-- קבוצת checkboxes: מערך --}}
<input type="checkbox" value="php" wire:model="skills">
<input type="checkbox" value="js" wire:model="skills">
<input type="checkbox" value="sql" wire:model="skills">
{{-- כפתורי radio --}}
<input type="radio" value="card" wire:model="payment">
<input type="radio" value="cash" wire:model="payment">
{{-- Select --}}
<select wire:model.live="categoryId">
<option value="">All categories</option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
{{-- Select מרובה --}}
<select wire:model="tagIds" multiple>
@foreach ($tags as $tag)
<option value="{{ $tag->id }}">{{ $tag->name }}</option>
@endforeach
</select>נתונים מקוננים
סימון נקודה עובד גם למערכים וגם למאפיינים של מודלים.
public array $form = [
'name' => '',
'address' => ['city' => '', 'street' => ''],
];
public Post $post;<input wire:model="form.name">
<input wire:model="form.address.city">
<input wire:model="post.title">wire:model="post.title" יעבוד, השדה חייב
להיות מותר בכלל ולידציה (rules או #[Validate]) — אחרת Livewire
זורק חריגה. זו ההגנה מפני mass assignment.
6. פעולות (Actions)
פעולה היא מתודה ציבורית של הקומפוננטה שנקראת מה-view. היא מחליפה את הצירוף הרגיל של
fetch() ו-handler בצד השרת.
{{-- לחיצה --}}
<button wire:click="save">Save</button>
{{-- שליחת טופס (מיורטת) --}}
<form wire:submit="save">
<input wire:model="title">
<button type="submit">Send</button>
</form>
{{-- מקשים --}}
<input wire:keydown.enter="search" wire:keydown.escape="clear">
{{-- אירועי DOM אחרים --}}
<div wire:mouseenter="preload">…</div>
<select wire:change="applyFilter">…</select>פרמטרים
<button wire:click="delete({{ $post->id }})">Delete</button>
<button wire:click="setStatus('published')">Publish</button>
<button wire:click="move({{ $item->id }}, 'up')">Move up</button>public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post); // תמיד לבדוק הרשאות
$post->delete();
$this->dispatch('notify', message: 'Post deleted');
}delete(999) עם כל מזהה שהוא. בדיקת הרשאות בתוך
המתודה היא חובה.
Model binding בפרמטרים
Livewire טוען מודלים לפי מזהה באותו אופן שה-router של Laravel עושה זאת.
public function archive(Post $post): void
{
$this->authorize('update', $post);
$post->update(['archived_at' => now()]);
}Modifiers של פעולות
{{-- אישור בדפדפן לפני השליחה --}}
<button wire:click="delete" wire:confirm="Delete for good? This cannot be undone.">Delete</button>
{{-- preventDefault / stopPropagation --}}
<a href="#" wire:click.prevent="open">Open</a>
<div wire:click.stop="select">…</div>
{{-- רץ פעם אחת --}}
<button wire:click.once="init">Initialise</button>
{{-- רק כשלחצו בדיוק על האלמנט הזה --}}
<div wire:click.self="close">…</div>פעולות קסם
| פעולה | מה היא עושה |
|---|---|
$refresh | רינדור מחדש בלי לשנות את ה-state |
$set('prop', value) | השמת ערך למאפיין |
$toggle('prop') | היפוך מאפיין בוליאני |
$dispatch('event') | שליחת אירוע Livewire |
$parent.method() | קריאה למתודה של קומפוננטת האב |
<button wire:click="$refresh">Refresh</button>
<button wire:click="$set('tab', 'settings')">Settings</button>
<button wire:click="$toggle('showFilters')">Filters</button>
<button wire:click="$parent.closeModal()">Close</button>הפניות (Redirects)
public function store(): void
{
$this->validate();
$post = Post::create($this->only('title', 'body'));
session()->flash('status', 'Post created');
$this->redirect(route('posts.show', $post), navigate: true);
}7. מחזור החיים של בקשה
הכרת סדר הביצוע מעלימה 90% מהבאגים ה"מסתוריים". יש שני מסלולים: הרינדור הראשוני (בקשת HTTP רגילה לעמוד) ועדכונים עוקבים (בקשות AJAX של Livewire).
רינדור ראשוני
1. The component instance is created
2. boot()
3. mount($params)
4. booted()
5. hydrate hooks do NOT run
6. render()
7. Markup is inserted into the pageעדכון עוקב
1. POST /livewire/update arrives with the state snapshot
2. The snapshot checksum is verified (tamper protection)
3. The component instance is created
4. boot()
5. State is restored from the snapshot
6. hydrate() and hydrateFoo() for each property
7. booted()
8. updating($prop, $value) / updatingFoo($value) — before assignment
9. Properties receive their new values
10. updated($prop, $value) / updatedFoo($value) — after assignment
11. Queued calls (actions) are executed
12. rendering()
13. render()
14. rendered($view, $html)
15. dehydrate() — state is packed back into the snapshot
16. Response: new markup plus effects (events, redirects, browser dispatches)Snapshot ו-checksum
כל ה-state של הקומפוננטה עובר ללקוח וחזרה בתוך ה-attribute wire:snapshot.
כדי למנוע מהלקוח לשנות אותו, Livewire חותם על ה-snapshot ב-HMAC שנגזר
מ-APP_KEY. חתימה שאינה תואמת נדחית עם "Livewire encountered corrupt data".
render() או דרך #[Computed].
מה קורה ל-DOM
Livewire לא מחליף את הצומת בשלמותו, אלא מבצע morph: הוא עובר על העץ הישן ועל
החדש ומשנה רק את ההבדלים. בזכות זה נשמרים הפוקוס בשדות, מיקום הגלילה וה-state של Alpine.
כשמבנה של רשימה משתנה, האלגוריתם צריך רמז — ראו wire:key
בפרק 10.
8. Lifecycle hooks
Hooks מאפשרים להתערב בכל שלב של המחזור.
class ProductEditor extends Component
{
public Product $product;
public string $name = '';
public float $price = 0;
// רץ בתחילת כל בקשה, לפני שחזור ה-state.
public function boot(): void
{
// מקום טוב לתלויות שלא ניתן לעשות להן סריאליזציה.
}
// רק ברינדור הראשון.
public function mount(Product $product): void
{
$this->product = $product;
$this->name = $product->name;
$this->price = $product->price;
}
// אחרי שחזור ה-state, בכל בקשה עוקבת.
public function hydrate(): void
{
}
// אחרי boot() ושחזור ה-state.
public function booted(): void
{
}
// לפני שינוי של מאפיין כלשהו.
public function updating(string $property, mixed $value): void
{
}
// אחרי שינוי של מאפיין כלשהו.
public function updated(string $property, mixed $value): void
{
$this->validateOnly($property);
}
// רק עבור המאפיין $price.
public function updatedPrice(mixed $value): void
{
$this->price = round((float) $value, 2);
}
// עבור המפתח המקונן form.email
public function updatedFormEmail(mixed $value): void
{
$this->form['email'] = strtolower(trim($value));
}
public function rendering(): void
{
}
public function rendered(mixed $view, string $html): void
{
}
// לפני שה-state נארז ל-snapshot.
public function dehydrate(): void
{
}
public function render()
{
return view('livewire.product-editor');
}
}כללי שמות
| מאפיין | Hook |
|---|---|
$price | updatedPrice() |
$isActive | updatedIsActive() |
$form['email'] | updatedFormEmail() |
$post->title | updatedPostTitle() |
public function updatedSearch() { $this->resetPage(); }
9. ולידציה ו-Form objects
Livewire משתמש ב-validator של Laravel — אותם כללים, הודעות ולוקליזציה.
אפשרות 1: attributes (Livewire 3)
use Livewire\Attributes\Validate;
class ContactForm extends Component
{
#[Validate('required|string|min:2|max:80')]
public string $name = '';
#[Validate('required|email:rfc,dns')]
public string $email = '';
#[Validate('required|string|min:20|max:2000')]
public string $message = '';
#[Validate('accepted', message: 'Please agree to the processing of your data.')]
public bool $consent = false;
public function submit(): void
{
$data = $this->validate();
Contact::create($data);
$this->reset();
session()->flash('status', 'Message sent');
}
}אפשרות 2: המתודה rules()
נדרשת כשהכללים תלויים ב-state.
protected function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->userId),
],
'password' => $this->userId ? 'nullable|min:8|confirmed' : 'required|min:8|confirmed',
];
}
protected function messages(): array
{
return [
'email.unique' => 'That email address is already registered.',
];
}
protected function validationAttributes(): array
{
return [
'email' => 'email address',
];
}ולידציה בזמן אמת
public function updated(string $property): void
{
$this->validateOnly($property); // בדיקה רק של השדה שהשתנה
}<form wire:submit="submit" novalidate>
<label for="email">Email</label>
<input id="email" type="email" wire:model.blur="email"
class="@error('email') border-red-500 @enderror">
@error('email')
<p class="text-sm text-red-600">{{ $message }}</p>
@enderror
<button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove wire:target="submit">Send</span>
<span wire:loading wire:target="submit">Sending…</span>
</button>
</form>ניהול שגיאות ידני
$this->addError('email', 'That domain is blocklisted.');
$this->resetValidation('email');
$this->resetValidation(); // ניקוי הכול
$this->validateOnly('email');
// זריקת חריגה בדיוק כמו ב-controller
throw ValidationException::withMessages([
'code' => 'Wrong confirmation code.',
]);Form objects
כשטופס גדל, העבירו אותו למחלקה משלו. הקומפוננטה נשארת רזה, ואת הכללים והנתונים אפשר לשתף בין יצירה לעריכה.
php artisan livewire:form PostForm<?php
namespace App\Livewire\Forms;
use App\Models\Post;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post = null;
#[Validate('required|string|min:5|max:180')]
public string $title = '';
#[Validate('required|string|min:50')]
public string $body = '';
#[Validate('nullable|date|after_or_equal:today')]
public ?string $publishAt = null;
public function setPost(Post $post): void
{
$this->post = $post;
$this->title = $post->title;
$this->body = $post->body;
$this->publishAt = $post->publish_at?->toDateString();
}
public function store(): Post
{
$this->validate();
return Post::create($this->except('post'));
}
public function update(): void
{
$this->validate();
$this->post->update($this->except('post'));
}
}class PostEditor extends Component
{
public PostForm $form;
public function mount(?Post $post = null): void
{
if ($post?->exists) {
$this->form->setPost($post);
}
}
public function save(): void
{
$this->form->post
? $this->form->update()
: $this->form->store();
$this->redirect(route('posts.index'), navigate: true);
}
}<form wire:submit="save">
<input wire:model="form.title">
@error('form.title') <span>{{ $message }}</span> @enderror
<textarea wire:model="form.body"></textarea>
@error('form.body') <span>{{ $message }}</span> @enderror
<button type="submit">Save</button>
</form>10. קומפוננטות מקוננות ו-wire:key
קומפוננטות ניתנות לקינון. כל קומפוננטת ילד היא יחידה עצמאית עם state ומחזור עדכון משלה: לחיצה בתוך הילד אינה מרנדרת מחדש את ההורה.
<div>
<h1>Orders</h1>
@foreach ($orders as $order)
<livewire:order-row :order="$order" :key="'order-'.$order->id" />
@endforeach
</div>:key הוא חובה בלולאות. ללא מפתח ייחודי אלגוריתם ה-morph
מתבלבל בין אלמנטים בעת מיון, סינון או מחיקה: ערכי שדות "עוברים" לשורות סמוכות.
המפתח חייב להיות יציב — $loop->index לא יתאים, השתמשו במזהה הרשומה.
העברת נתונים למטה
הפרמטרים מועברים פעם אחת, ב-mount(). ברינדורים הבאים של ההורה הילד
אינו מתעדכן אוטומטית — הוא חי את חייו שלו.
use Livewire\Attributes\Reactive;
class OrderTotal extends Component
{
// עם #[Reactive] הערך מגיע מההורה בכל רינדור שלו.
#[Reactive]
public int $quantity;
}קריאה להורה
<button wire:click="$parent.refreshList()">Refresh the list</button>קישור דו-כיווני: #[Modelable]
מאפשר לשים wire:model על הקומפוננטה עצמה — שימושי לווידג'טים של קלט.
use Livewire\Attributes\Modelable;
class RatingInput extends Component
{
#[Modelable]
public int $value = 0;
public function set(int $value): void
{
$this->value = $value;
}
}{{-- ב-view של ההורה --}}
<livewire:rating-input wire:model.live="review.rating" />רינדור מותנה של ילדים
@if ($showDetails)
<livewire:order-details :order-id="$orderId" :key="'details-'.$orderId" />
@endif11. אירועים
אירועים מחברים בין קומפוננטות שאין ביניהן יחסי הורה-ילד. ב-Livewire 3 שולחים באמצעות
dispatch() (בגרסה 2 השתמשו ב-emit()).
שליחה
// אירוע פשוט
$this->dispatch('post-created');
// עם פרמטרים בעלי שם
$this->dispatch('post-created', postId: $post->id, title: $post->title);
// לקומפוננטה ספציפית אחת
$this->dispatch('refresh')->to(OrderList::class);
// לעצמה בלבד
$this->dispatch('recalculate')->self();{{-- ישירות מה-view --}}
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">New order</button>האזנה
use Livewire\Attributes\On;
class OrderList extends Component
{
public array $orders = [];
#[On('post-created')]
public function onPostCreated(int $postId, string $title): void
{
$this->orders[] = ['id' => $postId, 'title' => $title];
}
// שם אירוע דינמי
#[On('order-updated.{orderId}')]
public function onOrderUpdated(): void
{
$this->refreshList();
}
}החלופה היא המערך $listeners (תואם לגרסה 2):
protected $listeners = [
'post-created' => 'onPostCreated',
'refresh' => '$refresh',
];אירועים לדפדפן
Livewire יכול לשלוח אירוע DOM רגיל, שנקלט על ידי Alpine.js או קוד JS משלכם. זו הדרך הנכונה להציג הודעות toast ולפתוח חלונות מודאליים.
$this->dispatch('notify', type: 'success', message: 'Order saved');<div x-data="{ show: false, message: '' }"
x-on:notify.window="message = $event.detail.message; show = true; setTimeout(() => show = false, 3000)"
x-show="show"
x-transition
class="toast">
<span x-text="message"></span>
</div>document.addEventListener('notify', (event) => {
console.log(event.detail.message);
});12. מאפיינים מחושבים
#[Computed] נותן לכם נתונים נגזרים בלי לשמור אותם ב-state. התוצאה נשמרת במטמון
למשך בקשה אחת, כך ששימוש בה שלוש פעמים ב-view אינו עולה שלוש שאילתות.
use Livewire\Attributes\Computed;
use Illuminate\Support\Collection;
class Cart extends Component
{
public array $items = [];
#[Computed]
public function products(): Collection
{
return Product::whereIn('id', array_keys($this->items))->get();
}
#[Computed]
public function subtotal(): float
{
return $this->products->sum(
fn (Product $product) => $product->price * $this->items[$product->id]
);
}
#[Computed]
public function total(): float
{
return round($this->subtotal * 1.2, 2); // כולל מע"מ
}
}<div>
@foreach ($this->products as $product)
<div>{{ $product->name }} — {{ $this->items[$product->id] }} pcs</div>
@endforeach
<p>Subtotal: {{ number_format($this->subtotal, 2) }} €</p>
<p>Total incl. VAT: {{ number_format($this->total, 2) }} €</p>
</div>$this. ב-view מאפיין מחושב הוא
$this->products, ולא $products. זה מה שמבדיל אותו ממאפיינים
ציבוריים רגילים.
מטמון בין בקשות
// נשמר במטמון ל-5 דקות במטמון המשותף של האפליקציה
#[Computed(persist: true, seconds: 300)]
public function statistics(): array
{
return [
'orders' => Order::whereMonth('created_at', now()->month)->count(),
'revenue' => Order::whereMonth('created_at', now()->month)->sum('total'),
];
}
// רשומת מטמון אחת המשותפת לכל המשתמשים
#[Computed(cache: true, key: 'global-stats')]
public function globalStats(): array
{
return app(StatsService::class)->build();
}ניקוי המטמון
public function addItem(int $productId): void
{
$this->items[$productId] = ($this->items[$productId] ?? 0) + 1;
unset($this->products, $this->subtotal, $this->total); // ניקוי המטמון
}נתוני render() לעומת #[Computed]
אף אחד מהם אינו שומר נתונים ב-state. ההבדל הוא בטווח: render() מעביר משתנים
ל-view בלבד, בעוד ש-#[Computed] זמין במתודות PHP וב-view, ונשמר במטמון.
public function render()
{
return view('livewire.orders', [
'orders' => Order::query()
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->latest()
->paginate(20),
]);
}13. State ב-URL וב-session
פילטרים, חיפוש והלשונית הפעילה צריכים לשרוד רענון ולהיות ניתנים לשיתוף כקישור. ה-attribute
#[Url] שומר מאפיין מסונכרן עם ה-query string.
use Livewire\Attributes\Url;
class ProductCatalog extends Component
{
#[Url]
public string $search = '';
// שם פרמטר שונה ב-URL
#[Url(as: 'cat')]
public ?int $categoryId = null;
// לא מופיע ב-URL כל עוד הוא שווה לערך ההתחלתי
#[Url(except: '')]
public string $sort = 'popular';
// נשמר בין ביקורי wire:navigate
#[Url(keep: true)]
public int $perPage = 24;
// שימוש ב-history.pushState במקום replaceState
#[Url(history: true)]
public string $tab = 'all';
}התוצאה: /catalog?search=laptop&cat=5&sort=price-asc&tab=sale
שמירה ב-session
use Livewire\Attributes\Session;
class Sidebar extends Component
{
#[Session]
public bool $collapsed = false;
#[Session(key: 'admin.table.density')]
public string $density = 'comfortable';
}Session ו-flash באופן ידני
public function save(): void
{
$this->validate();
Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);
session()->flash('status', 'Settings saved');
}@if (session('status'))
<div class="docs-note docs-note--tip">{{ session('status') }}</div>
@endif#[Url] פונה ל-History API
כשהוא משתנה. בשדות שמשתנים בכל הקשה, שלבו אותו עם
wire:model.live.debounce.
14. העלאת קבצים
ה-trait WithFileUploads מוסיף העלאות אמיתיות: הקובץ נשלח לשרת ברגע
שנבחר, נשמר באחסון זמני והופך לזמין כאובייקט
TemporaryUploadedFile.
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
class AvatarUploader extends Component
{
use WithFileUploads;
#[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')] // עד 4 MB
public $avatar;
#[Validate(['documents.*' => 'file|mimes:pdf,docx|max:10240'])]
public array $documents = [];
public function save(): void
{
$this->validate();
$path = $this->avatar->store('avatars', 'public');
auth()->user()->update(['avatar_path' => $path]);
$this->reset('avatar');
$this->dispatch('notify', message: 'Avatar updated');
}
public function removeDocument(int $index): void
{
unset($this->documents[$index]);
$this->documents = array_values($this->documents);
}
}<form wire:submit="save">
<input type="file" wire:model="avatar" accept="image/*">
{{-- התקדמות ההעלאה --}}
<div wire:loading wire:target="avatar" class="text-sm">Uploading…</div>
{{-- תצוגה מקדימה לפני השמירה --}}
@if ($avatar)
<img src="{{ $avatar->temporaryUrl() }}" alt="Preview" class="w-32 h-32 object-cover rounded">
@endif
@error('avatar') <p class="text-red-600">{{ $message }}</p> @enderror
<button type="submit" wire:loading.attr="disabled">Save</button>
</form>סרגל התקדמות מדויק
<div x-data="{ progress: 0, uploading: false }"
x-on:livewire-upload-start="uploading = true"
x-on:livewire-upload-finish="uploading = false; progress = 0"
x-on:livewire-upload-error="uploading = false"
x-on:livewire-upload-progress="progress = $event.detail.progress">
<input type="file" wire:model="avatar">
<div x-show="uploading" class="h-2 bg-gray-200 rounded">
<div class="h-2 bg-green-600 rounded" :style="`width: ${progress}%`"></div>
</div>
</div>הגדרת האחסון הזמני
'temporary_file_upload' => [
'disk' => 's3', // או null עבור דיסק ברירת המחדל
'rules' => ['file', 'max:12288'],
'directory' => 'livewire-tmp',
'middleware' => 'throttle:60,1',
'preview_mimes' => ['png', 'jpeg', 'jpg', 'webp', 'gif', 'mp4', 'pdf'],
'max_upload_time' => 5, // דקות עד לניקוי אוטומטי
],upload_max_filesize, post_max_size
ו-max_execution_time ב-php.ini, וכן client_max_body_size
ב-nginx, חייבים כולם להיות גדולים לפחות כמו כלל ה-max: שלכם. אחרת ההעלאה נכשלת
ללא הודעת שגיאה קריאה.
15. עימוד
ה-trait WithPagination מחבר את העימוד של Laravel ללא טעינה מחדש של הדף.
use Livewire\WithPagination;
use Livewire\Attributes\Url;
class OrderTable extends Component
{
use WithPagination;
#[Url]
public string $search = '';
#[Url]
public string $status = '';
public string $sortField = 'created_at';
public string $sortDirection = 'desc';
public int $perPage = 25;
// חזרה לעמוד הראשון בכל שינוי של פילטר.
public function updatedSearch(): void
{
$this->resetPage();
}
public function updatedStatus(): void
{
$this->resetPage();
}
public function sortBy(string $field): void
{
if ($this->sortField === $field) {
$this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc';
} else {
$this->sortField = $field;
$this->sortDirection = 'asc';
}
$this->resetPage();
}
public function render()
{
return view('livewire.order-table', [
'orders' => Order::query()
->with('customer')
->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
->when($this->status, fn ($q) => $q->where('status', $this->status))
->orderBy($this->sortField, $this->sortDirection)
->paginate($this->perPage),
]);
}
}<div>
<input type="search" wire:model.live.debounce.400ms="search" placeholder="Search by number">
<table>
<thead>
<tr>
<th wire:click="sortBy('number')" style="cursor:pointer">Number</th>
<th wire:click="sortBy('created_at')" style="cursor:pointer">Date</th>
<th wire:click="sortBy('total')" style="cursor:pointer">Total</th>
</tr>
</thead>
<tbody>
@forelse ($orders as $order)
<tr wire:key="order-{{ $order->id }}">
<td>{{ $order->number }}</td>
<td>{{ $order->created_at->format('d/m/Y') }}</td>
<td>{{ number_format($order->total, 2) }}</td>
</tr>
@empty
<tr><td colspan="3">No orders found</td></tr>
@endforelse
</tbody>
</table>
{{ $orders->links() }}
</div>פרטים שימושיים
// שם מותאם לפרמטר העמוד — נדרש כשבדף יש שני paginators
protected string $paginationTheme = 'tailwind'; // או 'bootstrap'
public function render()
{
return view('livewire.dashboard', [
'orders' => Order::paginate(10, pageName: 'orders-page'),
'invoices' => Invoice::paginate(10, pageName: 'invoices-page'),
]);
}paginate() מריץ
COUNT(*) נוסף. אם אינכם צריכים את מספר העמודים הכולל, השתמשו ב-simplePaginate() —
זה זול משמעותית.
16. מצבי טעינה
כל פעולה היא בקשת רשת. ללא משוב הממשק מרגיש תקוע. Livewire מגיע עם directives הצהרתיים שאינם דורשים JavaScript כלל.
{{-- מוצג במהלך כל בקשה מהקומפוננטה הזו --}}
<div wire:loading>Loading…</div>
{{-- מוסתר במהלך בקשה --}}
<div wire:loading.remove>Content</div>
{{-- רק עבור פעולה אחת --}}
<button wire:click="save">Save</button>
<span wire:loading wire:target="save">Saving…</span>
{{-- עבור כמה יעדים --}}
<span wire:loading wire:target="save,delete,publish">Working…</span>
{{-- עבור מאפיין מסוים --}}
<span wire:loading wire:target="search">Searching…</span>
{{-- החרגת יעד --}}
<div wire:loading wire:target.except="search">Updating…</div>Modifiers
{{-- השבתת הכפתור --}}
<button wire:click="save" wire:loading.attr="disabled">Save</button>
{{-- הוספת class של CSS --}}
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Save</button>
{{-- הסרת class --}}
<div wire:loading.class.remove="bg-white">…</div>
{{-- השהיה: המחוון מוצג רק אם הבקשה נמשכת יותר מ-300 ms --}}
<div wire:loading.delay>Loading…</div>
{{-- ספים מדויקים: shortest 50ms, shorter 100ms, short 150ms,
default 200ms, long 300ms, longer 500ms, longest 1s --}}
<div wire:loading.delay.long>Loading…</div>
{{-- שליטה במצב התצוגה --}}
<div wire:loading.flex>…</div>
<div wire:loading.grid>…</div>
<div wire:loading.inline-flex>…</div>שינויים שלא נשמרו: wire:dirty
<input wire:model="title">
<span wire:dirty wire:target="title" class="text-amber-600">You have unsaved changes</span>
<button wire:click="save" wire:dirty.class="ring-2 ring-amber-400">Save</button>אובדן חיבור: wire:offline
<div wire:offline class="banner banner--warn">
No connection to the server. Changes are not being saved.
</div>Skeleton בטעינה הראשונה
<div wire:init="loadHeavyData">
@if ($loaded)
{{-- הנתונים האמיתיים --}}
@else
<div class="skeleton h-40 w-full animate-pulse bg-gray-200"></div>
@endif
</div>18. טעינה עצלה ו-polling
טעינה עצלה של קומפוננטה
קומפוננטה כבדה לא חייבת להירנדר בתגובה הראשונה: העמוד נשלח מיד עם placeholder, והתוכן מגיע בבקשה שנייה.
<livewire:revenue-chart lazy />use Livewire\Attributes\Lazy;
#[Lazy]
class RevenueChart extends Component
{
public function placeholder(): string
{
return <<<'BLADE'
<div class="skeleton h-64 w-full animate-pulse rounded bg-gray-200"></div>
BLADE;
}
public function render()
{
return view('livewire.revenue-chart', [
'points' => app(RevenueService::class)->monthly(), // שאילתה יקרה
]);
}
}
כברירת מחדל, קומפוננטה עצלה נטענת מיד אחרי שהעמוד מצויר. #[Lazy(isolate: false)]
מאחד את הבקשות של כמה קומפוננטות עצלות לבקשה אחת, ו-
lazy="on-load" / lazy="on-scroll" קובעים מתי הטעינה מתבצעת.
{{-- טעינה כשהבלוק נכנס לתחום התצוגה --}}
<livewire:revenue-chart lazy="on-scroll" />אתחול נדחה: wire:init
<div wire:init="loadStats">
@if ($stats)
…
@else
<div class="skeleton"></div>
@endif
</div>תשאול השרת: wire:poll
{{-- רענון כל 2 שניות (ברירת המחדל היא 2500 ms) --}}
<div wire:poll>…</div>
{{-- מרווח מותאם --}}
<div wire:poll.5s>…</div>
<div wire:poll.750ms>…</div>
{{-- קריאה למתודה מסוימת --}}
<div wire:poll.10s="refreshQueue">…</div>
{{-- עצירת ה-polling כשהלשונית מוסתרת --}}
<div wire:poll.visible.5s="refreshQueue">…</div>
{{-- עצירה אחרי 5 דקות של חוסר פעילות המשתמש --}}
<div wire:poll.keep-alive.5s>…</div>wire:poll.2s בעמוד שפתוח ב-100 עמדות עבודה הוא 3,000
בקשות בדקה שפוגעות בתהליכי ה-PHP שלכם. הוסיפו תמיד .visible, בחרו
מרווח סביר, ושקלו websockets (Laravel Echo + Reverb) עבור נתונים חיים באמת.
19. Alpine.js, $wire ו-JS hooks
Alpine.js מגיע יחד עם Livewire 3. בתוך קומפוננטה זמין האובייקט $wire — proxy
אל ה-state והמתודות של קומפוננטת ה-PHP, ישירות מ-JavaScript.
<div x-data="{ open: false }">
{{-- state בצד הלקוח בלבד: השרת לא מעורב --}}
<button x-on:click="open = !open">Details</button>
<div x-show="open" x-transition>
{{-- קריאת property --}}
<p x-text="$wire.title"></p>
{{-- כתיבת property --}}
<button x-on:click="$wire.title = 'New title'">Rename</button>
{{-- קריאה למתודה (מחזירה Promise) --}}
<button x-on:click="$wire.save()">Save</button>
{{-- המתנה לתוצאה --}}
<button x-on:click="await $wire.calculate(); open = false">Calculate</button>
{{-- קריאה ללא רינדור מחדש --}}
<button x-on:click="$wire.$set('tab', 'stats', false)">Stats</button>
</div>
</div>קישור דו-כיווני: $wire.entangle
<div x-data="{ query: $wire.entangle('search') }">
<input x-model="query">
<p x-show="query.length > 0">Searching for: <span x-text="query"></span></p>
</div>
{{-- סנכרון נדחה: לא לשלוח בקשה על כל תו --}}
<div x-data="{ query: $wire.entangle('search').live }">…</div>סקריפטים משלכם בתוך קומפוננטה
@script
<script>
// רץ פעם אחת כשהקומפוננטה מאותחלת.
const chart = new Chart(document.getElementById('sales'), {
type: 'line',
data: @json($chartData),
});
// גם כאן $wire זמין.
$wire.on('data-updated', ({ points }) => {
chart.data.datasets[0].data = points;
chart.update();
});
</script>
@endscript{{-- טעינת ספרייה חיצונית פעם אחת לכל העמוד --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassetsהתעלמות מתת-עץ
כשווידג'ט צד שלישי מנהל את ה-DOM בעצמו, אלגוריתם ה-morph רק מפריע.
<div wire:ignore>
<select id="select2-field">…</select>
</div>
{{-- התעלמות מהילדים, אך המשך עדכון המאפיינים של האלמנט עצמו --}}
<div wire:ignore.self>…</div>JS hooks גלובליים
document.addEventListener('livewire:init', () => {
// לפני כל בקשה יוצאת
Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
options.headers['X-Tenant'] = window.tenantId;
succeed(({ status, json }) => {
console.debug('Livewire responded', status);
});
fail(({ status, preventDefault }) => {
if (status === 419) {
preventDefault();
window.location.reload(); // פג תוקף טוקן ה-CSRF
}
});
});
// לפני ואחרי morph של אלמנט בודד
Livewire.hook('morph.updated', ({ el, component }) => {});
// קומפוננטה אותחלה
Livewire.hook('component.init', ({ component }) => {});
});
// גישה פרוגרמטית לקומפוננטות
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());20. בדיקות
Livewire מגיע עם API בדיקות משלו, שנבנה מעל זה של Laravel. הבדיקות מהירות — אין צורך בדפדפן, PHPUnit או Pest רגילים מספיקים.
<?php
use App\Livewire\Counter;
use Livewire\Livewire;
it('increments the counter', function () {
Livewire::test(Counter::class)
->assertSet('count', 0)
->call('increment')
->assertSet('count', 1)
->call('increment')
->assertSet('count', 2)
->call('decrement')
->assertSet('count', 1);
});use App\Livewire\ContactForm;
use App\Models\Contact;
use Livewire\Livewire;
it('validates the required fields', function () {
Livewire::test(ContactForm::class)
->set('name', '')
->set('email', 'not-an-email')
->call('submit')
->assertHasErrors([
'name' => 'required',
'email' => 'email',
])
->assertNoRedirect();
});
it('stores a valid enquiry', function () {
Livewire::test(ContactForm::class)
->set('name', 'Irene Kovac')
->set('email', 'irene@example.com')
->set('message', str_repeat('We need a website for a clinic. ', 2))
->set('consent', true)
->call('submit')
->assertHasNoErrors()
->assertDispatched('notify');
expect(Contact::where('email', 'irene@example.com')->exists())->toBeTrue();
});ה-assertions העיקריים
| מתודה | מה נבדק |
|---|---|
assertSet('prop', $value) | ערך של property |
assertNotSet('prop', $value) | הערך שונה |
assertSee('text') | הטקסט מופיע ב-markup |
assertDontSee('text') | הטקסט לא מופיע |
assertSeeHtml('<b>') | ה-markup קיים |
assertHasErrors(['email']) | שגיאות ולידציה |
assertHasNoErrors() | אין שגיאות |
assertDispatched('event') | נשלח אירוע |
assertRedirect(route(…)) | בוצעה הפניה |
assertStatus(403) | סטטוס HTTP |
assertForbidden() | הגישה נדחתה |
assertCount('items', 3) | גודל מערך או collection |
אימות, פרמטרים, אירועים
// כמשתמש מחובר
Livewire::actingAs($admin)
->test(OrderTable::class)
->assertSee('All orders');
// עם פרמטרים ל-mount()
Livewire::test(OrderCard::class, ['order' => $order, 'mode' => 'full'])
->assertSee($order->number);
// קבלת אירוע
Livewire::test(OrderList::class)
->dispatch('order-created', orderId: 42)
->assertSee('Order #42');
// העלאת קובץ
use Illuminate\Http\UploadedFile;
Livewire::test(AvatarUploader::class)
->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
->call('save')
->assertHasNoErrors();
// קומפוננטה בתוך עמוד
$this->get('/dashboard')
->assertSeeLivewire(Dashboard::class)
->assertDontSeeLivewire(AdminPanel::class);21. אבטחה
1. בדקו הרשאות בתוך כל action
public function delete(int $postId): void
{
$post = Post::findOrFail($postId);
$this->authorize('delete', $post); // policy — לא נתון למשא ומתן
$post->delete();
}// בדיקת הרשאות לקומפוננטה כולה
public function mount(Project $project): void
{
$this->authorize('view', $project);
$this->project = $project;
}2. #[Locked] על מזהים
use Livewire\Attributes\Locked;
class InvoiceEditor extends Component
{
#[Locked]
public int $invoiceId; // לא ניתן לשינוי מה-frontend
public string $note = '';
}בלי #[Locked] מספיקה שורה אחת בקונסולת הדפדפן:
Livewire.find('...').set('invoiceId', 999) // החשבונית של מישהו אחר3. כללים עבור model binding
wire:model="post.title" עובד רק כשהשדה מותר על ידי כלל ולידציה —
כך Livewire מתגונן מפני mass assignment.
protected function rules(): array
{
return [
'post.title' => 'required|string|max:180',
'post.body' => 'required|string',
// 'post.user_id' הושמט בכוונה — אחרת אפשר היה להעביר את הפוסט למשתמש אחר
];
}4. בצעו escape לפלט
{{-- בטוח: עובר escape --}}
{{ $comment->body }}
{{-- מסוכן: markup גולמי ממשתמש --}}
{!! $comment->body !!}
{{-- אם צריך markup, בצעו לו sanitize בצד השרת --}}
{!! clean($comment->body) !!}5. Rate limiting
use Illuminate\Support\Facades\RateLimiter;
public function login(): void
{
$key = 'login:' . request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
throw ValidationException::withMessages([
'email' => 'Too many attempts. Try again in a minute.',
]);
}
RateLimiter::hit($key, 60);
// …
}6. לעולם אל תשמרו סודות ב-properties
class PaymentForm extends Component
{
public string $cardLast4 = ''; // תקין
// אסור: זה מגיע לדפדפן כטקסט גלוי בתוך wire:snapshot
// public string $apiSecret = '';
// public string $fullCardNumber = '';
protected function gateway(): PaymentGateway
{
return app(PaymentGateway::class); // הסודות נשארים בשרת
}
}רשימת בדיקה לפני שחרור
$this->authorize()מופיע בכל action שמשנה נתונים.- כל המזהים מסומנים ב-
#[Locked]. - כללי הוולידציה לא כוללים שדות פנימיים (
user_id,role,price). - properties ציבוריים לא מחזיקים טוקנים, מפתחות או מידע אישי מיותר.
- העלאות קבצים מוגבלות לפי סוג MIME וגודל.
- טפסי התחברות ויצירת קשר מוגבלים ב-rate limiting.
{!! !!}משמש רק עבור markup אמין או כזה שעבר sanitize.
22. ביצועים
1. שמרו על state ציבורי קטן
public Collection $products; // 500 מודלים נוסעים ללקוח ובחזרה
public function mount(): void
{
$this->products = Product::with('category', 'images')->get();
}public function render()
{
return view('livewire.catalog', [
'products' => Product::with('category')->paginate(24),
]);
}2. אל תשלחו בקשה על כל הקשה
{{-- רע: בקשה על כל תו --}}
<input wire:model.live="search">
{{-- טוב --}}
<input wire:model.live.debounce.400ms="search">3. פצלו עמודים כבדים לקומפוננטות
עדכון של קומפוננטת ילד לא מרנדר מחדש את ההורה. דשבורד שבנוי משישה ווידג'טים עצמאיים מרגיש זריז בהרבה ממונולית אחד.
4. שמרו עבודה יקרה ב-cache
#[Computed(persist: true, seconds: 600)]
public function monthlyRevenue(): array
{
return app(RevenueService::class)->byMonth();
}5. היזהרו מ-N+1
// רע — שאילתה אחת לכל שורה ב-view
$orders = Order::paginate(50);
// טוב
$orders = Order::with(['customer', 'items.product'])->paginate(50);הפעילו strict mode בסביבת הפיתוח כדי ש-N+1 ייכשל בקול רם:
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}6. wire:key ברשימות
בלי מפתחות, אלגוריתם ה-morph בונה מחדש יותר צמתים מהנדרש — וגם טועה.
7. טעינה עצלה לבלוקים כבדים
<livewire:revenue-chart lazy="on-scroll" />8. אל תגזימו עם polling
wire:poll.visible.10s במקום wire:poll חוסך לשרת סדר גודל
של בקשות.
| תסמין | סיבה סבירה |
|---|---|
| השהיה של 300–800 ms בכל action | render() יקר או N+1 |
| markup עמוד עצום | collections של מודלים ב-properties ציבוריים |
| עומס גובר על PHP-FPM | wire:poll אגרסיבי או .live בלי debounce |
| שדות "קופצים" כשהרשימה מתעדכנת | חסר wire:key |
| העמוד כולו מתרנדר מחדש | הכול בקומפוננטה אחת, בלי פיצול |
23. שגיאות נפוצות ופתרונות
"Component must have a single root element"
ב-view יש כמה צמתי שורש, או טקסט או הערה ברמה העליונה. עטפו הכול ב-<div> אחד.
<h1>Heading</h1>
<p>Text</p><div>
<h1>Heading</h1>
<p>Text</p>
</div>"Livewire encountered corrupt data"
חתימת ה-snapshot לא תואמת. סיבות: APP_KEY שהשתנה, עמוד שהוגש
מ-cache אחרי deploy, שתי לשוניות עם sessions שונים. טעינה מחדש בדרך כלל פותרת; אחרי deploy הריצו
php artisan optimize:clear.
לחיצות לא עושות כלום והקונסולה ריקה
@livewireScriptsחסר ב-layout.- נטען מופע שני של Alpine.js (Livewire 3 כבר כולל אותו).
- שגיאת JS גבוה יותר בעמוד עצרה את הריצה.
- האלמנט נמצא בתוך
wire:ignore.
ערכי שדות "זזים" בין שורות
בלולאה חסר wire:key או :key. המפתח חייב להיות יציב וייחודי.
@foreach ($rows as $row)
<div wire:key="row-{{ $row->id }}">…</div>
@endforeach"Unable to set component data. Public property not found"
קישרתם wire:model ל-property שלא קיים או שאינו ציבורי. בדקו את
האיות ואת מגדיר הנראות.
"Cannot bind to model data without validation rules"
קישור כמו wire:model="post.title" דורש כלל עבור post.title ב-
rules() או ב-#[Validate].
ווידג'ט צד שלישי נשבר אחרי עדכון
Select2, Flatpickr, TinyMCE ודומיהם משנים את ה-DOM בעצמם. עטפו אותם ב-
wire:ignore וסנכרנו ידנית.
<div wire:ignore x-data x-init="
const picker = flatpickr($refs.input, {
onChange: (dates, str) => $wire.set('date', str),
});
">
<input x-ref="input" type="text">
</div>העלאות קבצים נכשלות בשקט
בדקו את upload_max_filesize ואת post_max_size ב-PHP, וגם את
client_max_body_size ב-nginx. הם חייבים לעלות על המגבלה בכלל max: שלכם.
המודאל לא נסגר אחרי שמירה
שלחו אירוע דפדפן וטפלו בו ב-Alpine במקום להסתמך על רינדור מחדש.
$this->dispatch('close-modal', name: 'order-form');סקריפטים מפסיקים לעבוד אחרי wire:navigate
העבירו את האתחול מ-DOMContentLoaded ל-livewire:navigated.
שגיאה 419 (Page Expired)
פג תוקף ה-session. הגדילו את SESSION_LIFETIME, או תפסו את הסטטוס ב-hook
request וטענו מחדש את העמוד — ראו את הדוגמה בפרק 19.
24. דף עזר
דירקטיבות Blade
| דירקטיבה | מטרה |
|---|---|
wire:model | קישור שדה ל-property (נדחה) |
wire:model.live | קישור עם בקשה בכל שינוי |
wire:model.blur | שליחה באובדן פוקוס |
wire:click | קריאה למתודה בלחיצה |
wire:submit | טיפול בשליחת טופס |
wire:keydown.enter | תגובה למקש |
wire:change | תגובה לשינוי |
wire:confirm | אישור לפני ביצוע |
wire:loading | מחוון טעינה |
wire:target | הגבלת המחוון ל-action מסוים |
wire:dirty | יש שינויים שלא נשמרו |
wire:offline | אין חיבור |
wire:poll | רענון תקופתי |
wire:init | קריאה למתודה מיד אחרי הרינדור |
wire:navigate | ניווט קישורים בסגנון SPA |
wire:key | זהות אלמנט בתוך לולאה |
wire:ignore | החרגת תת-עץ מ-morph |
wire:transition | אנימציית כניסה/יציאה |
wire:stream | הזרמת תוכן |
Attributes של PHP
| Attribute | מטרה |
|---|---|
#[Validate] | כלל ולידציה ל-property |
#[Locked] | איסור שינויים מה-frontend |
#[Computed] | property מחושב עם caching |
#[Url] | סנכרון עם ה-query string |
#[Session] | שמירת הערך ב-session |
#[On] | מאזין לאירוע |
#[Reactive] | רענון prop מההורה |
#[Modelable] | תמיכה ב-wire:model על הקומפוננטה |
#[Lazy] | טעינה עצלה של הקומפוננטה |
#[Layout] | Layout לקומפוננטת עמוד מלא |
#[Title] | כותרת העמוד |
#[Renderless] | מתודה שמדלגת על רינדור מחדש |
מתודות שימושיות של קומפוננטה
$this->reset(); // כל ה-properties חוזרים לערכים ההתחלתיים
$this->reset('search', 'page'); // איפוס של חלק מהם
$this->only('title', 'body'); // מערך של חלק מה-properties
$this->except('password'); // הכול חוץ מאלה שצוינו
$this->fill(['title' => 'New']); // השמה מרובה
$this->pull('draft'); // קריאה ואיפוס
$this->validate();
$this->validateOnly('email');
$this->resetValidation();
$this->addError('email', 'Message');
$this->dispatch('saved', id: $post->id);
$this->redirect('/orders', navigate: true);
$this->redirectRoute('orders.index', navigate: true);
$this->skipRender(); // דילוג על רינדור מחדש במחזור הזה
$this->js('alert("Done")'); // הרצת JS בצד הלקוח
$this->stream(to: 'answer', content: $chunk);פקודות Artisan
php artisan livewire:make Orders/OrderTable
php artisan livewire:make Counter --inline
php artisan livewire:form PostForm
php artisan livewire:attribute ValidPhone
php artisan livewire:publish --config
php artisan livewire:publish --assetsמשאבים רשמיים
- תיעוד Livewire: livewire.laravel.com/docs
- תיעוד Laravel: laravel.com/docs
- Alpine.js: alpinejs.dev
אם יש סתירה בין התיעוד הזה למקור הרשמי, המקור הרשמי צודק. מצאתם שגיאה? ספרו לנו.