编程

Laravel 自定义 Select 组件

552 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变量,该变量由@foreachloop提供

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