Tray2.se Logo

Using a datalist instead of a dropdown in your forms

Have you ever had a huge list of values that the users needs to scroll through to get to the item they wanted, you know the classic Country dropdown, and for me who lives in Sweden will have to scroll through a load of countries to get to the desired one. I know that some sites have some really nifty JavaScript that allows you to filter the list. While this is all well and good there is another option, we could use a datalist for this. The datalist has built in filtering so no more tricky JavaScript to handle that, the only caveat is that the datalist doesn't have value and text, it only has value, let me demonstrate.

Regular dropdown.

1<select name="country">
2 <option value="SE">Sweden</option>
3</select>

Datalist.

1<input list="countries" name="country">
2<datalist id="countries">
3 <option value="Sweden"></option>
4</datalist>

This means that we need to do some trickery behind the scenes to get the SE or the country_id from the database. I personally think that it is a fair trade-off, and we have some other nifty things as well when it comes to displaying the selected/filtered value.

In this demonstration I will be using Laravel.

So let's run with the countries example, and just like we would when we create a regular dropdown, we need to pass the values from our controller to the view.

1public function create()
2{
3 return view('address.create')
4 ->with([
5 'countries' => Country::query()
6 ->orderBy('name')
7 ->get(),
8 ]);
9}

In our view we create the datalist. Notice that I give the input field the name country_name, and not country_id like I would have if it were a regular dropdown, this is so that we can make the distinction later.

1<input list="countries" name="country_name" id="country_name">
2<datalist id="countries">
3 @foreach($countries as $country)
4 <option value="{{$country}}">
5 @endforeach
6</datalist>

Next up we need to create the store method, and a method that converts the country name to the country id. I will call this method getCountryId, and we pass the country name from the request.

1public function store(Request $request)
2{
3 $validAddress = $request->validate([
4 //The other validation rules
5 'country_name' => 'required',
6 ]);
7 
8 Address::create(array_merge($validAddress,[
9 'country_id' => $this->getCountryId($request->country_name),
10 ]);
11}
12 
13protected function getCountryId($countryName)
14{
15 return Country::query()
16 ->where('name', $countryName)
17 ->value('id');
18}

One thing that is very important is to set the fillable array on the model, you can't just set the guarded property to an empty array, the reason for that is, that we validate country_name which we don't have in the addresses table. That means that if we try to run the code above we will get an error stating that we don't have a column with the name country_name. So we need to set the fillable property in the Address model.

1protected $fillable = [
2 //All columns in our addresses table except id and timestamps.
3];

Caution! Unlike the dropdown, the datalist allows you to add things that doesn't exist in the list, so you need to handle that in your validation, or create the item when it doesn't exist. We can do that by updating the getCountryId() to create it if it does not exist, or we can add a validation to the country_name like this.

1public function rules()
2{
3 return ([
4 //The other validation rules
5 'country_name' => 'required|exists:countries,name',
6 ]);
7}

If you decide that you want to create items that doesn't exist, you can just change the query that returns the id. Like this.

1//Method still in the controller
2protected function getCountryId($countryName)
3{
4 return Country::query()
5 ->firstOrCreate(['name' => $countryName])
6 ->id;
7}
8 
9 
10//Method extracted to the AddressFormRequest
11public function getCountryId()
12{
13 return Country::query()
14 ->firstOrCreate(['name' => $this->country_name])
15 ->id;
16}

It's a bit debatable if a FormRequest really should create data, but I haven't really found a better place for it.

Now when the easy part is done, we need to talk about a better place to store the getCountryId() method. Sure we can keep it in the controller, but we will need it in the update controller as well. Since the validation should be handled by a form request and not inline as we did, we can move it into the form request like so.

1//AddressController
2public function store(AddressFormRequest $request)
3{
4 $validAddress = $request->validated
5 Address::create(array_merge($validAddress,[
6 'country_id' => $request->getCountryId(),
7 ]);
8 //Do redirect here
9}
10 
11//AddressFormRequest
12public function rules()
13{
14 return ([
15 //The other validation rules
16 'country_name' => 'required',
17 ]);
18}
19 
20public function getCountryId()
21{
22 return Country::query()
23 ->where('name', $this->country_name)
24 ->value('id');
25}

This way you can use the getCountryId method in both your store and update methods. Of course, you don't need to move it to the form request if you have the store and update methods in the same controller, in other words you only need to do this if you are using single action controllers.

So how do we handle this in our edit view? We handle it like we would any other input, and we don't need to loop over the options in the country field to determine which one was selected before, we just assign the value to it.

So let's start with our controller.

1public function edit(Address $address)
2{
3 return view('address.edit')
4 ->with([
5 'address' => $address->load('country'),
6 'countries' => Country::query()
7 ->orderBy('name')
8 ->get(),
9 ]);
10}

We need to load the country relation since we don't have the id from the country table to compare with, and we also need to fetch all the countries so that we can populate the datalist. I think me showing this step shouldn't be necessary, but I will do that anyway just in case. We need to define the relationship that Address has with Country in our Address model.

1public function country()
2{
3 return $this->belongsTo(Country::class);
4}

Our edit form will look almost identical to the one we used in the create view.

1<input list="countries" name="country_name" id="country_name" value="{{ $address->country->name }}">
2<datalist id="countries">
3 @foreach($countries as $country)
4 <option value="{{$country}}">
5 @endforeach
6</datalist>

The only difference is that we assign the value for the country_name input.

So we are almost there, we just need to handle the update and some validation issues. Let's start with the update.

1public function store(Address $address, AddressFormRequest $request)
2{
3 $validAddress = $request->validated
4 $address->update(array_merge($validAddress,[
5 'country_id' => $request->getCountryId(),
6 ]);
7 // Do redirect here.
8}

Nothing really out of the ordinary going on here, except that we once again fetch the country_id from the database. If you stuck with a regular, multi-action controller and kept the getCountryId inside the controller, just change this line.

1'country_id' => $request->getCountryId(),

To this and you should be good to go.

1'country_id' => $this->getCountryId($request->country_name),

So what about validation failures? We just use the old helper like we would any other field.

In our create form we just do this.

1<input list="countries" name="country_name" id="country_name" value="{{ old('country_name') }}">

We add the value just like we would on a regular text input field, and we do the same for the edit form.

1<input list="countries" name="country_name" id="country_name" value="{{ old('country_name', $address->country->name) }}">

The only difference is that we add the value received from the database as the second parameter to the old helper.

I hope you have enjoyed this quick post on something other than database related things, and I thank you for reading this post. I would also like to thank my friend @rsinnbeck for helping with the editing and the technical review. He also has a very good blog that you should check out.

//Tray2

© 2022 Tray2.se. All rights reserved.

This site uses Torchlight for syntax highlighting.