DEV Community

marelons1337
marelons1337

Posted on • Edited on • Originally published at blog.rafaljaroszewicz.com

2 1

Create dynamic dependant dropdowns with Javascript in Rails 6.1.4

a gif showing final effect
What we want to do here

I managed to pull this stunt off in an app I'm working on. Let's say we want to have a payment module. On top of the form we have three dropdowns. To make things easier for the user, I want these dropdowns to be dependant, so first I choose the building, then another dropdown only server me flats that actually belong to the building and finally after choosing a flat, I will only be able to choose tenant that is living there.

Key points:

  1. Flat and Tenant dropdowns are empty until previous dropdown has changed.
  2. After Building and Flat dropdown change, I want to fetch the data for next dropdown.
  3. I want to use the data from fetch to fill out dropdowns.

I'm setting up my arrays in the controller so I can access all the data in my view.


before_action :set_fields, only: %i[ edit new create ]
def set_fields
@buildings_array = Building.all.map { |building| [building.name, building.id] }
@flats_array = Flat.all.map { |flat| [flat.door_number, flat.id] }
@tenants_array = Tenant.all.map { |tenant| [tenant.full_name, tenant.id] }
end

Now, if I want to fetch any data from the server I will need an endpoint that will allow me to access the data in JSON format so I can easily parse it and fill my dropdowns with it.
First, I create entries in my config/routes.rb file.


get '/buildings/:id/find_flats', to: 'buildings#find_belonging_flats', constraints: { format: 'json' }, as: 'find_flats'
get '/flats/:id/get_tenant', to: 'flats#get_tenant', constraints: { format: 'json' }, as: 'get_tenant'
view raw routes.rb hosted with ❤ by GitHub

That will reflect my actions in controllers:


### app/controllers/flats_controller.rb ###
def get_tenant
set_flat
@tenant = Tenant.where(flat_id: @flat.id)
return render json: @tenant
end
private def set_flat
@flat = Flat.find(params[:id])
end
### app/controllers/buildings_controller.rb ###
def find_belonging_flats
set_building
set_flats
return render json: @flats, only: [:id, :door_number]
end
private
def set_building
@building = Building.find(params[:id])
end
def set_flats
@flats = Flat.where(building_id: @building.id)
end

Now that I have my backend setup, I can proceed with the front.


<div class="field">
<%= form.label :building_id %>
<%= form.select :building_id, @buildings_array, { include_blank: I18n.t('forms.select_building') } %>
</div>
<div class="field">
<%= form.label :flat_id %>
<%= form.select :flat_id, @flats_array, { include_blank: I18n.t('forms.select_building_first') } %>
</div>
<div class="field">
<%= form.label :tenant_id %>
<%= form.select :tenant_id, @tenants_array, { include_blank: I18n.t('forms.select_flat_first') } %>
</div>
view raw _form.html.erb hosted with ❤ by GitHub

Here I have my dropdowns that I need to fill out dynamically.

At the time of writing this post, Rails 7 has already been released, but I already started my app in 6.1.4 and managed to understand a fraction of webpacker so I decided to stick with it. My JS code is inside javascript folder.
app/javascript/forms/fetch_building_data.js

Also, I added the require statement in application.js
require('forms/fetch_building_data')

Here, I load my variables as soon as turbolinks:load is finished. That's the correct way of adding this handler, because if you try to add DOMContentLoaded or load it won't work. Rails way🛤.

Because I'm also using this script on Tenants view used to create them to have only two dropdowns (for Building and Flat) I have bundled this code into one file.
Now, first of all I set up length of dependant select tags to 1, that way only my placeholder will be available until you actually choose something. The rest of the function takes care of collecting the input from the dropdown
buildingSelect.addEventListener('input', function (event)
and storing it let buildingId = event.target.value
Functions at the bottom create options for my select and append them.


window.addEventListener('turbolinks:load', function () {
const buildingSelect = document.getElementById('tenant_building_id') || document.getElementById('payment_building_id')
const doorNumberSelect = document.getElementById('tenant_flat_id') || document.getElementById('payment_flat_id')
const tenantSelect = document.getElementById('payment_tenant_id')
if (buildingSelect && doorNumberSelect) {
if (buildingSelect.value == '') {
doorNumberSelect.length = 1;
if (tenantSelect) {
tenantSelect.length = 1;
}
}
buildingSelect.addEventListener('input', function (event) {
doorNumberSelect.length = 1;
if (tenantSelect) {
tenantSelect.length = 1;
}
let buildingId = event.target.value
let buildingPath = `/buildings/${buildingId}/find_flats`
if (buildingId) {
fetch(buildingPath)
.then(response => response.json())
.then(data => {
appendFlatOptions(data)})
.catch(err => console.log(err))
}
})
}
if (tenantSelect && doorNumberSelect) {
doorNumberSelect.addEventListener('change', function (event) {
tenantSelect.length = 1;
let flatId = event.target.value
let flatPath = `/flats/${flatId}/get_tenant`
if (flatId) {
fetch(flatPath)
.then(response => response.json())
.then(data => {
appendTenantOptions(data)})
.catch(err => console.log(err))
}
})
}
function appendTenantOptions(data) {
data.map(function (element) {
let newOptionElement = document.createElement('option')
newOptionElement.value = element["id"];
newOptionElement.innerHTML = `${element["name"]} ${element["surname"]}`;
tenantSelect.appendChild(newOptionElement);
});
}
function appendFlatOptions(data) {
data.map(function (element) {
let newOptionElement = document.createElement('option')
newOptionElement.value = element["id"];
newOptionElement.innerHTML = element["door_number"];
doorNumberSelect.appendChild(newOptionElement);
});
}
})

That's it.

Image of Wix Studio

2025: Your year to build apps that sell

Dive into hands-on resources and actionable strategies designed to help you build and sell apps on the Wix App Market.

Get started

Top comments (0)

Image of AssemblyAI

Automatic Speech Recognition with AssemblyAI

Experience near-human accuracy, low-latency performance, and advanced Speech AI capabilities with AssemblyAI's Speech-to-Text API. Sign up today and get $50 in API credit. No credit card required.

Try the API

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay