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 (0)