表单
表单是大多数 Web 应用的核心,因此 Livewire 提供了大量实用工具来构建表单。从处理简单的输入元素,到实时验证、文件上传等复杂场景,Livewire 都有简洁且文档完善的工具,帮你更轻松地开发,并让用户体验更好。
下面开始。
提交表单
我们先看 post.create 组件里一个非常简单的表单。它有两个文本输入框和一个提交按钮,后端还有管理表单状态与提交的代码:
<?php // resources/views/components/post/⚡create.blade.php
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public $title = '';
public $content = '';
public function save()
{
Post::create(
$this->only(['title', 'content'])
);
session()->flash('status', 'Post successfully updated.');
return $this->redirect('/posts');
}
};
?>
<form wire:submit="save">
<input type="text" wire:model="title">
<input type="text" wire:model="content">
<button type="submit">Save</button>
</form>可以看到,我们用 wire:model 把表单里的公开属性 $title 和 $content「绑定」起来。这是 Livewire 最常用、也最强大的功能之一。
除了绑定 $title 和 $content,我们还用 wire:submit 在点击「Save」按钮时捕获 submit 事件,并调用 save() action。该 action 会把表单输入持久化到数据库。
新文章写入数据库后,我们会把用户重定向到文章列表页,并显示一条「flash」消息,提示新文章已创建。
添加验证
为避免存入不完整或危险的用户输入,大多数表单都需要某种输入验证。
在 Livewire 中验证表单很简单:在需要验证的属性上方加上 #[Validate] 属性即可。
属性一旦带上 #[Validate],只要该属性在服务端被更新,就会对其值应用对应的验证规则。
我们给 post.create 组件中的 $title 和 $content 加上一些基础验证规则:
<?php // resources/views/components/post/⚡create.blade.php
use Livewire\Attributes\Validate; // [tl! highlight]
use Livewire\Component;
use App\Models\Post;
new class extends Component {
#[Validate('required')] // [tl! highlight]
public $title = '';
#[Validate('required')] // [tl! highlight]
public $content = '';
public function save()
{
$this->validate(); // [tl! highlight]
Post::create(
$this->only(['title', 'content'])
);
return $this->redirect('/posts');
}
};我们还会修改 Blade 模板,在页面上显示验证错误。
<form wire:submit="save">
<input type="text" wire:model="title">
<div>
@error('title') <span class="error">{{ $message }}</span> @enderror <!-- [tl! highlight] -->
</div>
<input type="text" wire:model="content">
<div>
@error('content') <span class="error">{{ $message }}</span> @enderror <!-- [tl! highlight] -->
</div>
<button type="submit">Save</button>
</form>现在,如果用户未填写任何字段就提交表单,会看到验证消息,提示在保存文章前哪些字段为必填。
Livewire 还有更多验证功能。更多信息请参阅专门的验证文档页。
抽取表单对象
若表单较大,希望把属性、验证逻辑等抽到单独的类中,Livewire 提供了 form objects(表单对象)。
表单对象让你可以在多个组件间复用表单逻辑,并把与表单相关的代码集中到单独的类中,从而让组件类更干净。
你可以手写表单类,也可以使用便捷的 artisan 命令:
php artisan livewire:form PostForm上述命令会创建 app/Livewire/Forms/PostForm.php 文件。
我们把 post.create 组件改写成使用 PostForm 类:
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
class PostForm extends Form
{
#[Validate('required|min:5')]
public $title = '';
#[Validate('required|min:5')]
public $content = '';
}<?php // resources/views/components/post/⚡create.blade.php
use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public PostForm $form; // [tl! highlight]
public function save()
{
$this->validate();
Post::create(
$this->form->only(['title', 'content']) // [tl! highlight]
);
return $this->redirect('/posts');
}
};<form wire:submit="save">
<input type="text" wire:model="form.title">
<div>
@error('form.title') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model="form.content">
<div>
@error('form.content') <span class="error">{{ $message }}</span> @enderror
</div>
<button type="submit">Save</button>
</form>如果愿意,也可以把创建文章的逻辑抽到表单对象中,例如:
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use App\Models\Post;
use Livewire\Form;
class PostForm extends Form
{
#[Validate('required|min:5')]
public $title = '';
#[Validate('required|min:5')]
public $content = '';
public function store() // [tl! highlight:5]
{
$this->validate();
Post::create($this->only(['title', 'content']));
}
}现在可以在组件中调用 $this->form->store():
<?php // resources/views/components/post/⚡create.blade.php
use App\Livewire\Forms\PostForm;
use Livewire\Component;
new class extends Component {
public PostForm $form;
public function save()
{
$this->form->store(); // [tl! highlight]
return $this->redirect('/posts');
}
// ...
};若希望同一个表单对象同时用于创建和更新,可以轻松改造成同时支持这两种场景。
下面展示如何在 post.edit 组件中使用同一个表单对象,并用初始数据填充:
<?php // resources/views/components/post/⚡edit.blade.php
use App\Livewire\Forms\PostForm;
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public PostForm $form;
public function mount(Post $post)
{
$this->form->setPost($post);
}
public function save()
{
$this->form->update();
return $this->redirect('/posts');
}
};<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Livewire\Form;
use App\Models\Post;
class PostForm extends Form
{
public ?Post $post;
#[Validate('required|min:5')]
public $title = '';
#[Validate('required|min:5')]
public $content = '';
public function setPost(Post $post)
{
$this->post = $post;
$this->title = $post->title;
$this->content = $post->content;
}
public function store()
{
$this->validate();
Post::create($this->only(['title', 'content']));
}
public function update()
{
$this->validate();
$this->post->update(
$this->only(['title', 'content'])
);
}
}可以看到,我们给 PostForm 对象加了 setPost() 方法,可选地用已有数据填充表单,并把 post 保存在表单对象上供后续使用。同时还加了 update() 方法用于更新已有文章。
使用 Livewire 时并不强制使用表单对象,但它们提供了不错的抽象,能帮你把组件里重复的样板代码清掉。
重置表单字段
若使用了表单对象,可能希望在提交后重置表单。可以调用 reset() 方法:
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use App\Models\Post;
use Livewire\Form;
class PostForm extends Form
{
#[Validate('required|min:5')]
public $title = '';
#[Validate('required|min:5')]
public $content = '';
// ...
public function store()
{
$this->validate();
Post::create($this->only(['title', 'content']));
$this->reset(); // [tl! highlight]
}
}也可以把属性名传给 reset(),只重置指定属性:
$this->reset('title');
// Or multiple at once...
$this->reset(['title', 'content']);reset() 会把每个属性恢复为表单类上声明的状态。若带类型的属性没有默认值,重置后会处于未初始化状态。若重置后马上需要读取或渲染这些字段,请给它们提供默认值:
public string $title = '';
public ?string $subtitle = null;或者,在调用 reset() 之后、读取之前再给属性赋值。
拉取表单字段
你也可以用 pull() 方法,在一次操作中同时取出表单属性并重置它们。
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use App\Models\Post;
use Livewire\Form;
class PostForm extends Form
{
#[Validate('required|min:5')]
public $title = '';
#[Validate('required|min:5')]
public $content = '';
// ...
public function store()
{
$this->validate();
Post::create(
$this->pull() // [tl! highlight]
);
}
}也可以把属性名传给 pull(),只拉取指定属性:
// Return a value before resetting...
$this->pull('title');
// Return a key-value array of properties before resetting...
$this->pull(['title', 'content']);使用 Rule 对象
若验证场景更复杂、需要用到 Laravel 的 Rule 对象,也可以改用定义 rules() 方法来声明验证规则,例如:
<?php
namespace App\Livewire\Forms;
use Illuminate\Validation\Rule;
use App\Models\Post;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post;
public $title = '';
public $content = '';
protected function rules()
{
return [
'title' => [
'required',
Rule::unique('posts')->ignore($this->post), // [tl! highlight]
],
'content' => 'required|min:5',
];
}
// ...
public function update()
{
$this->validate();
$this->post->update($this->only(['title', 'content']));
$this->reset();
}
}使用 rules() 方法而不是 #[Validate] 时,Livewire 只会在你调用 $this->validate() 时运行验证规则,而不是每次属性更新时都验证。
若你在使用实时验证,或其他希望 Livewire 在每次请求后验证特定字段的场景,可以不带规则地使用 #[Validate],例如:
<?php
namespace App\Livewire\Forms;
use Livewire\Attributes\Validate;
use Illuminate\Validation\Rule;
use App\Models\Post;
use Livewire\Form;
class PostForm extends Form
{
public ?Post $post;
#[Validate] // [tl! highlight]
public $title = '';
public $content = '';
protected function rules()
{
return [
'title' => [
'required',
Rule::unique('posts')->ignore($this->post),
],
'content' => 'required|min:5',
];
}
// ...
public function update()
{
$this->validate();
$this->post->update($this->only(['title', 'content']));
$this->reset();
}
}这样,若在表单提交前更新了 $title 属性——例如使用 wire:model.live.blur——就会运行针对 $title 的验证。
显示加载指示器
默认情况下,表单提交过程中 Livewire 会自动禁用提交按钮,并将输入标记为 readonly,防止在第一次提交处理期间用户再次提交。
不过,若应用 UI 没有额外提示,用户可能很难察觉这种「加载中」状态。
下面示例通过 wire:loading 在「Save」按钮上加一个小加载动画,让用户知道表单正在提交:
<button type="submit">
Save
<div wire:loading>
<svg>...</svg> <!-- SVG loading spinner -->
</div>
</button>也可以配合 Tailwind 与 Livewire 自动添加的 data-loading 属性,写出更干净的标记:
<button type="submit">
<span class="in-data-loading:hidden">Save</span>
<span class="not-in-data-loading:hidden">
<svg>...</svg> <!-- SVG loading spinner -->
</span>
</button>实时更新字段
默认情况下,Livewire 仅在表单提交(或调用其他 action)时发送网络请求,而不是在填写表单的过程中。
以 post.create 组件为例。若希望用户输入时「title」输入框与后端 $title 属性同步,可给 wire:model 加上 .live 修饰符,例如:
<input type="text" wire:model.live="title">现在,用户在该字段中输入时,会向服务器发送网络请求以更新 $title。这对实时搜索等场景很有用——用户在搜索框输入时即可过滤数据集。
仅在 blur 时更新字段
多数情况下,用 wire:model.live 做实时字段更新没问题;但对文本输入来说,它可能过于消耗网络资源。
若不想在用户输入时就发网络请求,而只想在用户按 Tab 离开文本框(也称为对输入做「blur」)时才发送,可以使用 .blur 修饰符:
<input type="text" wire:model.live.blur="title" >现在,直到用户按 Tab 或点击文本框以外的地方,服务端的组件类才会被更新。
实时验证
有时,你希望在用户填写表单时就显示验证错误。这样用户能尽早发现问题,而不必等到整个表单填完。
Livewire 会自动处理这类场景。在 wire:model 上使用 .live 或 .blur 后,用户填写表单时会发送网络请求。每次请求在更新属性前都会运行相应的验证规则。若验证失败,服务端不会更新该属性,并向用户显示验证消息:
<input type="text" wire:model.live.blur="title">
<div>
@error('title') <span class="error">{{ $message }}</span> @enderror
</div>#[Validate('required|min:5')]
public $title = '';现在,若用户在「title」输入框只输入三个字符,然后点击表单中的下一个输入框,就会看到验证消息,提示该字段最少需要五个字符。
更多信息请参阅验证文档页。
实时保存表单
若希望用户填写时自动保存表单,而不是等点击「submit」,可以使用 Livewire 的 updated() 钩子:
<?php // resources/views/components/post/⚡edit.blade.php
use Livewire\Attributes\Validate;
use Livewire\Component;
use App\Models\Post;
new class extends Component {
public Post $post;
#[Validate('required')]
public $title = '';
#[Validate('required')]
public $content = '';
public function mount(Post $post)
{
$this->post = $post;
$this->title = $post->title;
$this->content = $post->content;
}
public function updated($name, $value) // [tl! highlight:5]
{
$this->post->update([
$name => $value,
]);
}
};
?>
<form wire:submit>
<input type="text" wire:model.live.blur="title">
<div>
@error('title') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model.live.blur="content">
<div>
@error('content') <span class="error">{{ $message }}</span> @enderror
</div>
</form>在上例中,用户完成某个字段(点击或 Tab 到下一个字段)时,会发送网络请求以更新组件上的该属性。属性在类上更新后,会立刻针对该属性名及其新值调用 updated() 钩子。
我们可以用这个钩子只更新数据库中对应的那个字段。
此外,由于这些属性带有 #[Validate],验证规则会在属性更新以及调用 updated() 钩子之前运行。
要了解更多关于「updated」生命周期钩子及其他钩子的内容,请参阅生命周期钩子文档。
显示未保存(dirty)指示器
在上面讨论的实时保存场景中,向用户提示某个字段尚未持久化到数据库会很有帮助。
例如,用户访问 post.edit 页面并在文本框中修改文章标题时,可能不清楚标题何时真正写入数据库——尤其是表单底部没有「Save」按钮时。
Livewire 提供了 wire:dirty 指令,可在输入值与服务端组件不一致时切换元素或修改 class:
<input type="text" wire:model.live.blur="title" wire:dirty.class="border-yellow">在上例中,用户在输入框中输入时,字段周围会出现黄色边框。用户 Tab 离开后,网络请求发出,边框消失,提示输入已持久化,不再是「dirty」。
若要切换整个元素的可见性,可将 wire:dirty 与 wire:target 一起使用。wire:target 用于指定要监视「脏状态」的数据。这里是「title」字段:
<input type="text" wire:model="title">
<div wire:dirty wire:target="title">Unsaved...</div>输入防抖
在文本输入上使用 .live 时,你可能希望更精细地控制网络请求的发送频率。默认会对输入应用「250ms」的 debounce;你也可以用 .debounce 修饰符自定义:
<input type="text" wire:model.live.debounce.150ms="title" >给字段加上 .debounce.150ms 后,处理该字段的输入更新会使用更短的「150ms」防抖。也就是说,用户输入时,只有停止输入至少 150 毫秒后才会发送网络请求。
输入节流
如前所述,对字段应用输入防抖后,只有用户停止输入一段时间后才会发送网络请求。这意味着若用户持续输入较长内容,在完成之前不会发送请求。
有时这并不是你想要的行为;你更希望在用户输入过程中就发送请求,而不是等他们完成或停顿。
这种情况下,可以改用 .throttle 指定发送网络请求的时间间隔:
<input type="text" wire:model.live.throttle.150ms="title" >在上例中,用户在「title」字段持续输入时,会每隔 150 毫秒发送一次网络请求,直到输入结束。
将输入字段抽取为 Blade 组件
即便是我们一直在讨论的 post.create 这样的小组件,也会重复大量表单字段样板代码,比如验证消息和标签。
把这类重复的 UI 元素抽成专用的 Blade 组件,在应用中复用,会很有帮助。
例如,下面是 post.create 组件原来的 Blade 模板。我们会把下面两个文本输入抽成专用 Blade 组件:
<form wire:submit="save">
<input type="text" wire:model="title"> <!-- [tl! highlight:3] -->
<div>
@error('title') <span class="error">{{ $message }}</span> @enderror
</div>
<input type="text" wire:model="content"> <!-- [tl! highlight:3] -->
<div>
@error('content') <span class="error">{{ $message }}</span> @enderror
</div>
<button type="submit">Save</button>
</form>抽取可复用的 Blade 组件 <x-input-text> 后,模板会变成这样:
<form wire:submit="save">
<x-input-text name="title" wire:model="title" /> <!-- [tl! highlight] -->
<x-input-text name="content" wire:model="content" /> <!-- [tl! highlight] -->
<button type="submit">Save</button>
</form>接下来是 x-input-text 组件的源码:
<!-- resources/views/components/input-text.blade.php -->
@props(['name'])
<input type="text" name="{{ $name }}" {{ $attributes }}>
<div>
@error($name) <span class="error">{{ $message }}</span> @enderror
</div>可以看到,我们把重复的 HTML 放进了专用的 Blade 组件。
大体上,该 Blade 组件只包含从原组件抽出的 HTML。不过我们额外加了两样东西:
- `@props` 指令
- 输入上的
{{ $attributes }}语句
我们分别说明这两处:
用 @props(['name']) 把 name 指定为「prop」,就是告诉 Blade:若组件上设置了名为「name」的属性,取其值并在组件内部以 $name 提供。
对于没有明确用途的其他属性,我们使用了 {{ $attributes }} 语句。这用于「属性转发」:把写在 Blade 组件上的 HTML 属性转发给组件内部的某个元素。
这样可确保 wire:model="title" 以及其他额外属性(如 disabled、class="..." 或 required)仍会转发到真正的 <input> 元素上。
自定义表单控件
在上例中,我们把一个 input 元素「包装」成可复用的 Blade 组件,使用起来就像原生 HTML 输入元素一样。
这个模式很实用;不过有时你可能想从零创建一个完整的输入组件(没有底层的原生 input),却仍希望能用 wire:model 把它的值绑定到 Livewire 属性。
例如,假设你想创建一个用 Alpine 写的简单「计数器」输入组件 <x-input-counter />。
在创建 Blade 组件之前,先看一个纯 Alpine 的简单「计数器」组件作为参考:
<div x-data="{ count: 0 }">
<button x-on:click="count--">-</button>
<span x-text="count"></span>
<button x-on:click="count++">+</button>
</div>可以看到,上面的组件显示一个数字,以及用于增减该数字的两个按钮。
现在假设我们想把它抽成名为 <x-input-counter /> 的 Blade 组件,并在组件中这样使用:
<x-input-counter wire:model="quantity" />创建这个组件大体上很简单:把计数器的 HTML 放进类似 resources/views/components/input-counter.blade.php 的 Blade 组件模板即可。
不过,要让它配合 wire:model="quantity" 工作,以便轻松把 Livewire 组件的数据绑定到这个 Alpine 组件里的「count」,还需要多一步。
组件源码如下:
<!-- resources/view/components/input-counter.blade.php -->
<div x-data="{ count: 0 }" x-modelable="count" {{ $attributes}}>
<button x-on:click="count--">-</button>
<span x-text="count"></span>
<button x-on:click="count++">+</button>
</div>可以看到,这段 HTML 唯一不同的地方是 x-modelable="count" 和 {{ $attributes }}。
x-modelable 是 Alpine 的一个工具,告诉 Alpine 让某段数据可从外部绑定。Alpine 文档有关于该指令的更多信息。
如前所述,{{ $attributes }} 会转发从外部传入 Blade 组件的任意属性。这里会转发 wire:model 指令。
由于有 {{ $attributes }},HTML 在浏览器中渲染时,wire:model="quantity" 会与 x-modelable="count" 一起出现在 Alpine 组件根 <div> 上,例如:
<div x-data="{ count: 0 }" x-modelable="count" wire:model="quantity">x-modelable="count" 告诉 Alpine 查找任何 x-model 或 wire:model 语句,并用「count」作为要绑定的数据。
因为 x-modelable 同时适用于 wire:model 和 x-model,这个 Blade 组件也可以在 Livewire 与 Alpine 之间互换使用。下面是在纯 Alpine 上下文中使用该 Blade 组件的示例:
<x-input-counter x-model="quantity" />在应用中创建自定义输入元素非常强大,但需要更深入理解 Livewire 与 Alpine 提供的工具以及它们如何协作。
另见
- 验证 — 用实时反馈验证表单输入
- wire:model — 将表单输入绑定到组件属性
- 文件上传 — 在表单中处理文件上传
- 动作 — 用动作处理表单提交
- 加载状态 — 在表单提交期间显示加载指示器