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 @endforeach6</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 rules5 '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 AddressFormRequest11public 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//AddressFormRequest12public function rules()13{14 return ([15 //The other validation rules16 '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 @endforeach6</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->validated4 $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