DEV Community

Manuk
Manuk

Posted on

Real-time Search with Laravel & Alpine.js: The Simple Approach

Filaforms demo templates

Overview

Learn how to build a fast, searchable selection modal using Laravel and Alpine.js. This tutorial shows the simple approach that performs well for small to medium datasets.

Tech Stack

  • Laravel - Backend framework
  • Alpine.js - Lightweight JavaScript reactivity
  • Tailwind CSS - Utility-first styling

The Approach

1. Pre-compute Search Data

Do the heavy work once during render:

// Pre-compute search text for each item $searchText = strtolower($item['name'] . ' ' . $item['description']); 
Enter fullscreen mode Exit fullscreen mode

2. Alpine.js for Search and Selection

Simple Alpine.js component:

{ 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; } } 
Enter fullscreen mode Exit fullscreen mode

3. Basic HTML Structure

<!-- Search input --> <input type="search" x-model="search" placeholder="Search..." /> <!-- Items grid --> <div class="grid gap-4"> <!-- Each item has data-search-text attribute --> <div class="item-card" data-search-text="contact form simple"> <h3>Contact Form</h3> <p>Simple contact form</p> </div> </div> <!-- Empty state --> <div x-show="search !== '' && !hasResults"> <p>No items found</p> <button x-on:click="search = ''">Clear search</button> </div> 
Enter fullscreen mode Exit fullscreen mode

Key Benefits

Instant Search Response

  • No server requests during search
  • Direct DOM manipulation for speed
  • Works well for up to 50 items

Progressive Enhancement

  • Works without JavaScript (graceful degradation)
  • Accessible by default
  • Mobile-friendly

Simple Maintenance

  • No complex state management
  • Easy to debug and extend
  • Standard Laravel patterns

Performance Tips

Pre-compute when possible:

// Do this once during render, not during search $searchText = strtolower($title . ' ' . $description); 
Enter fullscreen mode Exit fullscreen mode

Use direct DOM manipulation:

// Faster than virtual DOM for small datasets card.style.display = isVisible ? '' : 'none'; 
Enter fullscreen mode Exit fullscreen mode

Auto-focus for better UX:

this.$nextTick(() => this.$refs.searchInput?.focus()); 
Enter fullscreen mode Exit fullscreen mode

When to Use This Approach

Perfect for:

  • Small to medium datasets (< 50 items)
  • Real-time search requirements
  • Simple filtering logic
  • Laravel applications

Consider alternatives for:

  • Large datasets (> 100 items)
  • Complex search algorithms
  • Heavy data processing

Key Lessons

  1. Start Simple - Basic DOM manipulation often outperforms complex solutions
  2. Pre-compute When Possible - Do heavy work once, not repeatedly
  3. Progressive Enhancement - Build a working baseline first
  4. Alpine.js Shines - Perfect for form interactions and simple reactivity

Complete Working Example

Here's a full implementation you can copy and adapt:

{{-- Quick test component --}} @php $items = [ 'contact' => ['name' => 'Contact Form', 'description' => 'Simple contact form', 'category' => 'Business'], 'survey' => ['name' => 'Survey Form', 'description' => 'Multi-question survey', 'category' => 'Research'], 'registration' => ['name' => 'Event Registration', 'description' => 'Event signup form', 'category' => 'Events'], 'newsletter' => ['name' => 'Newsletter Signup', 'description' => 'Email subscription form', 'category' => 'Marketing'], 'feedback' => ['name' => 'Feedback Form', 'description' => 'Customer feedback collection', 'category' => 'Support'], ]; @endphp <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Test Searchable Component</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">Test: Real-time Search Component</h1> {{-- Search Input --}} <input type="search" x-model="search" x-ref="searchInput" placeholder="Search items..." 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" /> {{-- Items Grid --}} <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> {{-- Empty State --}} <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">No items found</p> <p class="text-gray-500">Try adjusting your search terms</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" > Clear search </button> </div> {{-- Results Info --}} <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">Current search:</strong> <span class="text-blue-600 font-mono" x-text="search || '(none)'"></span> </div> <div> <strong class="text-gray-700">Has results:</strong> <span :class="hasResults ? 'text-green-600' : 'text-red-600'" x-text="hasResults ? 'Yes' : 'No'"></span> </div> <div> <strong class="text-gray-700">Selected:</strong> <span class="text-blue-600 font-mono" x-text="selectedValue || '(none)'"></span> </div> </div> </div> </div> </body> </html> 
Enter fullscreen mode Exit fullscreen mode

How to Use

  1. Create the component - Save the above code as a Blade component
  2. Include it - Use <x-searchable-selector /> in your views
  3. Customize data - Replace the $items array with your data
  4. Style it - Adjust Tailwind classes to match your design

Key Implementation Details

Pre-computed search text:

$searchText = strtolower($item['name'] . ' ' . $item['description'] . ' ' . $item['category']); 
Enter fullscreen mode Exit fullscreen mode

Alpine.js filtering:

cards.forEach(card => { const text = card.dataset.searchText || ''; const isVisible = searchLower === '' || text.includes(searchLower); card.style.display = isVisible ? '' : 'none'; if (isVisible) visibleCount++; }); 
Enter fullscreen mode Exit fullscreen mode

Visual selection feedback:

:class="selectedValue === '{{ $value }}' ? 'border-blue-600 bg-blue-50' : 'border-gray-300'" 
Enter fullscreen mode Exit fullscreen mode

This approach scales well for typical use cases and can be enhanced later if requirements grow.


This tutorial shows the approach used in FilaForms - Laravel form infrastructure for rapid development.

Top comments (2)

Collapse
 
xwero profile image
david duymelinck

I would not call this real-time search. This is just filtering by input field.
A more user friendly way to do it is to provide categories to filter on.

Collapse
 
manukminasyan profile image
Manuk

Good point, David! You’re right — it’s technically filtering, not full search.