编程

Laravel 自定义 Select 组件

1050 2023-02-13 08:12:19

这是一篇关于为 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 一起工作很好,当我们将它们结合起来时,我们可以为我们的客户和用户解决很多问题。