Autocomplete with Livewire

Studying up for the Fly.io exam."
Image by Annie Ruygt

We’re gonna do some autocompletion with Livewire. Livewire works best when your app is close to your user. With Fly.io, you can get your Laravel app running globally in minutes!

The deal with React is that I don’t want it, but I’m jealous of the quality of React components.

Auto complete fields are an example. There’s a lot of hidden complexity and (even still) browser compatibility issues.

Here’s the rub: There are so many people using React that the quality of components is often very high. Porting that quality over to other frontends (such as Livewire) is hard!

Word on the street is that Livewire is actually working on an auto-complete field of its own. But I want something now. Luckily, there just so happens to be pretty good browser support for this idea.

It’s workable-but-kinda-ugly enough for me to love it.

Native Autocomplete

It turns out that modern browsers support the idea of auto-complete via datalist.

When combined with a text input, we get a list of stuff that a person can select. This comes comes with some bells and whistles such as keyboard shortcuts to navigate the list. Here’s an interactive look at how to do that. This will perform some basic auto-complete based on the options in the datalist. If you select an option from the list of possible values, it will update the text input’s value. Nice.

html input with datalist providing native autocomplete

Here’s where it gets a bit wonky, however. The only value we can get from the selected data is the display value.

Often you want some machine-readable value but with a human-readable label. Maybe we can add some JS and a data attribute?

<datalist id="some-data">
    <option data-value="1" value="foo" />
    <option data-value="2" value="bar" />
    <option data-value="3" value="baz" />
</datalist>

That looks good, we should be able to get the value from the datalist for a given selected label. But…we can’t.

Getting Values

We want to figure out a way to get the machine-readable value held in the data-value attributes when a user selects a label.

Unfortunately we can’t actually listen for change events directly on the datalist element. Instead, we need to use a change event on the <input /> itself.

This gives us an avenue to get the values we want, it’s just a teansy bit hacky. But this is Javascript — When in Rome!

Here’s what we’ll do. We listen for change events on the text input. When its value is changed, we find a matching value in the datalist and grab it’s data-value attribute. This means we’re matching text input value with the datalist value, using the human-readable label. I don’t really love this, but it works for most use cases.

// When we change the value of the text input
let onChange = (e) => {
    // Get the text input value
    // It will be the human-readable label from the
    // datalist's value="foo" attribute
    let value = e.target.value

    // Get the data-value attribute by selecting the datalist element
    // with a matching value ('foo', 'bar', 'baz' in our case)
    // This might create an invalid css selector, 
    // but you could also find the datalist element
    // and do a foreach on its child options
    let selected = document.body.querySelector("datalist [value=\""+value+"\"]")

    // If we find the selected option, grab the
    // machine-readable ID from the data-value attribute
    if (selected) {
        let id = selected.dataset.value
        console.log('selected value is:', id)
    }
}

Then we add that function as the listener for that input:

<input 
    type="text" 
    name="autocomplete" 
    class="rounded"
    list="some-data" 
    placeholder="choose a thing"
    onchange="onChange(event)" />

Here’s a JS Fiddle you can use to play with that.

Now we can get the data-value attribute to get a numerical ID (1, 2, 3) that relates to the human-readable label (foo, bar, baz) used when selecting a possible value from that dropdown list.

Fly.io ❤️ Laravel

Speed up Livewire and Fly your servers close to your users. Deploy globally on Fly in minutes!

Deploy your Laravel app!

Livewire

We can translate this to Livewire (and a hint of AlpineJS) pretty easily! The one thing I’ve added is populating the dataset options list dynamically based on user input. This was useful for a project where I auto-completed user addresses.

Assuming you have Livewire installed, we can create a new component and use that to help autocomplete based on user input.

php artisan make:livewire AddressAutocomplete

This generates files:

  1. app/Http/Livewire/AddressAutocomplete.php
  2. resources/views/livewire/address-autocomplete.blade.php

The template file can contain (among other things for fancier presentation), the following:

<form
    class="space-y-8 divide-y divide-gray-200"
    x-data='{
        addressSelected(e) {
            let value = e.target.value
            let id = document.body.querySelector("datalist [value=\""+value+"\"]").dataset.value

            // todo: Do something interesting with this
            console.log(id);
        }
    }'
>
    <input
        type="text"
        list="streetAddressOptions"
        wire:model="streetAddress"
        class="fancy-tailwind-things"
        x-on:change.debounce="addressSelected($event)"
    >

    <datalist id="streetAddressOptions">
        @foreach($searchResults as $result)
            <option
                wire:key="{{ $result->uniqueKey }}"
                data-value="{{ $result->uniqueKey }}"
                value="{{ $result->fullAddress }}"
            ></option>
        @endforeach
    </datalist>
</form>

We use AlpineJS’s x-data on the <form> to define function addressSelected. This is the change event handler when the value of our input is updated. Just like we saw up above, this matches a given address to an option in the datalist and grabs the data-value attribute, so we get a numerical ID (or whatever we need for the machine-readable data).

On the input, we use Alpine again to listen for change events (with debounce used, so we don’t send data over the wire on every keystroke).

The dynamic part here is the @foreach loop that updates the options in the datalist. This gets updated dynamically by Livewire.

The $searchResults variable gets updated based on the value that’s in the text input. We’ve used Livewire to wire up that text input to PHP variable $streetAddress.

To decide what populates the datalist options, we need to look at our Livewire component controller, file app/Http/Livewire/AddressAutocomplete.php (where variable $streetAddress is defined).

<?php

namespace App\Http\Livewire;

use Livewire\Component;
use Facades\App\Services\Smarty\Smarty;

class AddressAutocomplete extends Component
{
    public string $streetAddress = '';
    public string $city = '';
    public string $state = '';
    public string $zip = '';
    public string $country = '';
    public array $searchResults = [];

    // Magic method that is fired when `streetAddress` is updated
    public function updatedStreetAddress()
    {
        if($this->streetAddress != '') {
            // An array of SearchResults
            $this->searchResults = 
                Smarty::searchAddress($this->streetAddress);
        } else {
            $this->searchResults = [];
        }
    }

    public function render()
    {
        return view('livewire.address-autocomplete');
    }
}

I used the Smarty address API (not really shown, it’s hiding behind the Smarty facade) to take the user input and return a list of possible matching addresses.

The updatedStreetAddress function is a bit of magic that Livewire provides. When variable streetAddress is updated, that method is called if it’s present. That property is updated when there is user input, and so we can use that to have Smarty return a set of addresses to us.

autocompleting addresses

The search results are set in the $searchResults variable, which is sent back to the frontend and populates the datalist. And boom, we have an autocomplete field!