Search code examples
phplaravellaravel-livewire

Laravel Livewire live search not correctly updating results


I have the following livewire code to perform a search of page titles which works fine on the first search, but subsequent searches do not completely remove the results from the previous search.

namespace App\Http\Livewire\Page;

use App\Models\Page;
use Livewire\Component;

class Search extends Component
{
    public $searchQuery = "";
    public $searchResults;

    public function resetSearchValue() {
        $this->searchQuery = "";
    }

    public function mount() {
        $this->reset();
    }

    public function render()
    {
        if(strlen($this->searchQuery) > 2) {
            $this->searchResults = Page::query()
                ->select(['title', 'slug'])
                ->where('title', 'like', "%{$this->searchQuery}%")
                ->get();
        }
        return view('livewire.page.search');
    }
}

and

<div>
    <input 
        wire:model.debounce.350ms="searchQuery" 
        @focus="searchFocused = true" 
        @blur="searchFocused = false" 
        @click.outside="$wire.resetSearchValue()" 
        autocomplete="off" 
        type="text" 
        placeholder="Search..." 
        id="sitesearch" 
    >
    @if(strlen($searchQuery) > 0)
        <div  
            role="menu" 
            aria-orientation="vertical" 
            -labelledby="menu-button" 
            tabindex="-1"
        >
            <div class="py-1" role="none">
                @if(strlen($searchQuery) > 2)
                    @if($searchResults->count() > 0)
                        @foreach($searchResults as $result)
                            <a 
                                href="{{ route('pages.index', $result->slug) }}"
                                role="menuitem" 
                                tabindex="-1" 
                                id="menu-item-0"
                            >{{ $result->title }}</a>
                        @endforeach
                    @else
                        <p>
                            No results found
                        </p>
                    @endif
                @else
                    <p>
                        You need to type at least 3 characters
                    </p>      
            </div>     
        </div>
    @endif
</div>

Searching for "Page" for example will return something like

  • My Page 1
  • Page 2
  • Wonderful Page 3

But if I then search for "Page Z" the results I get are

  • My Page 1
  • Page 2
  • No results found

If I the search for "Page Zz" the results I get are

  • My Page 1
  • No results found

I can't work out why it's not clearing the results from previous results.


Solution

  • Basically, this is the result of a DOM-diffing issue Livewire has when it can't keep track of elements, typically dynamic elements generated in a loop.

    The simple solution to this, is to add wire:key with a value to the root element in your loop, like shown below.

    @if(strlen($searchQuery) > 2)
        @if($searchResults->count() > 0)
            @foreach($searchResults as $result)
                <a 
                    href="{{ route('pages.index', $result->slug) }}"
                    role="menuitem" 
                    tabindex="-1" 
                    id="menu-item-0"
                    wire:key="result-{{ $result->id }}"
                >{{ $result->title }}</a>
            @endforeach
        @else
            <p wire:key="no-results">
                No results found
            </p>
        @endif
    @else
        <p wire:key="searchquery-short">
            You need to type at least 3 characters
        </p> 
    @endif
    

    Also, I've added these to the other options which may be shown in place, just so it's no doubt about which element it should show.

    Just a note, all the values to wire:key on a page must be unique (like with ID attributes to HTML elements).