【Laravel實戰】Livewire實作示範-表單驗證
快速實作

步驟1 . 建立 ContactForm 組件
首先我們先來建立 Livewire 組件,名為 ContactForm
php artisan livewire:make ContactForm
(此動作將生成 app/Http/Livewire/ContactForm.php 以及 resources/views/livewire/contact-form.blade.php)
步驟2 . 將表單移入組件視圖內
這一步驟,我們需要將原來視圖裡頭的表單移到組件內的視圖內,這樣才能夠得到組件類別的公開屬性資料
//resources\views\livewire\contact-form.blade.php
<div class="relative bg-white mt-8">
<div class="absolute inset-0">
<div class="absolute inset-y-0 left-0 w-1/2 bg-gray-50"></div>
</div>
<div class="relative max-w-7xl mx-auto lg:grid lg:grid-cols-5">
<div class="bg-gray-50 py-16 px-4 sm:px-6 lg:col-span-2 lg:px-8 lg:py-24 xl:pr-12">
<div class="max-w-lg mx-auto">
<h2 class="text-2xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-3xl sm:leading-9">
與哥布林聯絡
</h2>
<p class="mt-3 text-lg leading-6 text-gray-500">
Nullam risus blandit ac aliquam justo ipsum. Quam mauris volutpat massa dictumst amet. Sapien tortor
lacus arcu.
</p>
<dl class="mt-8 text-base leading-6 text-gray-500">
<div>
<dt class="sr-only">Postal address</dt>
<dd>
<p>新北市板橋區中山路一號</p>
</dd>
</div>
<div class="mt-6">
<dt class="sr-only">Phone number</dt>
<dd class="flex">
<svg class="flex-shrink-0 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span class="ml-3">
+1 (555) 123-4567
</span>
</dd>
</div>
<div class="mt-3">
<dt class="sr-only">Email</dt>
<dd class="flex">
<svg class="flex-shrink-0 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="ml-3">
info@goblinlab.org
</span>
</dd>
</div>
</dl>
<p class="mt-6 text-base leading-6 text-gray-500">
尋找工作機會?
<a href="#" class="font-medium text-gray-700 underline">檢視目前所有開放職缺</a>.
</p>
</div>
</div>
<div class="bg-white py-16 px-4 sm:px-6 lg:col-span-3 lg:py-24 lg:px-8 xl:pl-12">
<div class="max-w-lg mx-auto lg:max-w-none">
<form action="/send-mail" method="POST" class="grid grid-cols-1 row-gap-6">
@csrf
@if (session()->has('successMessage'))
<div class="rounded-md bg-green-50 p-4 mt-8">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm leading-5 font-medium text-green-800">
{{ session()->get('successMessage') }}
</p>
</div>
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button
type="button"
class="inline-flex rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:bg-green-100 transition ease-in-out duration-150"
aria-label="Dismiss">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
@endisset
<div>
<label for="name" class="sr-only">姓名</label>
<div class="relative rounded-md shadow-sm">
<input id="name" name="name" value="{{ old('name') }}"
class="@error('name')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="姓名">
</div>
@error('name')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="sr-only">Email</label>
<div class="relative rounded-md shadow-sm">
<input id="email" type="text" name="email" value="{{ old('email') }}"
class="@error('email')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="Email">
</div>
@error('email')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="phone" class="sr-only">電話</label>
<div class="relative rounded-md shadow-sm">
<input id="phone" name="phone" value="{{ old('phone') }}"
class="@error('phone')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="電話">
</div>
@error('phone')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="message" class="sr-only">訊息</label>
<div class="relative rounded-md shadow-sm">
<textarea id="message" rows="4" name="message"
class="@error('message')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="訊息">{{ old('message') }}</textarea>
</div>
@error('message')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div class="">
<span class="inline-flex rounded-md shadow-sm">
<button type="submit"
class="inline-flex items-center justify-center py-3 px-6 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out disabled:opacity-50">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span>提交</span>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
記得要保留最外層的·<div> ,因為要確保根元素只有一個。要不然雖然畫面呈現看似正常,實際上JS腳本是無法正常執行的
//resources/views/form-validation.blade.php
@extends('layouts.app')
@section('content')
<div>
<div class="h-96"></div>
<div class="h-96"></div>
</div>
<h2 class="text-lg font-semibold">標準聯絡表單</h2>
<livewire:contact-form />
@endsection
完成後,試看看頁面是否正常
步驟3 .編輯組件視圖
在這一個步驟,你不妨試看看為 wire:model 加入修飾子後綴,看看將會有怎樣的差異
- wire:model.debounce.500ms (將 Ajax 請求間隔改為 500 微秒,預設為 250 微秒 )
- wire:model.lazy (當輸入項取消專注時才會進行請求)
- wire:model.defer (只有當表單提交時才會進行請求)
//resources\views\livewire\contact-form.blade.php
<div>
<div class="relative bg-white mt-8">
<div class="absolute inset-0">
<div class="absolute inset-y-0 left-0 w-1/2 bg-gray-50"></div>
</div>
<div class="relative max-w-7xl mx-auto lg:grid lg:grid-cols-5">
<div class="bg-gray-50 py-16 px-4 sm:px-6 lg:col-span-2 lg:px-8 lg:py-24 xl:pr-12">
<div class="max-w-lg mx-auto">
<h2 class="text-2xl leading-8 font-extrabold tracking-tight text-gray-900 sm:text-3xl sm:leading-9">
與哥布林聯絡
</h2>
<p class="mt-3 text-lg leading-6 text-gray-500">
Nullam risus blandit ac aliquam justo ipsum. Quam mauris volutpat massa dictumst amet. Sapien tortor
lacus arcu.
</p>
<dl class="mt-8 text-base leading-6 text-gray-500">
<div>
<dt class="sr-only">Postal address</dt>
<dd>
<p>新北市板橋區中山路一號</p>
</dd>
</div>
<div class="mt-6">
<dt class="sr-only">Phone number</dt>
<dd class="flex">
<svg class="flex-shrink-0 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
<span class="ml-3">
+1 (555) 123-4567
</span>
</dd>
</div>
<div class="mt-3">
<dt class="sr-only">Email</dt>
<dd class="flex">
<svg class="flex-shrink-0 h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<span class="ml-3">
info@goblinlab.org
</span>
</dd>
</div>
</dl>
<p class="mt-6 text-base leading-6 text-gray-500">
尋找工作機會?
<a href="#" class="font-medium text-gray-700 underline">檢視目前所有開放職缺</a>.
</p>
</div>
</div>
<div class="bg-white py-16 px-4 sm:px-6 lg:col-span-3 lg:py-24 lg:px-8 xl:pl-12">
<div class="max-w-lg mx-auto lg:max-w-none">
<form wire:submit.prevent="submitForm" action="/send-mail" method="POST" class="grid grid-cols-1 row-gap-6">
@csrf
@if (session()->has('successMessage'))
<div class="rounded-md bg-green-50 p-4 mt-8">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm leading-5 font-medium text-green-800">
{{ session()->get('successMessage') }}
</p>
</div>
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button
type="button"
wire:click="$set('successMessage', null)"
class="inline-flex rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:bg-green-100 transition ease-in-out duration-150"
aria-label="Dismiss">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
@endisset
<div>
<label for="name" class="sr-only">姓名</label>
<div class="relative rounded-md shadow-sm">
<input wire:model.defer="name" id="name" name="name" value="{{ old('name') }}"
class="@error('name')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="姓名">
</div>
@error('name')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="sr-only">Email</label>
<div class="relative rounded-md shadow-sm">
<input wire:model.defer="email" id="email" type="text" name="email" value="{{ old('email') }}"
class="@error('email')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="Email">
</div>
@error('email')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="phone" class="sr-only">電話</label>
<div class="relative rounded-md shadow-sm">
<input wire:model.defer="phone" id="phone" name="phone" value="{{ old('phone') }}"
class="@error('phone')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="電話">
</div>
@error('phone')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="message" class="sr-only">訊息</label>
<div class="relative rounded-md shadow-sm">
<textarea wire:model.defer="message" id="message" rows="4" name="message"
class="@error('message')border border-red-500 @enderror form-input block w-full py-3 px-4 placeholder-gray-500 transition ease-in-out duration-150"
placeholder="訊息">{{ old('message') }}</textarea>
</div>
@error('message')
<p class="text-red-500 mt-1">{{ $message }}</p>
@enderror
</div>
<div class="">
<span class="inline-flex rounded-md shadow-sm">
<button type="submit"
class="inline-flex items-center justify-center py-3 px-6 border border-transparent text-base leading-6 font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:border-indigo-700 focus:shadow-outline-indigo active:bg-indigo-700 transition duration-150 ease-in-out disabled:opacity-50">
<svg wire:loading wire:target="submitForm" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
<span>提交</span>
</button>
</span>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
步驟4 .編輯 Livewire 組件類別
在這步驟,我們將要為組件類別加入公開屬性,使之能夠在組件視圖內被存取,並利用 reset() 來重置公開屬性。除此之外,我們先暫時移除表單驗證功能,等會再加回來
//app\Http\Livewire\ContactForm.php
<?php
namespace App\Http\Livewire;
use Livewire\Component;
use App\Mail\ContactFormMailable;
use Illuminate\Support\Facades\Mail;
class ContactForm extends Component
{
public $name;
public $email;
public $phone;
public $message;
public function render()
{
return view('livewire.contact-form');
}
public function submitForm()
{
// $contact = $request->validate([
// 'name' => 'required',
// 'email' => 'required|email',
// 'phone' => 'required',
// 'message' => 'required',
// ]);
$contact['name'] = $this->name;
$contact['email'] = $this->email;
$contact['phone'] = $this->phone;
$contact['message'] = $this->message;
Mail::to('info@goblinlab.org')->send(new ContactFormMailable($contact));
//重置表單
$this->reset(['name', 'email', 'phone', 'message']);
//返回並以 Session 來傳送訊息
session()->flash('successMessage', '我們已經收到你的訊息,將盡快與你聯絡,感謝!');
}
}
步驟5 .將訊息顯示改為透過公開屬性
在這一步驟,我們試著將訊息顯示的功能改從原先的 Session 存取改為透過公開屬性存取
//app\Http\Livewire\ContactForm.php
public $success_message;
public function submitForm()
{
...
$this->success_message = '我們已經收到你的訊息,將盡快與你聯絡,感謝!';
}
//resources\views\livewire\contact-form.blade.php
@if($success_message)
<div class="rounded-md bg-green-50 p-4 mt-8">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<p class="text-sm leading-5 font-medium text-green-800">
{{ $success_message }}
</p>
</div>
<div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5">
<button wire:click="$set('success_message',null)" type="button"
class="inline-flex rounded-md p-1.5 text-green-500 hover:bg-green-100 focus:outline-none focus:bg-green-100 transition ease-in-out duration-150"
aria-label="Dismiss">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
</div>
</div>
@endif
步驟6 .加回驗證功能
現在我們將原先的驗證做法改為利用 Livewire 的驗證功能,透過 $this->validate()
//app\Http\Livewire\ContactForm.php
public function submitForm(){
$contact = $this->validate([
'name' => 'required',
'email' => 'required|email',
'phone' => 'required',
'message' => 'required',
]);
//$contact['name'] = $this->name;
//$contact['email'] = $this->email;
//$contact['phone'] = $this->phone;
//$contact['message'] = $this->message;
...
}
步驟7 .改成即時驗證
所謂即時驗證指的是並非等到表單提交才驗證,而是在輸入項編輯過程中進入即時驗證。 流程有二: 1.建立 $rules 屬性 2.宣告 updated()
//app\Http\Livewire\ContactForm.php
protected $rules = [
'name' => 'required',
'email' => 'required|email',
'phone' => 'required',
'message' => 'required|min:5',
];
public function updated($propertyName)
{
$this->validateOnly($propertyName);
}
public function submitForm(){
$contact = $this->validate();
...
}
步驟8 . 加入運行動畫
在這一步驟,我們利用 Livewire 來實作運行動畫,透過加入以下流程
1.加入 wire:loading 2.加入 wire:target="submitForm"
//resources\views\livewire\contact-form.blade.php
<svg wire:loading wire:target="submitForm" class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z">
</path>
</svg>
至此,我們已經完成了組件的表單驗證功能
功能測試
這個環節,我們來看看如何撰寫測試程式來測看看剛才我們所撰寫的功能
步驟 1.建立測試用例
所謂測試用例指的就是用來測試程式的程式碼
php artisan make:test ContactFormTest
步驟 2.編輯測試用例
在這一步,我們一一加入測試方法,要特別注意到每個方法的前綴必須以 test 開頭,否則是不會被加以呼叫執行的
//tests/Feature/ContactFormTest.php
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use Livewire\Livewire;
use App\Http\Livewire\ContactForm;
use App\Mail\ContactFormMailable;
use Illuminate\Support\Facades\Mail;
class ContactFormTest extends TestCase
{
public function test_the_page_contain_contact_form_livewire_component()
{
$this->get('/form-validate')
->assertSeeLivewire('contact-form');
}
public function test_form_sends_out_an_email()
{
Mail::fake();
Livewire::test(ContactForm::class)
->set('name', 'Goblin')
->set('email', 'demo@goblinlab.org')
->set('phone', '0911234567')
->set('message', 'Today is my day!!')
->call('submitForm')
->assertSee('我們已經收到你的訊息,將盡快與你聯絡,感謝!');
Mail::assertSent(function (ContactFormMailable $mail) {
$mail->build();
return $mail->hasTo('info@goblinlab.org') &&
$mail->hasFrom('demo@goblinlab.org') &&
$mail->subject == '聯絡表單通知';
});
}
public function test_form_name_field_is_required()
{
Livewire::test(ContactForm::class)
->set('email', 'demo@goblinlab.org')
->set('phone', '0911234567')
->set('message', 'Today is my day!!')
->call('submitForm')
->assertHasErrors(['name' => 'required']);
}
public function test_form_message_field_has_minium_chars()
{
Livewire::test(ContactForm::class)
->set('message', 'abc')
->assertHasErrors(['message' => 'min']);
}
}
步驟 3.進行測試
要執行測試有多種方式,在這裡我為你介紹最簡單的一種,就是直接呼叫 test 命令
php artisan test



