Skip to content
全部文档

表单

表单是大多数 Web 应用的核心,因此 Livewire 提供了大量实用工具来构建表单。从处理简单的输入元素,到实时验证、文件上传等复杂场景,Livewire 都有简洁且文档完善的工具,帮你更轻松地开发,并让用户体验更好。

下面开始。

提交表单

我们先看 post.create 组件里一个非常简单的表单。它有两个文本输入框和一个提交按钮,后端还有管理表单状态与提交的代码:

php
<?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
<?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 模板,在页面上显示验证错误。

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 命令:

shell
php artisan livewire:form PostForm

上述命令会创建 app/Livewire/Forms/PostForm.php 文件。

我们把 post.create 组件改写成使用 PostForm 类:

php
<?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
<?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');
    }
};
blade
<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
<?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
<?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
<?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
<?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
<?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(),只重置指定属性:

php
$this->reset('title');

// Or multiple at once...

$this->reset(['title', 'content']);

reset() 会把每个属性恢复为表单类上声明的状态。若带类型的属性没有默认值,重置后会处于未初始化状态。若重置后马上需要读取或渲染这些字段,请给它们提供默认值:

php
public string $title = '';

public ?string $subtitle = null;

或者,在调用 reset() 之后、读取之前再给属性赋值。

拉取表单字段

你也可以用 pull() 方法,在一次操作中同时取出表单属性并重置它们。

php
<?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(),只拉取指定属性:

php
// 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
<?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
<?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」按钮上加一个小加载动画,让用户知道表单正在提交:

blade
<button type="submit">
    Save

    <div wire:loading>
        <svg>...</svg> <!-- SVG loading spinner -->
    </div>
</button>

也可以配合 Tailwind 与 Livewire 自动添加的 data-loading 属性,写出更干净的标记:

blade
<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 修饰符,例如:

blade
<input type="text" wire:model.live="title">

现在,用户在该字段中输入时,会向服务器发送网络请求以更新 $title。这对实时搜索等场景很有用——用户在搜索框输入时即可过滤数据集。

仅在 blur 时更新字段

多数情况下,用 wire:model.live 做实时字段更新没问题;但对文本输入来说,它可能过于消耗网络资源。

若不想在用户输入时就发网络请求,而只想在用户按 Tab 离开文本框(也称为对输入做「blur」)时才发送,可以使用 .blur 修饰符:

blade
<input type="text" wire:model.live.blur="title" >

现在,直到用户按 Tab 或点击文本框以外的地方,服务端的组件类才会被更新。

实时验证

有时,你希望在用户填写表单时就显示验证错误。这样用户能尽早发现问题,而不必等到整个表单填完。

Livewire 会自动处理这类场景。在 wire:model 上使用 .live.blur 后,用户填写表单时会发送网络请求。每次请求在更新属性前都会运行相应的验证规则。若验证失败,服务端不会更新该属性,并向用户显示验证消息:

blade
<input type="text" wire:model.live.blur="title">

<div>
    @error('title') <span class="error">{{ $message }}</span> @enderror
</div>
php
#[Validate('required|min:5')]
public $title = '';

现在,若用户在「title」输入框只输入三个字符,然后点击表单中的下一个输入框,就会看到验证消息,提示该字段最少需要五个字符。

更多信息请参阅验证文档页

实时保存表单

若希望用户填写时自动保存表单,而不是等点击「submit」,可以使用 Livewire 的 updated() 钩子:

php
<?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:

blade
<input type="text" wire:model.live.blur="title" wire:dirty.class="border-yellow">

在上例中,用户在输入框中输入时,字段周围会出现黄色边框。用户 Tab 离开后,网络请求发出,边框消失,提示输入已持久化,不再是「dirty」。

若要切换整个元素的可见性,可将 wire:dirtywire:target 一起使用。wire:target 用于指定要监视「脏状态」的数据。这里是「title」字段:

blade
<input type="text" wire:model="title">

<div wire:dirty wire:target="title">Unsaved...</div>

输入防抖

在文本输入上使用 .live 时,你可能希望更精细地控制网络请求的发送频率。默认会对输入应用「250ms」的 debounce;你也可以用 .debounce 修饰符自定义:

blade
<input type="text" wire:model.live.debounce.150ms="title" >

给字段加上 .debounce.150ms 后,处理该字段的输入更新会使用更短的「150ms」防抖。也就是说,用户输入时,只有停止输入至少 150 毫秒后才会发送网络请求。

输入节流

如前所述,对字段应用输入防抖后,只有用户停止输入一段时间后才会发送网络请求。这意味着若用户持续输入较长内容,在完成之前不会发送请求。

有时这并不是你想要的行为;你更希望在用户输入过程中就发送请求,而不是等他们完成或停顿。

这种情况下,可以改用 .throttle 指定发送网络请求的时间间隔:

blade
<input type="text" wire:model.live.throttle.150ms="title" >

在上例中,用户在「title」字段持续输入时,会每隔 150 毫秒发送一次网络请求,直到输入结束。

将输入字段抽取为 Blade 组件

即便是我们一直在讨论的 post.create 这样的小组件,也会重复大量表单字段样板代码,比如验证消息和标签。

把这类重复的 UI 元素抽成专用的 Blade 组件,在应用中复用,会很有帮助。

例如,下面是 post.create 组件原来的 Blade 模板。我们会把下面两个文本输入抽成专用 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> 后,模板会变成这样:

blade
<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 组件的源码:

blade
<!-- 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" 以及其他额外属性(如 disabledclass="..."required)仍会转发到真正的 <input> 元素上。

自定义表单控件

在上例中,我们把一个 input 元素「包装」成可复用的 Blade 组件,使用起来就像原生 HTML 输入元素一样。

这个模式很实用;不过有时你可能想从零创建一个完整的输入组件(没有底层的原生 input),却仍希望能用 wire:model 把它的值绑定到 Livewire 属性。

例如,假设你想创建一个用 Alpine 写的简单「计数器」输入组件 <x-input-counter />

在创建 Blade 组件之前,先看一个纯 Alpine 的简单「计数器」组件作为参考:

blade
<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 组件,并在组件中这样使用:

blade
<x-input-counter wire:model="quantity" />

创建这个组件大体上很简单:把计数器的 HTML 放进类似 resources/views/components/input-counter.blade.php 的 Blade 组件模板即可。

不过,要让它配合 wire:model="quantity" 工作,以便轻松把 Livewire 组件的数据绑定到这个 Alpine 组件里的「count」,还需要多一步。

组件源码如下:

blade
<!-- 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> 上,例如:

blade
<div x-data="{ count: 0 }" x-modelable="count" wire:model="quantity">

x-modelable="count" 告诉 Alpine 查找任何 x-modelwire:model 语句,并用「count」作为要绑定的数据。

因为 x-modelable 同时适用于 wire:modelx-model,这个 Blade 组件也可以在 Livewire 与 Alpine 之间互换使用。下面是在纯 Alpine 上下文中使用该 Blade 组件的示例:

blade
<x-input-counter x-model="quantity" />

在应用中创建自定义输入元素非常强大,但需要更深入理解 Livewire 与 Alpine 提供的工具以及它们如何协作。

另见

  • 验证用实时反馈验证表单输入
  • wire:model将表单输入绑定到组件属性
  • 文件上传在表单中处理文件上传
  • 动作用动作处理表单提交
  • 加载状态在表单提交期间显示加载指示器