【Laravel實戰】25分鐘搞懂在Livewire使用AlpineJS

【Laravel實戰】25分鐘搞懂在Livewire使用AlpineJS

在許多情況下,前端頁面互動並不保證能對伺服器進行完整的請求流程,比如開啟或關閉一個 Modal 視圖

在這種情況下, AlpineJS 是 Livewire 最完美的夥伴

它讓我們可以以聲明式/反應式的方式將JavaScript直接添加到你的網頁標記中,就如同Vue.js那樣。看起來 Livewire 真的很想讓自己變成PHP版本的Vue.js

安裝AlpineJS

你如果想要在 Livewire 裡頭使用 Alpine 就必須先安裝它。要安裝 Alpine,請加入以下 script 標籤到視圖檔的 <head> 區塊

<head>
    ...
    <script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.8.0/dist/alpine.min.js" defer></script>
      <!-- "defer"屬性是非常重要的,用來確保 Livewire 能夠先載入. -->
</head>

如需了解更多的安裝資訊,請參考 Alpine 的文件

在Livewire 裡頭使用 Alpine

這是一個在 Livewire 組件視圖檔案使用 AlpineJS 做出下拉選單功能的例子

<div>
    ...

    <div x-data="{ open: false }">
        <button @click="open = true">Show More...</button>

        <ul x-show="open" @click.away="open = false">
            <li><button wire:click="archive">Archive</button></li>
            <li><button wire:click="delete">Delete</button></li>
        </ul>
    </div>
</div>

萃取出可重複使用的 Blade 組件

假如你還不熟悉 Livewire 和 AlpineJS,那麼在程式碼中混合這兩者的語法可能會造成你混淆

因此有一個策略可以使用的是,將 Alpine 語法的部分包成可重複使用的 Blade 組件,然後將它們用在 Livewire 或者是應用當中

以下是一個 Livewire 視圖例子,用的是 Laravel 7 Blade 組件標籤指令

<div>
    ...

    <x-dropdown>
        <x-slot name="trigger">
            <button>Show More...</button>
        </x-slot>

        <ul>
            <li><button wire:click="archive">Archive</button></li>
            <li><button wire:click="delete">Delete</button></li>
        </ul>
    </x-dropdown>
</div>

可重複使用的 Blade "dropdown" 組件:

<div x-data="{ open: false }">
    <span @click="open = true">{{ $trigger }}</span>

    <div x-show="open" @click.away="open = false">
        {{ $slot }}
    </div>
</div>

現在,Livewire 和 Alpine 的語法已經完成分離了,而且你已經有了一個可重複使用的 Blade 組件來用在其他 Livewire 組件

從 Alpine 來與 Livewire 進行互動: $wire

從任何 Livewire 組件裡頭的 Alpine 組件,你能夠取用魔法 $wire 物件來取用甚至是控制 Livewire 組件

為了示範該如何使用,我們將在 Alpine 創造一個 "counter" 組件,並用它來透過 hood 來控制 Livewire

//app/Http/Livewire/Counter.php

class Counter extends Component
{
    public $count = 0;

    public function increment()
    {
        $this->count++;
    }
}
//resources/views/livewire/counter.blade.php

<div>
    <!-- Alpine Counter Component -->
    <div x-data>
        <h1 x-text="$wire.count"></h1>

        <button x-on:click="$wire.increment()">Increment</button>
    </div>
</div>

現在當使用者按下 "Increment" 按鈕,一個標準的 Livewire 流程將會觸發,而且 Alpine 將會呈現 Livewire 的新 $count 值

因為 $wire 在 hood 裡頭使用了 JavaScript 代理,因此你能夠取用 Livewire 的屬性以及方法,而且這些操作都將傳給 Livewire。除了這些功能之外,$wire 也提供你一些內建方法讓你使用

以下是 $wire 完整的 API

// 取得 Livewire 的屬性 $wire.foo

// 呼叫 Livewire 的方法 $wire.someMethod(someParam)

// 呼叫 Livewire 的方法,並用其回傳值來做事情 $wire.someMethod(someParam) .then(result => { ... })

// 呼叫 Livewire 的方法,並用 async/await(異步的方式) 來儲存其回應 let foo = await $wire.getFoo()

// 發出一個名為 "some-event" 的 Livewire 事件並傳入兩個參數 $wire.emit('some-event', 'foo', 'bar')

// 偵聽一個名為 "some-event" 的 Livewire 事件 $wire.on('some-event', (foo, bar) => {})

// 取得 Livewire 屬性 $wire.get('property')

// 設定 Livewire 屬性為某個指定值 $wire.set('property', value)

// 呼叫 Livewire 的行動(方法) $wire.call('someMethod', param)

// 取得 Livewire 組件底層的 JavaScript 實例 $wire.__instance

在 Livewire 和 Alpine 之間分享狀態 :@entangle

Livewire 有一個極為強大的功能稱為 "entangle" 允許你把 Livewire 和 Alpine 屬性綁定在一起。透過綁定,當一個值改變,被綁定的另一個值也跟著改變

為了示範,同樣使用之前的 dropdown 範例,但這次 show 屬性將在 livewire 和 Alpine 之間綁定在一起,我們現在可以在兩者之間控制 dropdown 的狀態

//ap/Http/Livewire/Dropdown.php

class Dropdown extends Component
{
    public $showDropdown = false;

    public function archive()
    {
        ...
        $this->showDropdown = false;
    }

    public function delete()
    {
        ...
        $this->showDropdown = false;
    }
}
//resources/views/livewire/dropdown.blade.php

<div x-data="{ open: @entangle('showDropdown') }">
    <button @click="open = true">Show More...</button>

    <ul x-show="open" @click.away="open = false">
        <li><button wire:click="archive">Archive</button></li>
        <li><button wire:click="delete">Delete</button></li>
    </ul>
</div>

現在使用者可透過 Alpine 開啟 dropdown,但當他按下一個 Livewire 行動,比如 "Archive" 將會告知 Livewire 去關閉 dropdown。不管是 Alpine 又或者是 Livewire 都能夠控制其對應的屬性,那與之對應的屬性也會跟著變更

有時,不必在每次 Alpine 更改時都更新Livewire,而你希望將更改與下一個發出的 Livewire 請求捆綁在一起。在這些情況下,你可以像這樣加上 .defer 屬性

<div x-data="{ open: @entangle('showDropdown').defer }">
    ...

現在,當使用者切換下拉菜單的打開和關閉狀態時,將不會對 Livewire 發送 AJAX 請求,但是當通過 “archive” 或 “delete” 之類的按鈕觸發 Livewire 操作時,“showDropdown” 的新狀態將變為與請求捆綁在一起

假如你分不清兩者的差異,開啟瀏覽器的開發者工具,從 XHR 請求來觀察 .defer 有沒有加的差異

從 Blade 組件取用 Livewire 語句

在 Livewire 應用中提取可重複使用的 Blade 組件是一種基本模式

在 Livewire 上下文中實現 Blade 組件時,你可能會遇到的一個困難是從組件內部訪問屬性值,例如 wire:model

例如,你可能創造了一個文字輸入項 Blade 組件,像這樣:

使用
<x-inputs.text wire:model="foo"/>

定義
<div>
    <input type="text" {{ $attributes }}>
</div>

像這樣的簡單 Blade 組件可以運作良好, Laravel 和 Blade 將自動轉發添加到組件的任何其他屬性(在這種情況下,如wire:model),並將它們放置在 標籤上,因為我們回應了屬性包($ attributes)

但是,有時你可能需要提取有關傳遞給組件的 Livewire 屬性之更多詳細資料。針對這種情況, Livewire 提供了一個 $attributes->wire() 來幫助你搞定這些工作

比如下面的例子:

<x-inputs.text wire:model.defer="foo" wire:loading.class="opacity-25"/>

你能夠像這樣從 Blade 的 $attribute 屬性包取得 Livewire 語句訊息

$attributes->wire('model')->value(); // "foo"
$attributes->wire('model')->modifiers(); // ["defer"]
$attributes->wire('model')->hasModifier('defer'); // true
$attributes->wire('loading')->hasModifier('class'); // true
$attributes->wire('loading')->value(); // "opacity-25"

你也能夠個別的轉發這些 Livewire 語句,例如:

<x-inputs.text wire:model.defer="foo" wire:loading.class="opacity-25"/>

你能夠像這樣去轉發 "wire:model.defer="foo" 語句
<input type="text" {{ $attributes->wire('model') }}>

輸出
<input type="text" wire:model.defer="foo">

使用此工具的方式有很多,但是一種常見的例子是將其與上述的 @entangle 指令結合在一起使用

<x-dropdown wire:model="show">
    <x-slot name="trigger">
        <button>Show</button>
    </x-slot>

    Dropdown Contents
</x-dropdown>
定義
<div x-data="{ open: @entangle($attributes->wire('model')) }">
    <span @click="open = true">{{ $trigger }}</span>

    <div x-show="open" @click.away="open = false">
        {{ $slot }}
    </div>
</div>

注意

假如 .defer 修飾子是透過 wire:model.defer 來傳入, @entangle 語句將自動進行分析並在 hood 時加入 @entangle('...').defer 修飾子

建立一個 Datepicker 組件

Livewire 裡頭的 JavaScript 的常見應用是自定義表單輸入,比如日期選擇器,顏色選擇器等之類的組件對於你的應用通常是必不可少的

通過使用與上面相同的模式(並添加一些額外的小變化),我們可以利用 Alpine 輕鬆地與這些類型的 JavaScript 組件進行交互

讓我們創建一個稱為 date-picker 的可重複使用的 Blade 組件,我們可以透過它使用 wire:model 將某些數據綁定到 Livewire 中以示範你可以如何使用它:

<form wire:submit.prevent="schedule">
    <label for="title">Event Title</label>
    <input wire:model="title" id="title" type="text">

    <label for="date">Event Date</label>
    <x-date-picker wire:model="date" id="date"/>

    <button>Schedule Event</button>
</form>

對於此組件,我們將使用 Pikaday 函式庫。根據文件,該套件包的最基本用法如下所示範:

<input type="text" id="datepicker">

<script>
    new Pikaday({ field: document.getElementById('datepicker') })
</script>

你只需要加入一個元素, Pikaday 將為你添加所有額外的日期選擇器行為

現在,讓我們看看如何利用這個函式庫來編寫可重複使用的日期選擇器 Blade 組件:

<input
    x-data
    x-ref="input"
    x-init="new Pikaday({ field: $refs.input })"
    type="text"
    {{ $attributes }}
>

注意:

{{$ attributes}}表達式是 Laravel 7 及更高版本中的一種機制,用於轉發在組件標籤上聲明的額外HTML屬性

轉發 wire:model 輸入事件

在 hood 裡頭, wire:model 每次派發輸入事件在元素上或元素下時,都會添加一個事件偵聽器以更新屬性。在 Livewire 和 Alpine 之間進行通信的另一種方法是使用 Alpine 派發一個輸入事件,該事件具有在其上具有 wire:model 的元素內或元素上的一些數據

請看這個例子,當使用者按下第一個按鈕時,名為 $foo 的屬性設置為bar,而當使用者按下第二個按鈕時,$foo 設置為baz。

Livewire 組件視圖:

<div>
    <div wire:model="foo">
        <button x-data @click="$dispatch('input', 'bar')">Set to "bar"</button>
        <button x-data @click="$dispatch('input', 'baz')">Set to "baz"</button>
    </div>
</div>

一個更常見的例子是創建一個"顏色選擇器" Blade 組件,該組件可能會在 Livewire 組件中使用選色器組件用法:

<div>
    <x-color-picker wire:model="color"/>
</div>

為了定義組件,我們將使用名為 Vanilla Picker 的第三方顏色選擇器函式庫,本範例假定你已將該函式庫加載到頁面上,選色器 Blade組件定義:

<div
    x-data="{ color: '#ffffff' }"
    x-init="
        picker = new Picker($refs.button);
        picker.onDone = rawColor => {
            color = rawColor.hex;
            $dispatch('input', color)
        }
    "
    wire:ignore
    {{ $attributes }}
>
    <span x-text="color" :style="`background: ${color}`"></span>
    <button x-ref="button">Change</button>
</div>
Color-picker Blade Component Definition (Commented):

<div
    x-data="{ color: '#ffffff' }"
    x-init="
        // Wire up to show the picker when clicking the 'Change' button.
        picker = new Picker($refs.button);
        // Run this callback every time a new color is picked.
        picker.onDone = rawColor => {
            // Set the Alpine 'color' property.
            color = rawColor.hex;
            // Dispatch the color property for 'wire:model' to pick up.
            $dispatch('input', color)
        }
    "
    // Vanilla Picker will attach its own DOM inside this element, so we need to
    // add `wire:ignore` to tell Livewire to skip DOM-diffing for it.
    wire:ignore
    // Forward the any attributes added to the component tag like `wire:model=color`
    {{ $attributes }}
>
    <!-- Show the current color value with the backgound color set to the chosen color. -->
    <span x-text="color" :style="`background: ${color}`"></span>
    <!-- When this button is clicked, the color-picker dialogue is shown. -->
    <button x-ref="button">Change</button>
</div>

忽略 DOM-改動(使用 wire:ignore)

幸運的是,像 Pikaday 這樣的函式庫在頁面末尾添加了其額外的DOM。許多其他函式庫在初始化DOM後就立即對其進行操作,並在與它們交互時繼續使DOM發生變化

發生這種情況時, Livewire 很難跟踪要在組件更新中保留哪些DOM操作以及要丟棄哪些DOM操作

要告訴 Livewire 忽略組件中 HTML 子集的更改,可以添加 wire:ignore 指令

Select2 是接管一部分DOM的函式庫之一(它將