支持 Laravel.io 的持续开发 →

使用 HTMX 在 Laravel 中进行表格排序和分页

27 Oct, 2023 7 min read

在我的上一篇文章中,我们介绍了如何入门Laravel 中的 HTMX。我们探索了一些核心概念,包括使用 hx-gethx-post 来添加异步功能。

在本篇博文中,我们将使用 HTMX 优化 Laravel Blade 模板,添加客户端排序和分页功能。

介绍

在上篇文章中,我们构建了一个非常简单的用于 contacts CRUD 的数据表,现在让我们通过添加分页和排序功能,并使其成为一个可重用的组件来扩展它。

在 Laravel 中使用 HTMX 实现分页

虽然 Laravel 中的 {{ $contacts->links() }} 提供分页链接,但它缺少响应性。这就是 hx-boost 属性发挥作用的地方。

我们将 hx-boost 属性应用到容器中,确保容器内链接的后续请求都接受 hx-get 处理。

以下是我们的代码示例

<div id="table-container" 
	hx-get="{{ route('contacts.index') }}" 
	hx-trigger="loadContacts from:body">  
  
    <table id="contacts-table" class="table-auto w-full">
    ...
    </table>  
  
    <div id="pagination-links" class="p-3" 
	    hx-boost="true" 
	    hx-target="#table-container">  
        {{ $contacts->links() }}  
    </div>  
  
</div>

此外,我们将 hx-gethx-trigger 属性从表格移动到父容器 div 中。

以下是更新的 ContactsController

<?php  
  
namespace App\Http\Controllers;  
  
use App\Http\Requests\ContactRequest;  
use App\Models\Contact;  
use Illuminate\Http\Request;  
  
class ContactController extends Controller  
{  
    public function index(Request $request)  
    {  
        $searchTerm = $request->input('q');  
  
        $contacts = Contact::where('name', 'LIKE', "%$searchTerm%")   
				            ->paginate(10);  
  
        if ($request->header('hx-request') 
	        && $request->header('hx-target') == 'table-container') {  
            return view('contacts.partials.table', compact('contacts'));  
        }  
  
        return view('contacts.index', compact('contacts'));  
    }
    ...
    ...
}    

现在,当点击分页链接时,HTMX 会发送异步请求,并在收到响应后交换内容。

在 Laravel 中使用 HTMX 实现排序

数据排序是另一个常见的需求。我们的目标是通过点击列标题来重新排列表格行。让我们启用 HTMX 的异步排序功能。

在表格标题中,我们包含用于交互式排序所需的 HTMX 属性

<div id="table-container" 
	 hx-get="{{ route('contacts.index') }}" 
	 hx-trigger="loadContacts from:body">

@php
	$sortField = request('sort_field');
	$sortDir = request('sort_dir', 'asc') === 'asc' ? 'desc' : 'asc';
	$sortIcon = fn($field) =>
	    $sortField === $field ? ($sortDir === 'asc' ? '↑' : '↓') : '';
	$hxGetUrl = fn($field) =>
	    request()->fullUrlWithQuery([
	        'sort_field' => $field,
	        'sort_dir' => $sortDir
	    ]);
@endphp

<table id="contacts-table" class="table-auto w-full">
    <thead>
        <th class='px-4 py-2 border text-left cursor-pointer'
            hx-get="{{ $hxGetUrl('name') }}"
            hx-trigger='click'
            hx-replace-url='true'
            hx-swap='outerHTML'
            hx-target='#table-container'>
	            Name
            <span class="ml-1" role="img">{{ $sortIcon('name') }}</span>
        </th>

		<th class='px-4 py-2 border text-left cursor-pointer'
            hx-get="{{ $hxGetUrl('email') }}"
            hx-trigger='click'
            hx-replace-url='true'
            hx-swap='outerHTML'
            hx-target='#table-container'>
	            Email
            <span class="ml-1" role="img">{{ $sortIcon('email') }}</span>
        </th>
        
        <th class="px-4 py-2 border text-left">Phone</th>
        <th class="px-4 py-2 border text-left">Address</th>
        <th class="px-4 py-2 border text-left">Actions</th>
    </thead>

    <tbody id="contacts-table-body"
        ...
        ...
    </tbody>

</table>

<div id="pagination-links" class="p-3" 
	hx-boost="true" 
	hx-target="#table-container">
    {{ $contacts->links() }}
</div>

</div>

@php标签内,我们定义变量以处理排序功能。变量$sortField$sortDir管理排序的字段和方向,而$sortIcon函数用于生成表示排序方向的适当箭头图标。函数$hxGetUrl有助于创建包含更新排序参数的HTMX请求的URL。

这是再次更新的ContactsController

<?php

namespace App\Http\Controllers;

use App\Http\Requests\ContactRequest;
use App\Models\Contact;
use Illuminate\Http\Request;

class ContactController extends Controller
{
    public function index(Request $request)
    {
        $searchTerm = $request->input('q');

        $contacts = Contact::where('name', 'LIKE', "%$searchTerm%")
            ->when($request->has('sort_field'), function ($query) use ($request) {
                $sortField = $request->input('sort_field');
                $sortDir = $request->input('sort_dir', 'asc');
                $query->orderBy($sortField, $sortDir);
            })
            ->paginate(10);

        if ($request->header('hx-request')
            && $request->header('hx-target') == 'table-container') {
            return view('contacts.partials.table', compact('contacts'));
        }

        return view('contacts.index', compact('contacts'));
    }
    ...
    ...
}
   

使用Blade组件重构

我们的表格代码正在变得越来越长且缺乏可重用性。为了解决这个问题,我们可以将每个表格元素分离成一个自己的组件。

让我们将其分解为可重用组件

components
├── table
│   ├── actions
│   │   ├── delete.blade.php
│   │   ├── edit.blade.php
│   │   └── view.blade.php
│   ├── tbody.blade.php  
│   ├── td.blade.php
│   ├── th.blade.php
│   ├── thead.blade.php
│   └── tr.blade.php
└── table.blade.php

以下命令将帮助您创建这些组件

php artisan make:component table --view

php artisan make:component table.td --view 
php artisan make:component table.th --view 
php artisan make:component table.tr --view 
php artisan make:component table.thead --view 
php artisan make:component table.tbody --view 

php artisan make:component table.actions.delete --view 
php artisan make:component table.actions.edit --view 
php artisan make:component table.actions.view --view 

主要的表格组件x-table如下所示

@props(['columns', 'records'])  
  
<table {{ $attributes->merge(['id' => 'table','class' => 'table-auto w-full']) }}>  
  
    @if(isset($columns))  
        <x-table.thead :columns="$columns"/>  
        @if(isset($records))  
            <x-table.tbody :columns="$columns" :records="$records"/>  
        @endif  
    @endif  
    
    {{ $slot }}  
  
</table>

此代码为标题和主体创建槽,传递例如列和记录的数据。

头部组件 x-table.thead 对列进行迭代

@props(['columns'])  
<thead>  
    @if(isset($columns) && is_array($columns))  
        @foreach ($columns as $column)  
            <x-table.th field="{{ $column }}" />  
        @endforeach  
    @endif    

    {{ $slot }}  
</thead>

头部单元格组件 x-table.th 生成排序URL并在需要时显示排序图标

@props(['field'])  
  
@php  
    $sortField = request('sort_field');  
    $sortDir = request('sort_dir', 'asc') === 'asc' ? 'desc' : 'asc';  
    $sortIcon = fn($field) => 
			    $sortField === $field ? ($sortDir === 'asc' ? '↑' : '↓') : '';  
    $hxGetUrl = fn($field) => 
			    request()->fullUrlWithQuery([
				    'sort_field' => $field, 
				    'sort_dir' => $sortDir
				]);  
@endphp  
  
<th {{ $attributes->merge([  
    'class' => 'px-4 py-2 border text-left cursor-pointer',  
    'hx-get' => $hxGetUrl($field),  
    'hx-trigger' => 'click',  
    'hx-replace-url' => 'true',  
    'hx-swap' => 'outerHTML',  
    'hx-target' => '#table-container',  
]) }}>  
    @if(isset($slot) && trim($slot) !== '')  
        {{ $slot }}  
    @else  
        <span>{{ Str::title($field) }}</span>  
    @endif  
    <span class="ml-1" role="img">{{ $sortIcon($field) }}</span>  
</th>

而主体 x-table.tbody 对记录进行循环

@props(['columns', 'records'])  
  
<tbody {{ $attributes->merge(['id' => 'table-body']) }}>  
    @if(isset($records))  
        @forelse ($records as $record)  
            <x-table.tr id="row-{{ $record->id }}">  
                @foreach($columns as $column)  
                    <x-table.td>  
                        @if($column === 'actions')  
                            @if(isset($actions))  
                                {{ $actions($record) }}  
                            @else  
                                <x-table.actions.view :record="$record"/>  
                                <x-table.actions.edit :record="$record"/>  
                                <x-table.actions.delete :record="$record"/>  
                            @endif  
                        @else                            
                        {{ $record->{$column} }}  
                        @endif  
                    </x-table.td>  
                @endforeach  
            </x-table.tr>  
        @empty  
            <x-table.tr>  
                <x-table.td colspan="100%">No record found.</x-table.td>  
            </x-table.tr>  
        @endforelse  
    @endif    
    
    {{ $slot }}  
</tbody>

单独的单元格 x-table.td 负责显示数据

<td {{ $attributes->merge(['class' => 'px-4 py-2 border']) }}>  
  {{ $slot }}  
</td>

而行 x-table.tr 提供包装

// Just for fun : time() - rand(100,2000)
<tr {{ $attributes->merge(['id' => "row-".(time() - rand(100,2000))]) }}>  
    {{ $slot }}  
</tr>

同样,我们还可以集成如查看、编辑和删除的表格功能。务必探索全面指南以获取更多详细信息。

使用表格组件

现在我们已经将表格分解为可重用的Blade组件,让我们探索如何在我们的代码中应用它们。通过有效地利用这些组件,我们可以确保更加组织和流畅的方法。

让我们看看表格组件的实际应用

<div id="table-container" 
	 hx-get="{{ route('contacts.index') }}" 
	 hx-trigger="loadContacts from:body">  
  
    <x-table :records="$contacts" 
		     :columns="['name', 'email', 'phone', 'address', 'actions']"/>  
  
    <div id="pagination-links" class="p-3" 
	    hx-boost="true" 
		hx-target="#table-container">  
        {{ $contacts->links() }}  
    </div>  
  
</div>

结束语

在将HTMX与Laravel结合的探索中,我们实现了动态表格排序和流畅的分页。通过创建可重用的Blade组件,我们简化了开发并改进了代码组织。

有关更详细信息,请查看我的博客文章,并在GitHub上找到此实现的源代码。

请确保关注我的Twitter以获取我发布更多内容的通知。

最后更新9个月前。

driesvints, spiritkiddie, mshaf liked this article

3
喜欢这篇文章?告诉作者并给予他们支持和掌声!

你可能还喜欢这些文章

2024年3月11日

使用Larastan让您的Laravel应用从0到9

在执行之前在您的Laravel应用中查找错误是可能的,多亏了Larastan,它...

阅读文章
2024年7月19日

无需特质标准化API响应

我发现大多数用于API响应的库都是使用特质实现的,并且...

阅读文章
2024年7月17日

通过Discord通知在您的Laravel项目中收集反馈

如何在Laravel项目中创建反馈模块,并在收到消息时获取Discord通知...

阅读文章

我们想感谢这些 惊人的公司 对我们的支持

您的标志在这里?

Laravel.io

Laravel问题解决、知识分享和社区建设的门户。

© 2024 Laravel.io - 所有权保留。