Giới thiệu
Trong bài viết này, chúng ta sẽ tìm hiểu cách xây dựng một modal chọn lựa có khả năng tìm kiếm nhanh với Laravel và Alpine.js. Hướng dẫn này sẽ giúp bạn thực hiện một cách đơn giản và hiệu quả cho các tập dữ liệu nhỏ đến trung bình.
Công nghệ sử dụng
- Laravel: Framework phát triển backend mạnh mẽ.
- Alpine.js: Thư viện JavaScript nhẹ để tạo tính tương tác.
- Tailwind CSS: Framework thiết kế với các lớp tiện ích.
Cách tiếp cận
1. Tính toán dữ liệu tìm kiếm trước
Thực hiện công việc nặng nề một lần trong quá trình render:
php
// Tính toán văn bản tìm kiếm cho mỗi mục
$searchText = strtolower($item['name'] . ' ' . $item['description']);
2. Sử dụng Alpine.js cho tìm kiếm và chọn lựa
Một component đơn giản sử dụng Alpine.js:
javascript
{
search: '',
hasResults: true,
selectedValue: '',
init() {
this.$watch('search', () => this.filterItems());
},
filterItems() {
const searchLower = this.search.toLowerCase().trim();
const cards = this.$el.querySelectorAll('.item-card');
let visibleCount = 0;
cards.forEach(card => {
const text = card.dataset.searchText || '';
const isVisible = searchLower === '' || text.includes(searchLower);
card.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
this.hasResults = visibleCount > 0;
}
}
3. Cấu trúc HTML cơ bản
html
<!-- Input tìm kiếm -->
<input type="search" x-model="search" placeholder="Tìm kiếm..." />
<!-- Lưới các mục -->
<div class="grid gap-4">
<!-- Mỗi mục có thuộc tính data-search-text -->
<div class="item-card" data-search-text="form liên hệ đơn giản">
<h3>Mẫu liên hệ</h3>
<p>Mẫu liên hệ đơn giản</p>
</div>
</div>
<!-- Trạng thái rỗng -->
<div x-show="search !== '' && !hasResults">
<p>Không tìm thấy mục nào</p>
<button x-on:click="search = ''">Xóa tìm kiếm</button>
</div>
Lợi ích chính
Phản hồi tìm kiếm tức thì
- Không có yêu cầu đến server trong quá trình tìm kiếm.
- Thao tác trực tiếp với DOM để tăng tốc độ.
- Thích hợp cho tối đa 50 mục.
Tiến bộ theo từng bước
- Hoạt động bình thường ngay cả khi không có JavaScript (giảm thiểu sự cố).
- Có thể truy cập mặc định.
- Thân thiện với thiết bị di động.
Bảo trì đơn giản
- Không có quản lý trạng thái phức tạp.
- Dễ dàng gỡ lỗi và mở rộng.
- Tuân theo các mẫu chuẩn của Laravel.
Mẹo tối ưu hiệu suất
Tính toán trước khi có thể:
php
// Thực hiện một lần trong quá trình render, không phải trong quá trình tìm kiếm
$searchText = strtolower($title . ' ' . $description);
Sử dụng thao tác DOM trực tiếp:
javascript
// Nhanh hơn so với virtual DOM cho các tập dữ liệu nhỏ
card.style.display = isVisible ? '' : 'none';
Tự động lấy nét để cải thiện trải nghiệm người dùng:
javascript
this.$nextTick(() => this.$refs.searchInput?.focus());
Khi nào nên sử dụng cách tiếp cận này
Thích hợp cho:
- Tập dữ liệu nhỏ đến trung bình (< 50 mục).
- Yêu cầu tìm kiếm thời gian thực.
- Logic lọc đơn giản.
- Ứng dụng Laravel.
Xem xét các lựa chọn thay thế cho:
- Tập dữ liệu lớn (> 100 mục).
- Thuật toán tìm kiếm phức tạp.
- Xử lý dữ liệu nặng.
Bài học quan trọng
- Bắt đầu đơn giản - Thao tác DOM cơ bản thường vượt trội hơn so với các giải pháp phức tạp.
- Tính toán trước khi có thể - Thực hiện công việc nặng nề một lần, không lặp lại.
- Tiến bộ theo từng bước - Xây dựng một nền tảng hoạt động trước.
- Alpine.js tỏa sáng - Hoàn hảo cho các tương tác trong biểu mẫu và tính tương tác đơn giản.
Ví dụ hoàn chỉnh
Dưới đây là một triển khai đầy đủ bạn có thể sao chép và điều chỉnh:
php
{{-- Component kiểm tra nhanh --}}
@php
$items = [
'contact' => ['name' => 'Mẫu liên hệ', 'description' => 'Mẫu liên hệ đơn giản', 'category' => 'Kinh doanh'],
'survey' => ['name' => 'Mẫu khảo sát', 'description' => 'Khảo sát nhiều câu hỏi', 'category' => 'Nghiên cứu'],
'registration' => ['name' => 'Đăng ký sự kiện', 'description' => 'Biểu mẫu đăng ký sự kiện', 'category' => 'Sự kiện'],
'newsletter' => ['name' => 'Đăng ký bản tin', 'description' => 'Biểu mẫu đăng ký email', 'category' => 'Marketing'],
'feedback' => ['name' => 'Mẫu phản hồi', 'description' => 'Thu thập phản hồi của khách hàng', 'category' => 'Hỗ trợ'],
];
@endphp
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Component Tìm kiếm</title>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50 min-h-screen">
<div
x-data="{
search: '',
hasResults: true,
selectedValue: '',
init() {
this.$watch('search', () => this.filterItems());
this.$nextTick(() => this.$refs.searchInput?.focus());
},
filterItems() {
const searchLower = this.search.toLowerCase().trim();
const cards = this.$el.querySelectorAll('.item-card');
let visibleCount = 0;
cards.forEach(card => {
const text = card.dataset.searchText || '';
const isVisible = searchLower === '' || text.includes(searchLower);
card.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
this.hasResults = visibleCount > 0;
}
}"
class="p-6 max-w-4xl mx-auto"
>
<h1 class="text-3xl font-bold mb-8 text-gray-800">Kiểm tra: Component Tìm kiếm Thời gian Thực</h1>
{{-- Input Tìm kiếm --}}
<input
type="search"
x-model="search"
x-ref="searchInput"
placeholder="Tìm kiếm các mục..."
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none text-lg"
/>
{{-- Lưới các Mục --}}
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-8">
@foreach ($items as $value => $item)
@php
$searchText = strtolower($item['name'] . ' ' . $item['description'] . ' ' . $item['category']);
@endphp
<label
class="item-card cursor-pointer block"
data-search-text="{{ $searchText }}"
>
<input
type="radio"
name="selected_item"
value="{{ $value }}"
x-model="selectedValue"
class="sr-only"
/>
<div
class="border rounded-xl p-6 transition-all duration-200 hover:shadow-lg"
:class="selectedValue === '{{ $value }}' ? 'border-blue-600 bg-blue-50 shadow-lg ring-2 ring-blue-100' : 'border-gray-200 bg-white hover:border-gray-300'"
>
<h3 class="font-bold text-xl mb-3" :class="selectedValue === '{{ $value }}' ? 'text-blue-900' : 'text-gray-900'">{{ $item['name'] }}</h3>
<p class="text-gray-600 mb-3 leading-relaxed">{{ $item['description'] }}</p>
<span
class="inline-block px-3 py-1 text-sm rounded-full font-medium"
:class="selectedValue === '{{ $value }}' ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-700'"
>{{ $item['category'] }}</span>
</div>
</label>
@endforeach
</div>
{{-- Trạng thái Rỗng --}}
<div x-show="search !== '' && !hasResults" class="text-center py-16">
<div class="text-gray-400 mb-6">
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
<p class="text-xl font-semibold text-gray-600 mb-2">Không tìm thấy mục nào</p>
<p class="text-gray-500">Hãy thử điều chỉnh các thuật ngữ tìm kiếm của bạn</p>
</div>
<button
type="button"
x-on:click="search = ''"
class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
Xóa tìm kiếm
</button>
</div>
{{-- Thông tin Kết quả --}}
<div class="mt-8 p-4 bg-white border border-gray-200 rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 text-sm">
<div>
<strong class="text-gray-700">Tìm kiếm hiện tại:</strong>
<span class="text-blue-600 font-mono" x-text="search || '(không có)'"></span>
</div>
<div>
<strong class="text-gray-700">Có kết quả:</strong>
<span :class="hasResults ? 'text-green-600' : 'text-red-600'" x-text="hasResults ? 'Có' : 'Không'"></span>
</div>
<div>
<strong class="text-gray-700">Đã chọn:</strong>
<span class="text-blue-600 font-mono" x-text="selectedValue || '(không có)'"></span>
</div>
</div>
</div>
</div>
</body>
</html>
Cách sử dụng
- Tạo component - Lưu đoạn mã trên dưới dạng một component Blade.
- Bao gồm nó - Sử dụng
<x-searchable-selector />
trong các view của bạn. - Tùy chỉnh dữ liệu - Thay thế mảng
$items
bằng dữ liệu của bạn. - Phong cách - Điều chỉnh các lớp Tailwind để phù hợp với thiết kế của bạn.
Chi tiết triển khai chính
Văn bản tìm kiếm đã tính toán trước:
php
$searchText = strtolower($item['name'] . ' ' . $item['description'] . ' ' . $item['category']);
Lọc bằng Alpine.js:
javascript
cards.forEach(card => {
const text = card.dataset.searchText || '';
const isVisible = searchLower === '' || text.includes(searchLower);
card.style.display = isVisible ? '' : 'none';
if (isVisible) visibleCount++;
});
Phản hồi trực quan khi chọn:
javascript
:class="selectedValue === '{{ $value }}' ? 'border-blue-600 bg-blue-50' : 'border-gray-300'"
Cách tiếp cận này có thể mở rộng tốt cho các trường hợp sử dụng điển hình và có thể được cải tiến sau này nếu yêu cầu phát triển.
Hướng dẫn này cho thấy cách tiếp cận được sử dụng trong FilaForms - hạ tầng biểu mẫu Laravel cho phát triển nhanh chóng.