[ Livewire · תיעוד ]

תיעוד Livewire 4

תיעוד מלא ל-Livewire 4: קומפוננטות בקובץ יחיד, islands עם רינדור עצמאי, slots, גרירה ושחרור מובנים והנחיות צד-לקוח — עם סימון היכן גרסה 4 שונה מגרסה 3.

  • 4.x · נוכחית
  • Laravel 11 / 12
  • PHP 8.2+
  • 26 פרקים
  • 40 דק׳ קריאה

1. מה משתנה ב-Livewire 4

Livewire 4 היא הגרסה הגדולה ביותר בתולדות הפריימוורק. מודל הקומפוננטה עצמו לא השתנה — מחלקת PHP ועוד תצוגת Blade, state בצד השרת, HTML על גבי הרשת — אבל כמעט כל מה שסביבו זז: היכן הקומפוננטות נמצאות, איך כותבים אותן, וכמה מהעמוד נוגע עדכון בודד.

חמשת הדברים החשובים

יכולתמה היא נותנת לכם
קומפוננטות בקובץ יחיד המחלקה והמרקאפ נמצאים בקובץ .blade.php אחד. אין יותר קפיצות בין שתי תיקיות בשביל קומפוננטה של עשרים שורות.
Islands אזורים מבודדים בתוך קומפוננטה שמתרנדרים מחדש בעצמם. מונה הכנסות מתעדכן כבר לא מריץ מחדש את השאילתות של כל הדשבורד.
Slots קומפוננטת אב מזריקה מרקאפ לקומפוננטות בת — והוא מוערך בהקשר של האב, כך ש-wire:click בתוך slot קורא למתודה של האב.
Optimistic UI wire:show, wire:text, wire:bind מעדכנים את ה-DOM מיד בצד הלקוח, בלי סבב מול השרת.
גרירה ושחרור wire:sort מובנה. בלי SortableJS ובלי קוד דבק.

האם צריך לשכתב את האפליקציה?

לא. Livewire 4 שומרת על תאימות לאחור חזקה: קומפוננטות מבוססות מחלקה ממשיכות לעבוד. קובץ יחיד הוא ברירת המחדל לקומפוננטות חדשות, לא מיגרציה כפויה. יש סדרה של שינויים שוברים שכדאי לקרוא לפני השדרוג — הם מרוכזים ב השוואה בין Livewire 3 ל-4, ו תיעוד השדרוג הרשמי הוא הרשימה המחייבת.

מגיעים מ-Livewire 3? פרקים 3, 6, 10 ו-11 הם המקום שבו נמצא החידוש האמיתי — קומפוננטות בקובץ יחיד, הסמנטיקה החדשה של wire:model, islands ו-slots. שאר מודל הקומפוננטה ירגיש מוכר.

2. התקנה ודרישות

Bash
composer require livewire/livewire:^4.0

php artisan optimize:clear

כמו בגרסה 3, ה-assets מוזרקים אוטומטית. את @livewireStyles ואת @livewireScripts צריך למקם ידנית רק כשמשתמשים ב-layout לא סטנדרטי או ב-Content Security Policy מחמיר.

נקודות הקצה של Livewire השתנו. כתובות העדכון כוללות כעת hash: /livewire/ הפך ל-/livewire-{hash}/. אם יש לכם חוקי firewall, החרגות WAF, חוקי עקיפה ב-CDN או locations ב-nginx שמקובעים לנתיב המילולי /livewire/, צריך להרחיב אותם לתבנית החדשה — אחרת האפליקציה תיראה שבורה ב-production בזמן שהיא עובדת מצוין על הלפטופ שלכם.

פרסום קובץ הקונפיגורציה

Bash
php artisan livewire:publish --config

הגדרות חדשות או ששמן שונה בגרסה 4:

הגדרהתפקיד
component_locationsתיקיות שנסרקות לאיתור קומפוננטות. ברירת המחדל: resources/views/components ו-resources/views/livewire.
component_namespacesשורשי קומפוננטות בעלי שם, למשל pages::.
component_layoutנקרא layout ב-v3. משתמש ב-namespace layouts::.
component_placeholderנקרא lazy_placeholder ב-v3.
make_commandקובע מה make:livewire מייצר: קובץ יחיד או מבוסס מחלקה.
smart_wire_keysברירת המחדל כעת היא true.
csp_safeמייצר build שעומד ב-Content Security Policies מחמירים יותר.

בחירת סגנון הקומפוננטה כברירת מחדל

config/livewire.php
// Keep generating class-based components instead of single-file ones
'make_command' => [
    'type' => 'class',
],

3. קומפוננטות בקובץ יחיד

השינוי המרכזי בעבודה היומיומית. קומפוננטה היא כעת קובץ Blade אחד שנפתח בבלוק PHP המצהיר על מחלקה אנונימית, ואחריו המרקאפ.

Bash
php artisan make:livewire post.create
# resources/views/components/post/⚡create.blade.php
resources/views/components/post/⚡create.blade.php
<?php

use Livewire\Component;
use App\Models\Post;
use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|string|min:5|max:180')]
    public string $title = '';

    #[Validate('required|string|min:50')]
    public string $body = '';

    public function save()
    {
        $this->validate();

        Post::create([
            'title' => $this->title,
            'body'  => $this->body,
            'user_id' => auth()->id(),
        ]);

        $this->reset();

        $this->dispatch('notify', message: 'Post created');
    }
};

?>

<div>
    <form wire:submit="save">
        <input type="text" wire:model="title" placeholder="Title">
        @error('title') <span class="error">{{ $message }}</span> @enderror

        <textarea wire:model="body" placeholder="Body"></textarea>
        @error('body') <span class="error">{{ $message }}</span> @enderror

        <button type="submit">Save post</button>
    </form>
</div>

מאפיינים ציבוריים זמינים במרקאפ כמשתנים רגילים — {{ $title }} עובד בדיוק כמו בקובץ תצוגה נפרד.

לגבי האימוג'י

שם הקובץ כברירת מחדל מתחיל בקידומת ⚡. זהו סימון ויזואלי שמבדיל קומפוננטות Livewire מקומפוננטות Blade רגילות באותה תיקייה, והוא אופציונלי — אפשר לכבות אותו ב-config/livewire.php אם כלי הפיתוח, הטרמינל או הצוות שלכם מעדיפים שמות פשוטים.

קומפוננטות מרובות קבצים

כשקומפוננטה גדלה מעבר למה שנוח להחזיק בקובץ אחד, צרו אותה כקומפוננטה מרובת קבצים וקבלו שוב מחלקה, תצוגה ובדיקה מופרדות.

Bash
php artisan make:livewire post.create --mfc

קומפוננטות מבוססות מחלקה עדיין עובדות

app/Livewire/CreatePost.php
<?php

namespace App\Livewire;

use Livewire\Component;

class CreatePost extends Component
{
    public string $title = '';

    public function render()
    {
        return view('livewire.create-post');
    }
}
מה לבחור. קובץ יחיד מנצח בקומפוננטות קטנות ובינוניות — טופס, שורת טבלה, ווידג'ט. בחרו במבוססת מחלקה או במרובת קבצים כשהמחלקה מכילה לוגיקה של ממש, יש לה הרבה תלויות, או כשלצוות שלכם כבר יש מוסכמות סביב app/Livewire.

4. מיקומים, שמות ו-namespaces

קומפוננטות Livewire כבר לא יושבות כברירת מחדל בסילו נפרד משלהן ב-resources/views/livewire; הן נמצאות לצד שאר קומפוננטות ה-Blade שלכם.

מבנה תיקיות
resources/views/
├── components/
│   ├── ⚡counter.blade.php              → <livewire:counter />
│   ├── post/
│   │   ├── ⚡create.blade.php           → <livewire:post.create />
│   │   └── ⚡index.blade.php            → <livewire:post.index />
│   └── button.blade.php                 (a plain Blade component)
└── pages/
    └── ⚡dashboard.blade.php            → <livewire:pages::dashboard />

רינדור

Blade
{{-- By name --}}
<livewire:counter />

{{-- Nested directories use dots --}}
<livewire:post.create />

{{-- Namespaced --}}
<livewire:pages::post.create />

{{-- With props --}}
<livewire:post.create :title="$initialTitle" :author="$user" />
יש לסגור תגיות של קומפוננטות. ב-v4 תגית <livewire:some-component> שלא נסגרה פשוט לא מתרנדרת — בלי שגיאה ובלי פלט. תמיד השתמשו בסגירה עצמית: <livewire:some-component />. זו ההפתעה הנפוצה ביותר בהעברת קבצי Blade מ-v3.

ניתוב לקומפוננטה

קומפוננטות עמוד מלא מקבלות מאקרו ניתוב ייעודי. הצורה הישנה Route::get('/dashboard', Dashboard::class) מוחלפת ב:

routes/web.php
use App\Livewire\Dashboard;

// By class
Route::livewire('/dashboard', Dashboard::class);

// By component name — the usual choice for view-based components
Route::livewire('/dashboard', 'pages::dashboard')
    ->middleware('auth')
    ->name('dashboard');

Layouts

config/livewire.php
'component_layout' => 'layouts::app',
PHP
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;

new class extends Component {
    #[Layout('layouts::app')]
    #[Title('Dashboard')]
    public function render() { /* … */ }
};

5. מאפיינים ו-props

מאפיינים ציבוריים הם עדיין ה-state של הקומפוננטה, עדיין עוברים סריאליזציה ל-snapshot בין בקשות, ועדיין כפופים לאותן מגבלות טיפוסים: ערכים סקלריים, מערכים של סקלרים, מודלים וקולקציות של Eloquent, Carbon, enums.

PHP
new class extends Component {
    public string $name = '';
    public ?int $age = null;
    public array $tags = [];
    public bool $isPublic = false;
};

Props מהאב: #[Prop]

ערכים שמועברים מתבנית האב אפשר להצהיר במפורש. כך הממשק הציבורי של הקומפוננטה קריא במבט אחד, ונוצרת הפרדה בין "זה מגיע מבחוץ" לבין "זה state פנימי".

resources/views/components/⚡alert.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $type = 'info';

    #[Prop]
    public bool $dismissible = false;
};

?>

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $slot }}

    @if ($dismissible)
        <button wire:click="$dispatch('dismiss')">×</button>
    @endif
</div>
Blade
<livewire:alert type="warning" dismissible class="mt-4">
    The invoice is overdue.
</livewire:alert>

mount() עדיין רץ ראשון

PHP
use App\Models\Order;

new class extends Component {
    public Order $order;
    public string $mode;

    public function mount(Order $order, string $mode = 'compact'): void
    {
        $this->order = $order;
        $this->mode  = $mode;
    }
};

#[Locked] חשוב כתמיד

PHP
use Livewire\Attributes\Locked;

new class extends Component {
    #[Locked]
    public int $invoiceId;   // the client cannot substitute another id

    public string $note = '';
};
שום דבר ב-v4 לא משנה את מודל האיומים: כל מאפיין ציבורי ניתן לכתיבה מצד הלקוח אלא אם נעלתם אותו, וכל מתודה ציבורית היא נקודת קצה נגישה. ראו פרק 25.

6. wire:model בגרסה 4

זה השינוי שהכי סביר שינשוך אתכם. שני היבטים של wire:model מתנהגים אחרת ב-v4, ושניהם נכשלים בשקט ולא ברעש.

שינוי 1: המודיפיירים שולטים כעת בסנכרון בצד הלקוח

ב-v3, .blur ו-.change קבעו רק מתי נשלחת בקשת רשת. ב-v4 הם קובעים מתי הערך מסתנכרן בכלל — כולל בצד הלקוח. כדי לקבל את ההתנהגות הישנה, הוסיפו .live.

Blade
{{-- Livewire 3 --}}
<input wire:model.blur="title">

{{-- The v4 equivalent of that behaviour --}}
<input wire:model.live.blur="title">

שינוי 2: אין יותר bubbling של אירועים מאלמנטים פנימיים

wire:model על אלמנט עוטף כבר לא תופס אירועים שמבעבעים משדות קלט מקוננים. אם הסתמכתם על זה, הוסיפו .deep.

Blade
{{-- Livewire 3: the wrapper caught the input's event --}}
<div wire:model="value">
    <input type="text">
</div>

{{-- Livewire 4: opt back in explicitly --}}
<div wire:model.deep="value">
    <input type="text">
</div>

כל השאר לא השתנה

Blade
{{-- Deferred by default --}}
<input type="text" wire:model="name">

{{-- Request on every change --}}
<input type="text" wire:model.live="search">

{{-- Debounced live search --}}
<input type="text" wire:model.live.debounce.400ms="search">

{{-- Nested data --}}
<input wire:model="form.address.city">
<input wire:model="post.title">
קישור למאפיין של מודל (post.title) עדיין מחייב חוק ולידציה תואם. זו ההגנה מפני mass assignment, והיא עברה ל-v4 ללא שינוי.

7. Actions, async ו-renderless

Actions מוצהרים בדיוק כמו קודם — מתודות ציבוריות שנקראות מהמרקאפ.

PHP
new class extends Component {
    public function save()
    {
        $this->validate();
        // …
    }
};
Blade
<button wire:click="save">Save</button>

<form wire:submit="save">…</form>

<button wire:click="delete({{ $post->id }})"
        wire:confirm="Delete this post? This cannot be undone.">Delete</button>

Actions ללא רינדור

כש-action לא משנה שום דבר שהמשתמש רואה — הגדלת מונה צפיות, כתיבת שורת audit — דילוג על הרינדור מחדש חוסך מעבר מלא על התבנית והשוואת DOM.

PHP
use Livewire\Attributes\Renderless;

new class extends Component {
    #[Renderless]
    public function incrementViewCount(): void
    {
        $this->post->increment('views');
    }
};
Blade
{{-- Or from the template side --}}
<button type="button" wire:click.renderless="incrementViewCount">Track</button>

המקבילה האימפרטיבית בתוך מתודה היא עדיין $this->skipRender().

Actions אסינכרוניים

#[Async] (או המודיפייר .async) מריץ action במקביל, מחוץ לתור הבקשות הרגיל. אידיאלי לעבודת fire-and-forget: פינגים לאנליטיקה, לוגים, חימום cache.

PHP
use Livewire\Attributes\Async;

new class extends Component {
    #[Async]
    public function logInteraction(string $element): void
    {
        Analytics::record($element, auth()->id());
    }
};
Blade
<button wire:click.async="logInteraction('cta-hero')">Get started</button>
לעולם אל תשתמשו ב-action אסינכרוני שמשנה state שמוצג בממשק. מכיוון שהוא עוקף את תור הבקשות, התוצאה שלו עלולה להגיע שלא לפי הסדר ביחס לעדכונים אחרים, ומתקבל state שסותר את מה שעל המסך. Async מיועד לתופעות לוואי שהממשק לא קורא בחזרה.

Magic actions

Blade
<button wire:click="$refresh">Refresh</button>
<button wire:click="$set('tab', 'settings')">Settings</button>
<button wire:click="$toggle('showFilters')">Filters</button>
<button wire:click="$dispatch('open-modal', { name: 'create' })">New</button>
<button wire:click="$parent.closeModal()">Close</button>
האבטחה לא זזה. בצעו הרשאה לכל פרמטר בצד השרת, וסמנו מתודות עזר כ-protected או private כדי שלא ניתן יהיה להפעיל אותן מהלקוח.

8. מחזור חיים ו-hooks

מחזור החיים לא השתנה מאז גרסה 3. הכרת הסדר עדיין מעלימה את רוב הבאגים המבלבלים.

רינדור ראשוני
1. Component instance created
2. boot()
3. mount($params)
4. booted()
5. render()
עדכון עוקב
 1. POST /livewire-{hash}/update arrives with the state snapshot
 2. Snapshot checksum verified
 3. Component instance created
 4. boot()
 5. State restored from the snapshot
 6. hydrate() / hydrateFoo()
 7. booted()
 8. updating($prop, $value) / updatingFoo($value)
 9. Properties receive new values
10. updated($prop, $value) / updatedFoo($value)
11. Queued actions run
12. rendering() → render() → rendered($view, $html)
13. dehydrate() — state packed back into the snapshot
14. Response: markup + effects
PHP
new class extends Component {
    public float $price = 0;

    public function boot(): void {}
    public function booted(): void {}
    public function hydrate(): void {}
    public function dehydrate(): void {}

    public function updating(string $property, mixed $value): void {}

    public function updated(string $property, mixed $value): void
    {
        $this->validateOnly($property);
    }

    // Property-specific
    public function updatedPrice(mixed $value): void
    {
        $this->price = round((float) $value, 2);
    }

    // Nested key form.email
    public function updatedFormEmail(mixed $value): void
    {
        $this->form['email'] = strtolower(trim($value));
    }
};
Islands משנים את כמה מתרנדר, לא את סדר ה-hooks האלה. עדכון של island עדיין מריץ את מחזור החיים המלא בשרת — הוא פשוט מחזיר קטע במקום את הקומפוננטה כולה.

9. ולידציה ו-Form objects

הוולידציה עוברת מ-v3 ללא שינוי, כולל attributes ו-Form objects.

PHP
use Livewire\Attributes\Validate;

new class extends Component {
    #[Validate('required|string|min:2|max:80')]
    public string $name = '';

    #[Validate('required|email:rfc,dns')]
    public string $email = '';

    #[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();
    }
};

חוקים מותנים

PHP
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',
    ];
}

Form objects

app/Livewire/Forms/PostForm.php
<?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 = '';

    public function setPost(Post $post): void
    {
        $this->post  = $post;
        $this->title = $post->title;
        $this->body  = $post->body;
    }

    public function store(): Post
    {
        $this->validate();

        return Post::create($this->except('post'));
    }

    public function update(): void
    {
        $this->validate();

        $this->post->update($this->except('post'));
    }
}
קומפוננטה בקובץ יחיד שמשתמשת בו
<?php

use Livewire\Component;
use App\Livewire\Forms\PostForm;

new class extends Component {
    public PostForm $form;

    public function save()
    {
        $this->form->post ? $this->form->update() : $this->form->store();

        $this->redirect(route('posts.index'), navigate: true);
    }
};

?>

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

שגיאות ב-JavaScript

גרסה 4 חושפת את שגיאות הוולידציה ללקוח דרך ה-magic property $errors.

Blade
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>

10. Islands

Islands הם היכולת הדגל של Livewire 4, וזו שמשנה את הדרך שבה חושבים על גודל קומפוננטה. island הוא אזור בתוך קומפוננטה שמתרנדר מחדש באופן עצמאי: כשהוא מתעדכן, רק הקטע הזה מחושב מחדש ומוטלא — לא הקומפוננטה כולה.

Blade
@island
    <div>Revenue: {{ $this->revenue }}</div>
@endisland

למה זה חשוב

ב-v3 הפתרון המקובל ל"הדשבורד הזה איטי" היה לפצל אותו לשש קומפוננטות בת, כדי שעדכון ווידג'ט אחד לא יריץ מחדש את השאילתות של חמשת האחרים. Islands נותנים את הבידוד הזה בלי גבול של קומפוננטה: קומפוננטה אחת, מחלקה אחת, כמה אזורים שמתעדכנים באופן עצמאי.

דשבורד עם שלושה islands
<?php

use Livewire\Component;
use Livewire\Attributes\Computed;

new class extends Component {
    #[Computed]
    public function revenue()
    {
        return Order::whereMonth('created_at', now()->month)->sum('total');
    }

    #[Computed]
    public function queue()
    {
        return Job::pending()->count();
    }

    #[Computed]
    public function feed()
    {
        return Activity::latest()->take(20)->get();
    }
};

?>

<div>
    @island(name: 'revenue')
        <div class="card">
            <h3>Revenue this month</h3>
            <p class="stat">{{ number_format($this->revenue, 2) }} €</p>
        </div>
    @endisland

    @island(name: 'queue', poll: '5s')
        <div class="card">
            <h3>Jobs pending</h3>
            <p class="stat">{{ $this->queue }}</p>
        </div>
    @endisland

    @island(name: 'feed', lazy: true)
        @placeholder
            <div class="card skeleton animate-pulse h-64"></div>
        @endplaceholder

        <div class="card">
            @foreach ($this->feed as $activity)
                <div wire:key="activity-{{ $activity->id }}">{{ $activity->summary }}</div>
            @endforeach
        </div>
    @endisland
</div>

אפשרויות

אפשרותהשפעה
nameמזהה את ה-island כדי ש-actions ו-JavaScript יוכלו לפנות אליו. כמה islands עם אותו שם תמיד מתרנדרים כקבוצה.
lazyמתרנדר כשה-island נגלל לתוך שטח התצוגה (intersection observer).
deferמתרנדר מיד אחרי טעינת העמוד, בלי קשר לנראות.
alwaysמאלץ את ה-island להתעדכן בכל פעם שהאב מתרנדר מחדש.
skipמדלג לגמרי על הרינדור הראשוני.

פנייה ל-island מתוך action

Blade
@island(name: 'revenue')
    Revenue: {{ $this->revenue }}
@endisland

<button wire:click="$refresh" wire:island="revenue">Refresh revenue</button>

Append ו-prepend — גלילה אינסופית בלי צנרת

Blade
@island(name: 'feed')
    @foreach ($this->posts as $post)
        <article wire:key="post-{{ $post->id }}">{{ $post->title }}</article>
    @endforeach
@endisland

<button wire:click="loadMore" wire:island.append="feed">Load more</button>

אותו דבר, מונע מ-Alpine או מ-JavaScript רגיל:

Blade
<button x-on:click="$wire.$island('feed', { mode: 'append' }).loadMore()">
    Load more
</button>

Polling מוגבל ל-island

Blade
@island(name: 'queue')
    <div wire:poll.3s>
        Jobs pending: {{ $this->queue }}
    </div>
@endisland

ה-poll מרענן רק את ה-island. השוו זאת ל-v3, שבה wire:poll על דשבורד הריץ מחדש כל שאילתה בעמוד כל כמה שניות.

Islands לעומת קומפוננטות בת. השתמשו ב-island כשהאזור חולק את ה-state של האב וצריך רק רינדור מבודד. השתמשו בקומפוננטת בת כשהאזור צריך state משלו, מחזור חיים משלו, או כשהוא נמצא בשימוש חוזר בכמה מקומות.

11. Slots והעברת מאפיינים

רכיבי Livewire יכולים כעת לקבל תוכן slot בדיוק כמו רכיבי Blade — עם הבדל אחד חשוב: תוכן ה-slot מוערך בהקשר של רכיב האב. wire:click שנכתב בתוך slot קורא למתודה של רכיב האב, לא של רכיב הבן.

Slot ברירת מחדל

תבנית רכיב האב
<livewire:modal>
    <h2>Create a post</h2>

    <form wire:submit="save">
        <input wire:model="title">
        <button type="submit">Save</button>
    </form>
</livewire:modal>
resources/views/components/⚡modal.blade.php
<?php

use Livewire\Component;

new class extends Component {
    public bool $isOpen = false;

    public function toggle(): void
    {
        $this->isOpen = ! $this->isOpen;
    }
};

?>

<div wire:show="isOpen" class="modal">
    <button wire:click="toggle" class="modal__close">×</button>

    <div class="modal__body">
        {{ $slot }}
    </div>
</div>

בדוגמה זו wire:submit="save" ו-wire:model="title" שייכים לרכיב האב, בעוד wire:click="toggle" שייך ל-modal. זו בדיוק החלוקה הרצויה: ה-modal אחראי על פתיחה וסגירה, ורכיב האב אחראי על הטופס.

Slots בעלי שם

תבנית רכיב האב
<livewire:modal>
    <wire:slot name="header">
        <h2>Create a post</h2>
    </wire:slot>

    <form wire:submit="save">
        <input wire:model="title">
    </form>

    <wire:slot name="footer">
        <button wire:click="save">Save</button>
        <button wire:click="$parent.close()">Cancel</button>
    </wire:slot>
</livewire:modal>
בתוך הרכיב
<div class="modal">
    @if ($header = $slot('header'))
        <div class="modal__header">{{ $header }}</div>
    @endif

    <div class="modal__body">{{ $slot }}</div>

    @if ($footer = $slot('footer'))
        <div class="modal__footer">{{ $footer }}</div>
    @endif
</div>

העברת מאפיינים

בדיוק כמו רכיבי Blade, רכיב Livewire יכול לקבל ולמזג מאפייני HTML שרירותיים דרך $attributes.

resources/views/components/⚡alert.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $type = 'info';
};

?>

<div {{ $attributes->merge(['class' => 'alert alert-'.$type]) }}>
    {{ $slot }}
</div>
Blade
<livewire:alert type="danger" class="mt-6" data-testid="overdue-alert">
    This invoice is 14 days overdue.
</livewire:alert>
כשמרנדרים רכיב עמוד מלא, ניתן למקם slots בעלי שם המיועדים ל-layout מחוץ לאלמנט השורש של הרכיב.

12. רכיבים מקוננים ו-wire:key

קינון עובד כמו ב-v3: כל רכיב בן הוא יחידה עצמאית עם state משלו ומחזור עדכון משלו.

Blade
<div>
    @foreach ($orders as $order)
        <livewire:order-row :order="$order" :key="'order-'.$order->id" />
    @endforeach
</div>

Smart wire keys

גרסה 4 מפעילה את smart_wire_keys כברירת מחדל, כך ש-Livewire מסיק מפתחות במצבי לולאה רבים שבעבר דרשו מפתח מפורש. זה מצמצם קוד שבלוני, אך לא הופך את המפתחות למיותרים.

המשיכו לכתוב מפתחות כשהזהות חשובה. כל רשימה שניתן לשנות את סדרה, לסנן אותה או למחוק ממנה חלקית צריכה לשאת wire:key מפורש הקשור ל-id של הרשומה. Smart keys הם נוחות למקרים הפשוטים, לא תחליף לכך שתגידו ל-framework מהי שורה בפועל.
Blade
@foreach ($rows as $row)
    <div wire:key="row-{{ $row->id }}">…</div>
@endforeach

Props ריאקטיביים

PHP
use Livewire\Attributes\Reactive;

new class extends Component {
    #[Reactive]
    public int $quantity;
};

קריאה לרכיב האב

Blade
<button wire:click="$parent.refreshList()">Refresh the list</button>

קישור דו-כיווני על רכיב

PHP
use Livewire\Attributes\Modelable;

new class extends Component {
    #[Modelable]
    public int $value = 0;
};
Blade
<livewire:rating-input wire:model.live="review.rating" />
כשיש islands, יש פחות סיבות לפנות לרכיב בן. אם הסיבה היחידה לפיצול הייתה "שירונדר מחדש בנפרד", island קל יותר.

13. אירועים

ה-API של האירועים לא השתנה מאז גרסה 3.

PHP
// שליחה
$this->dispatch('post-created');
$this->dispatch('post-created', postId: $post->id, title: $post->title);
$this->dispatch('refresh')->to(OrderList::class);
$this->dispatch('recalculate')->self();
PHP
use Livewire\Attributes\On;

new class extends Component {
    #[On('post-created')]
    public function onPostCreated(int $postId, string $title): void
    {
        // …
    }
};
Blade
<button wire:click="$dispatch('open-modal', { name: 'create-order' })">New order</button>

אירועים לדפדפן

PHP
$this->dispatch('notify', type: 'success', message: 'Order saved');
Blade
<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>
Islands מחליפים לעיתים קרובות אירועים. הדפוס הקלאסי של v3 "רכיב A שולח אירוע, רכיב B מאזין ומתרענן" הופך פעמים רבות ל"שני האזורים הם islands ברכיב אחד, והפעולה מכוונת ל-island השני" — בקשה אחת במקום שתיים.

14. Computed properties

#[Computed] עובד כמו ב-v3 ומשתלב היטב במיוחד עם islands: island שקורא computed property מחשב אותו מחדש רק כשה-island הזה מתעדכן.

PHP
use Livewire\Attributes\Computed;

new class extends Component {
    public array $items = [];

    #[Computed]
    public function products()
    {
        return Product::whereIn('id', array_keys($this->items))->get();
    }

    #[Computed]
    public function subtotal(): float
    {
        return $this->products->sum(
            fn ($product) => $product->price * $this->items[$product->id]
        );
    }

    #[Computed]
    public function total(): float
    {
        return round($this->subtotal * 1.2, 2);
    }
};
Blade
@island(name: 'totals')
    <p>Subtotal: {{ number_format($this->subtotal, 2) }} €</p>
    <p>Total incl. VAT: {{ number_format($this->total, 2) }} €</p>
@endisland

Cache מעבר לבקשה

PHP
#[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();
}

ניקוי ה-cache

PHP
unset($this->products, $this->subtotal, $this->total);
ב-view, computed property נכתב כ-$this->products, לעולם לא $products.

15. State ב-URL וב-session

PHP
use Livewire\Attributes\Url;
use Livewire\Attributes\Session;

new class extends Component {
    #[Url]
    public string $search = '';

    #[Url(as: 'cat')]
    public ?int $categoryId = null;

    #[Url(except: '')]
    public string $sort = 'popular';

    #[Url(keep: true)]
    public int $perPage = 24;

    #[Url(history: true)]
    public string $tab = 'all';

    #[Session]
    public bool $sidebarCollapsed = false;

    #[Session(key: 'admin.table.density')]
    public string $density = 'comfortable';
};

התוצאה: /catalog?search=laptop&cat=5&sort=price-asc&tab=sale

הודעות flash

PHP
public function save(): void
{
    $this->validate();

    Setting::updateOrCreate(['key' => 'theme'], ['value' => $this->theme]);

    session()->flash('status', 'Settings saved');
}
Blade
@if (session('status'))
    <div class="alert alert-success">{{ session('status') }}</div>
@endif
כל property עם #[Url] קורא ל-History API בכל שינוי. עבור שדות שמשתנים בכל הקשה, שלבו אותו עם wire:model.live.debounce.

16. העלאת קבצים

העלאות עובדות כמו בגרסה 3, באמצעות ה-trait WithFileUploads.

resources/views/components/⚡avatar-uploader.blade.php
<?php

use Livewire\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\Validate;

new class extends Component {
    use WithFileUploads;

    #[Validate('required|image|mimes:jpg,jpeg,png,webp|max:4096')]
    public $avatar;

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

?>

<div>
    <form wire:submit="save">
        <input type="file" wire:model="avatar" accept="image/*">

        <div wire:loading wire:target="avatar">Uploading…</div>

        @if ($avatar)
            <img src="{{ $avatar->temporaryUrl() }}" alt="Preview" class="preview">
        @endif

        @error('avatar') <p class="error">{{ $message }}</p> @enderror

        <button type="submit" wire:loading.attr="disabled">Save</button>
    </form>
</div>

סרגל התקדמות מדויק

Blade
<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="progress">
        <div class="progress__bar" :style="`width: ${progress}%`"></div>
    </div>
</div>
config/livewire.php
'temporary_file_upload' => [
    'disk'            => 's3',
    '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 ב-PHP ו- client_max_body_size ב-nginx חייבים כולם לעלות על כלל ה-max: שלכם, אחרת ההעלאות נכשלות ללא שגיאה קריאה.

17. עימוד

PHP
use Livewire\WithPagination;
use Livewire\Attributes\Url;

new class extends Component {
    use WithPagination;

    #[Url]
    public string $search = '';

    public string $sortField = 'created_at';
    public string $sortDirection = 'desc';

    public function updatedSearch(): 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 with(): array
    {
        return [
            'orders' => Order::query()
                ->with('customer')
                ->when($this->search, fn ($q) => $q->where('number', 'like', "%{$this->search}%"))
                ->orderBy($this->sortField, $this->sortDirection)
                ->paginate(25),
        ];
    }
};

Islands הופכים עימוד לגלילה אינסופית

Blade
<div>
    <input type="search" wire:model.live.debounce.400ms="search">

    @island(name: 'rows')
        @foreach ($orders as $order)
            <div wire:key="order-{{ $order->id }}">{{ $order->number }}</div>
        @endforeach
    @endisland

    <button wire:click="nextPage" wire:island.append="rows">Load more</button>
</div>
בטבלאות גדולות paginate() עולה COUNT(*) נוסף. אם אינכם מציגים את מספר העמודים הכולל — ובטעינה בסגנון append בדרך כלל לא — simplePaginate() זול משמעותית.

18. מצבי טעינה

משפחת wire:loading לא השתנתה, ו-v4 מוסיפה מאפייני data-loading לעיצוב.

Blade
<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>
<div wire:loading wire:target.except="search">Updating…</div>

<button wire:click="save" wire:loading.attr="disabled">Save</button>
<button wire:click="save" wire:loading.class="opacity-50 cursor-wait">Save</button>

{{-- הצגת האינדיקטור רק אם הבקשה נמשכת יותר מהסף --}}
<div wire:loading.delay>Loading…</div>
<div wire:loading.delay.long>Loading…</div>

עיצוב מ-CSS

CSS
[data-loading] .btn {
    opacity: 0.5;
    pointer-events: none;
}

שינויים שלא נשמרו וקישוריות

Blade
<input wire:model="title">

<span wire:dirty wire:target="title">You have unsaved changes</span>

<div wire:offline class="banner banner--warn">
    No connection to the server. Changes are not being saved.
</div>

19. Optimistic UI בצד הלקוח

סוג שלם של אינטראקציות מעולם לא נזקק לשרת: פתיחה וסגירה של פאנל, הצגת מונה תווים, צביעת שדה באדום כשהוא ארוך מדי. גרסה 4 מעניקה להם directives מובנים שמעדכנים את ה-DOM מיידית, ללא בקשה כלל.

wire:show

Blade
{{-- מחליף נראות דרך CSS, מיידית, ללא סבב מול השרת --}}
<div wire:show="showModal" class="modal">…</div>

<button wire:click="$toggle('showModal')">Open</button>

wire:text

Blade
{{-- תוכן הטקסט עוקב אחרי ה-property בצד הלקוח --}}
<span wire:text="title"></span>

<input wire:model="title">

wire:bind

Blade
<textarea wire:model="message" maxlength="280"></textarea>

<span wire:text="message.length"></span> / 280

<span wire:bind:class="message.length > 240 && 'text-red-500'">
    Getting long
</span>

פעולות צד לקוח בלבד עם $js

Blade
<?php

new class extends Component {
    public function save() { /* … */ }
};

?>

<div>
    <button wire:click="save">Save</button>

    {{-- רץ בצד הלקוח בלבד --}}
    <button x-on:click="$wire.$js.showToast = true">Show toast</button>
</div>
אופטימי אינו סמכותי. ה-directives האלה משנים את מה שהמשתמש רואה לפני שהשרת אישר. כל מה שמשפיע על נתונים, הרשאות או כסף עדיין חייב לעבור ולידציה ולהתבצע בצד השרת — ה-state בצד הלקוח הוא תצוגה מקדימה, לא מקור אמת.
כלל טוב: wire:show / wire:text למשוב שהמשתמש צריך להרגיש מיידית; action רגיל לכל מה שחייב להישמר.

20. גרירה ושחרור

שינוי סדר פירושו היה בעבר להוסיף את SortableJS, לחבר את ה-callbacks שלו ל-action של Livewire ולשמור על סנכרון בין שני המודלים. בגרסה 4 זה directive.

Blade
<ul wire:sort="reorder">
    @foreach ($tasks as $task)
        <li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            {{ $task->title }}
        </li>
    @endforeach
</ul>
PHP
new class extends Component {
    public function reorder(array $order): void
    {
        foreach ($order as $position => $id) {
            Task::where('id', $id)->update(['position' => $position]);
        }
    }
};

ידיות גרירה

Blade
<ul wire:sort="reorder">
    @foreach ($tasks as $task)
        <li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            <span wire:sort:handle class="cursor-grab">⠿</span>

            <span>{{ $task->title }}</span>

            {{-- אלמנטים אינטראקטיביים לא אמורים להתחיל גרירה --}}
            <button wire:sort:ignore wire:click="delete({{ $task->id }})">Delete</button>
        </li>
    @endforeach
</ul>

גרירה בין רשימות

Blade
<div class="board">
    @foreach ($columns as $column)
        <ul wire:sort="moveCard" wire:sort:group="board" wire:key="column-{{ $column->id }}">
            @foreach ($column->cards as $card)
                <li wire:sort:item="{{ $card->id }}" wire:key="card-{{ $card->id }}">
                    {{ $card->title }}
                </li>
            @endforeach
        </ul>
    @endforeach
</div>
שמרו את הסדר. ה-directive משנה את סדר ה-DOM באופן אופטימי ומעביר לכם את הרצף החדש — כתיבתו למסד הנתונים היא באחריותכם. אם ה-handler זורק חריגה, הממשק והנתונים לא תואמים עד הרינדור המלא הבא.

21. CSS וסקריפטים מקומיים לרכיב

מאחר שרכיב הוא עכשיו קובץ אחד, גם הסגנונות והסקריפטים שלו שייכים לאותו קובץ. Livewire מגביל את ה-CSS לרכיב, כך שהוא לא יכול לדלוף לשאר הדף.

resources/views/components/⚡pricing-card.blade.php
<?php

use Livewire\Component;
use Livewire\Attributes\Prop;

new class extends Component {
    #[Prop]
    public string $plan = 'starter';
};

?>

<div class="card">
    <h3 class="title">{{ ucfirst($plan) }}</h3>
    <p class="price">{{ $this->price }} € / month</p>
</div>

<style>
    /* Scoped to this component — no global .title collision */
    .card  { border: 1px solid #e4e2da; border-radius: 14px; padding: 24px; }
    .title { font-size: 20px; font-weight: 700; }
    .price { color: #2d6a4f; }
</style>

סגנונות גלובליים במכוון

Blade
<style global>
    :root { --brand: #f53004; }
</style>

סקריפטים של רכיב

Blade
@script
<script>
    const chart = new Chart(document.getElementById('sales'), {
        type: 'line',
        data: @json($chartData),
    });

    $wire.on('data-updated', ({ points }) => {
        chart.data.datasets[0].data = points;
        chart.update();
    });
</script>
@endscript
Blade
{{-- Load a third-party library once for the whole page --}}
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets
CSS מקומי הוא נוחות לעיצוב ברמת הרכיב, לא תחליף למערכת עיצוב. שמרו את ה-tokens, הטיפוגרפיה ואבני הבניין של הפריסה בגיליון הסגנונות הגלובלי; ברכיב השאירו רק את הכללים הבודדים שנוגעים לו בלבד.

23. אינטגרציה עם JavaScript

Alpine.js עדיין מגיע עם Livewire, ו-$wire נשאר הגשר ממצב הרכיב אל JavaScript.

Blade
<div x-data="{ open: false }">
    <button x-on:click="open = !open">Details</button>

    <div x-show="open">
        <p x-text="$wire.title"></p>

        <button x-on:click="$wire.title = 'New title'">Rename</button>
        <button x-on:click="$wire.save()">Save</button>
        <button x-on:click="await $wire.calculate(); open = false">Calculate</button>
    </div>
</div>
Blade
{{-- Two-way binding into Alpine --}}
<div x-data="{ query: $wire.entangle('search') }">
    <input x-model="query">
</div>

wire:ref — מתן שמות לאלמנטים ולרכיבים

Blade
<livewire:modal wire:ref="modal" />

<button x-on:click="$refs.modal.open()">Open the modal</button>

#[Json] — החזרת נתונים ישירות ל-JavaScript

PHP
use Livewire\Attributes\Json;

new class extends Component {
    #[Json]
    public function searchSuggestions(string $term): array
    {
        return Product::search($term)->take(5)->pluck('name')->all();
    }
};
Blade
<input x-on:input.debounce.300ms="suggestions = await $wire.searchSuggestions($event.target.value)">

Interceptors

גרסה 4 מחליפה את ה-hooks של v3, commit ו-request, ב- interceptMessage() וב-interceptRequest().

JavaScript
document.addEventListener('livewire:init', () => {

    Livewire.interceptRequest(({ options, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();   // the CSRF token expired
            }
        });
    });

    Livewire.interceptMessage(({ component, succeed }) => {
        succeed(() => {
            console.debug('component updated', component.name);
        });
    });
});
JavaScript
// Programmatic access
Livewire.dispatch('refresh-orders');
Livewire.find('component-id').call('save');
Livewire.all().forEach((component) => component.$refresh());

התעלמות מתת-עץ

Blade
<div wire:ignore>
    <select id="select2-field">…</select>
</div>

<div wire:ignore.self>…</div>

24. בדיקות

Pest היא הדרך המומלצת לבדוק רכיבי Livewire 4. רכיבים מבוססי view יכולים להחזיק את הבדיקה שלהם ליד הרכיב, במקום בעץ תיקיות נפרד.

Bash
php artisan make:livewire post.create --test
# resources/views/components/post/create.test.php

בדיקת רכיב מבוסס view

פנו לרכיב לפי השם שלו בכתיב נקודות, ולא לפי מחלקה:

PHP
use Livewire\Livewire;

it('renders', function () {
    Livewire::test('post.create')
        ->assertStatus(200);
});

it('validates required fields', function () {
    Livewire::test('post.create')
        ->set('title', '')
        ->set('body', 'too short')
        ->call('save')
        ->assertHasErrors(['title' => 'required']);
});

it('stores a valid post', function () {
    Livewire::actingAs($user)
        ->test('post.create')
        ->set('title', 'A perfectly reasonable title')
        ->set('body', str_repeat('Body copy that is long enough. ', 3))
        ->call('save')
        ->assertHasNoErrors()
        ->assertDispatched('notify');

    expect(Post::where('title', 'A perfectly reasonable title')->exists())->toBeTrue();
});

רכיבים מבוססי מחלקה

PHP
use App\Livewire\Counter;

it('increments', function () {
    Livewire::test(Counter::class)
        ->assertSet('count', 0)
        ->call('increment')
        ->assertSet('count', 1);
});

ה-assertions העיקריים

מתודהמה נבדק
assertSet() / assertNotSet()ערכי מאפיינים
assertSee() / assertDontSee()הפלט המרונדר
assertViewHas()נתונים שהועברו ל-view
assertHasErrors() / assertHasNoErrors()ולידציה
assertDispatched()אירועים
assertRedirect()הפניות
assertUnauthorized() / assertForbidden()הרשאות
assertStatus()סטטוס HTTP
PHP
// Uploads
use Illuminate\Http\UploadedFile;

Livewire::test('avatar-uploader')
    ->set('avatar', UploadedFile::fake()->image('avatar.jpg', 400, 400))
    ->call('save')
    ->assertHasNoErrors();

// A component inside a page
$this->get('/dashboard')
    ->assertSeeLivewire('pages::dashboard');
בדקו קודם כול הרשאות בתוך actions, כללי ולידציה ומעברי מצב. שם הפונקציונליות נשברת ושם מסתתרות פרצות האבטחה — islands ו-slots לא משנים את זה.

25. אבטחה וביצועים

מודל האיומים זהה ל-v3. כל מתודה ציבורית היא endpoint פתוח ב-HTTP, וכל מאפיין ציבורי ניתן לכתיבה מצד הלקוח. רכיבים בקובץ יחיד קלים יותר לסריקה מהירה — ולכן גם קל יותר לפספס בסריקה כזו בדיקת הרשאה חסרה.

רשימת בדיקות אבטחה

  • $this->authorize() בכל action שמשנה נתונים.
  • כל המזהים מסומנים ב-#[Locked].
  • מתודות עזר מוגדרות כ-protected או private, כדי שהלקוח לא יוכל לקרוא להן.
  • כללי הוולידציה לא כוללים שדות פנימיים (user_id, role, price).
  • אין tokens, מפתחות או מידע אישי מיותר במאפיינים ציבוריים — ה-snapshot עובר לדפדפן כטקסט גלוי.
  • העלאות מוגבלות לפי סוג MIME וגודל.
  • טפסי התחברות ויצירת קשר מאחורי rate limiter.
  • {!! !!} רק עבור markup מהימן או מחוטא.
  • Optimistic UI לעולם לא נחשב למקור האמת.
PHP
public function delete(int $postId): void
{
    $post = Post::findOrFail($postId);

    $this->authorize('delete', $post);

    $post->delete();
}

ביצועים

תסמיןהפתרון ב-v4
עדכון של ווידג'ט מריץ מחדש כל שאילתה בדףעטפו את האזורים ב-@island
markup ענק בדףהוציאו קולקציות של מודלים מהמאפיינים הציבוריים; שלפו אותן ב-with() או ב-#[Computed]
בקשה על כל הקשהwire:model.live.debounce.400ms
עומס מ-pollingpoll בתוך island, והוסיפו .visible
round trip לשרת רק כדי לפתוח/לסגור פאנלwire:show במקום action
הגדלת מונה מרנדרת מחדש את הדף#[Renderless]
שאילתות N+1eager loading עם with(); Model::preventLazyLoading() בסביבת הפיתוח
ציור ראשון איטי בגלל בלוק כבד@island(lazy: true) עם @placeholder
app/Providers/AppServiceProvider.php
public function boot(): void
{
    Model::preventLazyLoading(! app()->isProduction());
}

26. פתרון תקלות ודף עזר מקוצר

הרכיב לא מרונדר בכלל

התגית לא סגורה עצמית. ב-v4, <livewire:my-component> לא מפיק כלום — בשקט. כתבו <livewire:my-component />.

בפרודקשן הכול מחזיר 404, אבל מקומית זה עובד

ה-endpoint של Livewire עבר מ-/livewire/ ל-/livewire-{hash}/. כלל firewall, חריגת WAF, כלל CDN או location של nginx שעדיין מקובעים לנתיב הישן יחסמו את בקשות העדכון.

שדה הפסיק להסתנכרן אחרי השדרוג

wire:model.blur ו-.change שולטים עכשיו גם בסנכרון בצד הלקוח, ולא רק ברשת. הוסיפו .live: wire:model.live.blur="title".

עטיפה הפסיקה לקלוט את ה-input שלה

Event bubbling כבוי כברירת מחדל. השתמשו ב-wire:model.deep.

ה-modifiers של wire:transition לא עושים כלום

.opacity, .scale ו-.duration.200ms הוסרו — v4 משתמש ב-View Transitions API המובנה בדפדפן. הגדירו את האנימציה ב-CSS במקום.

island אף פעם לא מתעדכן

  • ל-island אין name, אבל ה-action מכוון ל-island בשם.
  • wire:island="…" מצביע על שם שלא קיים בפלט המרונדר.
  • האזור תלוי במצב של ההורה שלא באמת השתנה — הוסיפו always: true.

תוכן ה-slot קורא למתודה הלא נכונה

זה בכוונה: ה-markup של ה-slot מוערך בהקשר של ההורה. כדי לקרוא לרכיב הילד, עברו דרך אירוע או דרך wire:ref.

רכיבי Volt הפסיקו לעבוד

Volt שולב בליבה. החליפו את Livewire\Volt\Component ב-Livewire\Component, את Volt::route() ב-Route::livewire(), את Volt::test() ב- Livewire::test(), והסירו את ה-service provider ואת החבילה של Volt.

דף עזר לדירקטיבות

דירקטיבהייעודחדש ב-v4
wire:modelקישור שדה למאפייןהסמנטיקה השתנתה
wire:model.deepקליטת אירועים מאלמנטים צאצאיםכן
wire:click / wire:submitקריאה ל-action—
wire:click.asyncהרצת ה-action במקבילכן
wire:click.renderlessדילוג על הרינדור מחדשכן
wire:islandכיוון action ל-islandכן
wire:island.appendהוספה ל-island במקום החלפהכן
wire:showהצגה/הסתרה בצד הלקוחכן
wire:textקישור תוכן טקסט בצד הלקוחכן
wire:bindקישור ריאקטיבי של attributeכן
wire:sortמיון מחדש ב-drag-and-dropכן
wire:intersectפעולה כשהאלמנט נכנס ל-viewportכן
wire:refמתן שם לאלמנט או לרכיב עבור JSכן
wire:navigateניווט בסגנון SPA—
wire:navigate:scrollשמירת הגלילה בקונטיינרשם חדש
wire:loading / wire:dirty / wire:offlineמצב הבקשה—
wire:pollרענון תקופתימוגבל ל-island
wire:keyזהות אלמנט בלולאהsmart keys מופעלים כברירת מחדל
wire:ignoreהחרגת תת-עץ מ-morphing—

דירקטיבות Blade

דירקטיבהייעוד
@island … @endislandאזור שמתרנדר באופן עצמאי
@placeholder … @endplaceholderplaceholder ל-island עצל
<wire:slot name="…">תוכן של slot בעל שם
@script … @endscriptJavaScript מקומי לרכיב
@assets … @endassetsנכסים ברמת הדף שנטענים פעם אחת
@persist … @endpersistשמירת אלמנט בין מעברי ניווט
<style> / <style global>CSS מקומי / גלובלי של הרכיב

Attributes של PHP

Attributeייעוד
#[Validate]כלל ולידציה
#[Locked]חסימת כתיבה מה-frontend
#[Computed]ערך נגזר שנשמר במטמון
#[Url] / #[Session]שימור מצב
#[On]מאזין לאירועים
#[Prop]הגדרת prop שמגיע מההורה
#[Reactive] / #[Modelable]קישור הורה–ילד
#[Async]הרצת ה-action במקביל
#[Renderless]דילוג על רינדור מחדש
#[Json]החזרת נתונים ישירות ל-JavaScript
#[Lazy] / #[Layout] / #[Title]טעינה ומעטפת הדף

פקודות Artisan

Bash
php artisan make:livewire post.create              # single-file
php artisan make:livewire post.create --mfc        # multi-file
php artisan make:livewire post.create --test       # with a test
php artisan make:livewire pages::dashboard         # namespaced
php artisan livewire:publish --config
php artisan optimize:clear

משאבים רשמיים

Livewire 4 מתפתח באופן פעיל. במקרה של סתירה בין התיעוד הזה לתיעוד הרשמי, התיעוד הרשמי קובע — בדקו אותו לפני שאתם מסתמכים על פרט כלשהו בפרודקשן.
מותאם מהתיעוד הרשמי

אם יש סתירה בין התיעוד הזה למקור הרשמי, המקור הרשמי צודק. מצאתם שגיאה? ספרו לנו.