29 Aug
Dependent Dropdowns with Hotwire - Rails Tricks Issue 17
This week I will show you how to make dependent dropdowns with Hotwire!
I will use a toy app as an example. This app will have a page where addresses can be created. The address will consist of a country, a state, a city, and a postcode. Except for the postcode, we will have a list of options coming from the database and when the user selects the country, we load the states for the selected option. When the user selects the state we will load the cities in that state. Let’s start by generating a Rails app, the necessary models and a scaffold for the address:
I already have an excerpt of a list of countries, states, and cities in the necessary yaml format, so I just moved those into the fixtures folder of the project and loaded them:
I will use a toy app as an example. This app will have a page where addresses can be created. The address will consist of a country, a state, a city, and a postcode. Except for the postcode, we will have a list of options coming from the database and when the user selects the country, we load the states for the selected option. When the user selects the state we will load the cities in that state. Let’s start by generating a Rails app, the necessary models and a scaffold for the address:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQyLCJwdXIiOiJibG9iX2lkIn19--ae88519c5627196d458ed44b707483e21877b30c/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(11).png)
I already have an excerpt of a list of countries, states, and cities in the necessary yaml format, so I just moved those into the fixtures folder of the project and loaded them:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzLCJwdXIiOiJibG9iX2lkIn19--3747513343b544f4bb8446268ee04eaa78f59726/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(12).png)
We need to define the has_many associations on the Country and State model, and while we are there, let’s set a default scope to order them by name:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0LCJwdXIiOiJibG9iX2lkIn19--363cc7450e4128490eed8b0b35787d794b0b65e5/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(13).png)
In the view files, Rails generated text_field inputs for the associations, let’s convert all three to collection_select:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ1LCJwdXIiOiJibG9iX2lkIn19--ff71134c80760c2f629ba83b553f36274523eefa/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(14).png)
We are getting to the fun part now. The goal is to submit the form when a country is selected, so the states field is populated with the appropriate ones. Let’s add a button underneath the country selector to see how this works:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ2LCJwdXIiOiJibG9iX2lkIn19--384ea96dabae190684d9b102933995ab14968ea6/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(21).png)
If you start the Rails app and click that “Select” button, it submits the form and the the states are loaded. But the whole form is reloaded on the screen, so you see the validation errors on the top. Imagine that this is part of a larger form, that would make this an even worse user experience. But don’t worry, there is turbo frames to the rescue. Let’s wrap the section we want to reload when this button is clicked into a turbo frame, and set the button to reload that frame only:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ3LCJwdXIiOiJibG9iX2lkIn19--7b0ecc511fd5a2beb5b5b0612bc5d6ae57be6a76/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(22).png)
If you reload the page and click the button again, it will only reload the frame with the three dropdowns. The next step is to trigger the reload on the change event of the select element. To achieve this, we can create a Stimulus controller:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ4LCJwdXIiOiJibG9iX2lkIn19--a9172f22995082835dff531c1516e83f4bdcc309/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(23).png)
In the controller we need to set a target for the button and a function to trigger a click event when we call it:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ5LCJwdXIiOiJibG9iX2lkIn19--4ff63c2bfe3e501ca35f4396f11ac8ae331dccdf/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(24).png)
And we need to update our view to use this controller:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTUwLCJwdXIiOiJibG9iX2lkIn19--7aa8ebca5f47297ababc79a8a99ef272e27bc578/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(25).png)
We can also hide the button, since the user no longer needs to click it. And we need to change the markup for the state_id section too, and we will have a working dependent dropdown for the address fields.
If you look at the network tab of the browser, you can see that Rails still returns the markup for the whole page, but since we only need the form, we can at least discard the layout from rendering to gain a little performance improvement. To achieve this, we need to change the render line in the controller to not render the layout when the request format is turbo_frame:
![](https://pombomailer.com/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTUxLCJwdXIiOiJibG9iX2lkIn19--18dbe95efe6e52abd0e18ad3c7ca9f83df1b0d87/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fbGltaXQiOlsxMDI0LDc2OF19LCJwdXIiOiJ2YXJpYXRpb24ifX0=--cefc62acd607acb36c209b04aa5893ccc86cdf29/carbon%20(26).png)
If you want to see the source code for the above example, it is on Github: https://github.com/gregmolnar/dependent-dropdown
That’s it for this week! Until next time!