Remember my blog post about dynamic dropdowns in rails 6? Outdated. Now we can do it with Turbo frames and it’s so much fun! Turbo Frames allows you to easily create interactive, partial page updates in your Rails app, while Stimulus provides a simple and lightweight way to add interactivity to your HTML.
When I’m talking Turbo here, I’m only using turbo_frames for now as I’m still getting familiar with the thing but I’ll be updating my blog with a few new posts about further Turbo aspects like streams etc. 🚀
Setup
"@hotwired/stimulus": "^3.2.1",
"@hotwired/turbo-rails": "^7.2.4",
gem "rails"
gem "turbo-rails"
gem "stimulus-rails"
These are our necessities for this example. There are many tutorials out there on how to install and setup these so I won’t bother you with that.
Inside my form, I’ll have two dropdowns that will be dependent on each other.
<div class="flex justify-between gap-3">
<span class="w-1/2">
<%= form.label :expensable_type, class: label_class %>
<%= form.select :expensable_type, Property::Expense.expensable_types,
{ include_blank: true }, class: input_class %>
</span>
<% if @property_expense.expensable_type.present? %>
<span id="expensable_ids" class="w-1/2">
<%= form.label :expensable_id, class: label_class %>
<%= form.select :expensable_id,
@property_expense.expensable_values_array,
{}, class: input_class %>
</span>
<% end %>
</div>
I am using a helper method here to get the array for existing records in for example EDIT action to show correct current value. The reason I do this is because these are dynamic so in my example it can either be Property::Sale
or Property::Rental
so I need to decide up front which collection I want to inject into my select tag. I also don’t want to show the second dropdown in the NEW action so user will have to choose the expensable_type
first.
def expensable_values_array
self.expensable_type.constantize.send('name_and_id_array_for_select')
end
def self.name_and_id_array_for_select
all.map { |object| [object.get_name, object.id] }
end
Adding Turbo and Stimulus
Now, we need to add some flavour to this view, and prepare it for using stimulus and Turbo.
<div class="flex justify-between gap-3">
<span class="w-1/2">
<%= form.label :expensable_type, class: label_class %>
<%= form.select :expensable_type,
Property::Expense.expensable_types, { include_blank: true },
class: input_class,
data: { controller: 'dynamic-select',
'turbo-type': 'expensable_ids',
url: fetch_expensables_property_expenses_path } %>
</span>
<%= turbo_frame_tag "expensable_ids", class:"w-1/2" do %>
<% if @property_expense.expensable_type.present? %>
<span id="expensable_ids" class="w-1/2">
<%= form.label :expensable_id, class: label_class %>
<%= form.select :expensable_id,
@property_expense.expensable_values_array,
{}, class: input_class %>
</span>
<% end %>
<% end %>
</div>
Few things happened here.
- I added
data
attributes to the first select that will enable me to use them within Stimulus controller as well as connecting this controller. - Added a turbo_frame_tag in which I will input the results from the query of method I’m gonna show next.
As you saw fetch_expensables_property_expenses_path
must lead somewhere, and it’s just a simple action within the controller
def fetch_expensables
expensables = if params[:type].present?
params[:type].constantize.all.map { |exp| [exp.get_name, exp.id] }
else
[]
end
respond_to do |format|
format.html { render("property/expenses/frames/expensables_select", locals: { expensables: expensables }) }
end
end
namespace :property do
resources :expenses do
collection do
get :fetch_expensables
end
end
end
In this action I render a partial, and this is the partial we will render upon our action submission
<%= turbo_frame_tag "expensable_ids" do %>
<% if expensables.present? %>
<span id="expensable_ids" class="w-1/2">
<%= label_tag 'property_expense[expensable_id]', nil,
class: label_class %>
<%= select_tag 'property_expense[expensable_id]',
options_for_select(expensables), class: input_class %>
</span>
<% end %>
<% end %>
It’s really not that different from the previous view but the crucial part is that our turbo_frame_tag
has the same ID as the one in the previous view so that it knows which partial it’s going to replace on our action.
Stimulus controller
Now that we have all our partials and controllers working, let’s get the last part of it, Stimulus controller!
rails g stimulus dynamic-select
There’s really not much to it here.
import { Controller } from "@hotwired/stimulus"
// Connects to data-controller="dynamic-select"
export default class extends Controller {
connect() {
this.element.dataset.action = "change->dynamic-select#change"
}
change() {
// get value of the select
const value = this.element.value
// get data-url from the select
const url = this.element.dataset.url
// fetch turbo-type from the select
const turboType = this.element.dataset.turboType
// create new url with the value
this.url = (`${url}?type=${value}`)
// replace the turbo-frame with the new url
let frame = document.querySelector(`turbo-frame#${turboType}`)
frame.src = this.url
frame.reload()
}
}
All we are doing here is using our parameters that we have passed on from the view into this controller and using them to change the source of turbo_frame. Once that is changed, we can reload the frame and get new values working. Magic!