Laravel 自定义 Select 组件
这是一篇关于为 Laravel Livewire 应用创建自定义 Select 组件的文章。
当涉及到表单元素时,我们可能会立即寻求开源或付费库。预先构建的组件加快了开发,使用经过良好测试的、健壮的库可以减轻我们的压力。
但当我们需要一些定制的东西时呢?定制第三方包通常比自己制作组件更困难。此外,学习如何制作可重用组件可以提高我们对 Livewire 的总体理解。
今天,我们将使用 Livewire 和 Tailwind 制作一个自定义选择组件。然后,我们将进一步考虑使用 Alpine.js 访问它的方法。我们将完全自定义它,而不使用 HTML<select> 标记,这在外观和用户体验方面给了我们很大的自由。
我们开始吧
为了简单起见,我们假设您已经创建了一个新的 Laravel 项目,使用 composer 安装了 Livewire,并使用 npm 安装了Tailwind。
使用 php artisan make:Livewire Select
生成 Livewire 组件。
它将创建两个文件:
- 组件类:
app/Http/Livewire/Select.php
- 组件视图:
resources/views/livewire/select.blade.php
然后,编辑 welcome.blade.php
文件并引入 select 组件。
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Livewire Select</title>
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
@livewireStyles
</head>
<body class="flex items-center justify-center min-h-screen">
<div class="w-56">
<livewire:select/>
</div>
@livewireScripts
</body>
</html>
制作布局
转到 select.blade.php
文件。该组件有 3 个部分,即所选项目的标签、所选项目/占位符和带有所有可选选项的绝对定位列表。
<div>
<label>
Label
</label>
<div class="relative">
<button>
Selected item
</button>
<ul class="absolute z-10">
<li>
Option 1
</li>
<li>
Option 2
</li>
</ul>
</div>
</div>
我们可以通过 Tailwind 类。
<div>
<label class="text-gray-500">
Label
</label>
<div class="relative">
<button class="w-full flex items-center h-12 bg-white border rounded-lg px-2">
Selected item
</button>
<ul class="bg-white absolute mt-1 z-10 border rounded-lg w-full">
<li class="px-3 py-2">
Option 1
</li>
<li class="px-3 py-2">
Option 2
</li>
</ul>
</div>
</div>
修饰好后,它看起来像这样:
选项渲染和切换
现在让我们实现渲染和选项的打开/关闭。
在 Select.php
文件中创建一些属性和一个 toggle()
函数
class Select2 extends component
{
public $items;
public $selected = null;
public $label;
public $open = false;
public function toggle()
{
$this->open = !$this->open;
}
...
}
为了使组件可重用,我们从外部传递这些,当前在 welcome.blade.php
中
<livewire:select
:selected="1"
:items="['Apple','Banana','Strawberry']"
label="Favorite fruit"
/>
替换 select.blade.php
中的一些部分,以从给定的道具动态渲染,并在 <button>
中添加一个 click 监听器,以添加打开/关闭功能。
<div>
<label class="text-gray-500">
{{ $label }}
</label>
<div class="relative">
<button
wire:click="toggle"
class="w-full flex items-center h-12 bg-white border rounded-lg px-2"
>
@if ($selected !== null)
{{ $items[$selected] }}
@else
Choose...
@endif
</button>
@if ($open)
<ul class="bg-white absolute mt-1 z-10 border rounded-lg w-full">
@foreach($items as $item)
<li class="px-3 py-2 cursor-pointer">
{{ $item }}
</li>
@endforeach
</ul>
@endif
</div>
</div>
通过这些编辑,我们的项目被渲染,我们可以打开和关闭选项列表。
Making the Selection
让我们在 select.php
类中创建一个 select($index)
函数。这会将选定项设置为给定索引,如果给定索引是当前选定项,则还会对取消选择进行处理。
public function select($index) {
$this->selected = $this->selected !== $index ? $index : null;
$this->open = false;
}
将 click 事件添加到 <li> 中,并添加一些条件类以突出显示所选选项。我们使用 @class
blade指令和 $loop
变量,该变量由 @foreach
loop 提供
<li wire:click="select({{ $loop->index }})"
@class([
'px-3 py-2 cursor-pointer',
'bg-blue-500 text-white' => $selected === $loop->index,
'hover:bg-blue-400 hover:text-white',
])
>
{{ $item }}
</li>
现在我们有了一个工作选择组件!! 🚀
外观美化
在深入 Alpine.js 部分之前,让我们做一些UI改进。我们需要一个打开/关闭指示器,在当前选中的项目上包含一个复选图标将很酷。为了简单起见,我们将使用 Heroicon 并复制粘贴 SVG。
你可以在此处找到 select.blade.php
的所有的标记:
<div>
<label class="text-gray-500">
{{ $label }}
</label>
<div class="relative">
<button
wire:click="toggle"
class="w-full flex items-center justify-between h-12 bg-white border rounded-lg px-2"
>
@if ($selected !== null)
{{ $items[$selected] }}
@else
Choose...
@endif
<div class="text-gray-400">
@if ($open)
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z"
clip-rule="evenodd"/>
</svg>
@else
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"/>
</svg>
@endif
</div>
</button>
@if ($open)
<ul class="bg-white absolute mt-1 z-10 border rounded-lg w-full">
@foreach($items as $item)
<li wire:click="select({{ $loop->index }})"
@class([
'px-3 py-2 cursor-pointer flex items-center justify-between',
'bg-blue-500 text-white' => $selected === $loop->index,
'hover:bg-blue-400 hover:text-white',
])
>
{{ $item }}
@if ($selected === $loop->index)
<div class="text-white">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
@endif
</li>
@endforeach
</ul>
@endif
</div>
</div>
使用 Alpine.js 使其可访问
无障碍是网络开发的一个重要组成部分,不仅仅是当我们考虑残疾人时,而且让事物无障碍为每个人提供了更好的用户体验。
我们的要求:
- 按 TAB 键,我们可以获得组件焦点
- 按空格键打开/关闭组件
- 按向上和向下箭头在项目之间导航
- 按 ENTER 键选择当前高亮显示的元素
以上所有这些都可以通过 Livewire 实现,但它会向后端发出大量请求。由于这些东西只是 UI 状态,所以使用 Alpine.js(一种流行的轻量级 Javascript 框架)在浏览器中实现它们是有意义的。
首先,我们需要在 welcome.blade.php
中引入 Alpine 的 JS
<head>
...
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
...
</head>
然后,在父级的 div 中添加一个 x-data
属性
<div x-data="{}">
...
</div>
要在鼠标未悬停的情况下高亮显示项目,我们必须跟踪当前高亮显示的元素。此外,我们必须计算下一个和上一个项目,为此,我们需要从 PHP 传递项目的计数。
<div x-data="{
highlighted: 0,
count: {{ count($items) }},
}">
我们还需要三个函数 next、previous 和 select。我们将在 next/previous 计算中使用神奇的模运算符,它将在除法后返回余数。例如:3%2=1,8%3=2
要选择当前突出显示的项目,我们将使用此选项 $wire
变量的 call()
方法。
<div x-data="{
highlighted: 0,
count: {{ count($items) }},
next() {
this.highlighted = (this.highlighted + 1) % this.count;
},
previous() {
this.highlighted = (this.highlighted + this.count - 1) % this.count;
},
select() {
this.$wire.call('select', this.highlighted)
}
}">
现在我们已经拥有了所有需要的数据和函数,让我们将它们连接到布局中。
将 Alpine.js 事件监听器添加到<button>
<button
wire:click="toggle"
class="w-full flex items-center justify-between h-12 bg-white border rounded-lg px-2"
@keydown.arrow-down="next()"
@keydown.arrow-up="previous()"
@keydown.enter.prevent="select()"
>
现在,为了突出显示正确的项,我们使用当前的 $index
向 <li> 添加一个 x-data
,如果 $index
与高亮显示的变量匹配,则使用 Alpine 添加一些类。
<li wire:click="select({{ $loop->index }})"
x-data="{ index: {{ $loop->index }} }"
class="px-3 py-2 cursor-pointer flex items-center justify-between"
:class="{'bg-blue-400 text-white': index === highlighted}"
@mouseover="highlighted = index"
>
...
</li>
最后一步
我们快完成了,只剩下几个小问题了。
当我们第一次打开这个 Select 组件时,它应该突出显示当前选定的项目。让我们使用 x-init
属性将此信息提供给 Alpine。
<div x-data="{...}"
x-init="highlighted = {{ $selected ?: 0 }}"
>
我们可以通过向 Alpine 添加 close()
函数来处理 “click outside”,并向父 div 添加 @click.outside listener
,以在用户单击其他位置时关闭弹出窗口。
<div x-data="{
...
close() {
if (this.$wire.open) {
this.$wire.open = false;
}
}
}"
...
@click.outside="close()"
>
在 select 组件未打开并且我们按下 ENTER 键,它将取消选择当前项目,让我们在 select.php
中解决这个问题
public function select($index) {
if (!$this->open) {
return;
}
$this->selected = $this->selected !== $index ? $index : null;
$this->open = false;
}
当高亮显示的项目和选中的项目不同时,我们需要将复选图标的颜色更改为蓝色;否则在白色背景上看不到。让我们用一些动态类来解决这个问题。
@if ($selected === $loop->index)
<div :class="index === highlighted ? 'text-white' : 'text-blue-500'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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>
@endif
就这样,我们完成了! 🚀 👏
正如你所看到的,通过将多个 <livewire:select/>
组件放入我们的视图中,每个组件都完成了它的工作。
结语
有时,当使用第三方组件包时,我们不知道到底发生了多少事情。当我们自己做的时候,除了学习所涉及的内容之外,我们也意识到这是我们自己可以做的事情。所以当我们需要一些定制的东西时,我们不会害怕。
另一个好处是:如果你希望 Livewire 组件是可重用的,那么应该将外部的每个依赖项传递给它们,而不是使用全局事件和侦听器。
我们可以使用 Livewire 做很多事情,但我们应该注意使用 Javascript 更有意义的情况。Alpine.js 和 Livewire 一起工作很好,当我们将它们结合起来时,我们可以为我们的客户和用户解决很多问题。