Shikha Rajpoot
Shikha Rajpoot

Reputation: 11

How to implement Nested Dropdowns in rails

I want to implement model based dropdowns where in when value for one dropdown(model) is selected it serves the basis for selection to be in second dropdown.

And hence I am requiring the id from first dropdown to be used to apply filter for second dropdown.

<div class="study-search">
  <%= form_with(url: show_subjects_path(:site_id), method: :get, html: {style: 'font-size:16px'}) do |f| %>
    <%= f.label 'Select Study' %>
    <%= f.select :id, Study.all.collect {|study| [study.title, study.id] },  {:include_blank => "--Choose--"}%>
    <%= f.label 'Select Site' %>
    <%= f.select :site_id, Site.where(study_id: :id).collect {|site| [site.name, site.id] },  {:include_blank => "--Choose--"}%>
    <div class="btn btn-sm", style="display: inline">
      <%= f.submit "GO" %>
    </div>
  <% end %>
</div>

the :id used in the below snippet is not the correct way. Please suggest the proper way to do so.

<%= f.select :site_id, Site.where(study_id: :id).collect {|site| [site.name, site.id] },  {:include_blank => "--Choose--"}%>

enter image description here

Upvotes: 1

Views: 1705

Answers (1)

pinzonjulian
pinzonjulian

Reputation: 181

There are multiple ways to solve this with different levels of complexity. I'll start from the least complex (but probably clunky) to the most complex

The first two options will do a full page refresh which isn't as bad as it sounds, specially if you're using Turbo Drive (formerly known as Turbolinks).

The caveat with these two options is that the navigation will always be a Turbo visit navigation meaning that each time, you'll create a new page in the navigation stack. You can overcome this by overriding your form submission with Stimulus and making a Turbo GET request with a replace action. See the Turbo Drive docs for more on this.

Option 1: The simplest one

What you have there can already achieve what you expect if the user presses the Go button when they change the first select box. You'll just have to change the Site select to filter by the params sent by the form:

    <%= f.select :site_id, Site.where(study_id: params[:site_id]).collect {|site| [site.name, site.id] },  {:include_blank => "--Choose--"}%>

After the page renders again you'll have a new select box with the filtered data.

This approach is very clunky but it set's a great foundation for you to optimize it.

Option 2: Automate sending the form on change

Now if you add some Javascript, you'll be able to detect when the first select changes and submit the form when it does. For this, I suggest you use Stimulus JS

Check out the docs to learn more about Stimulus if you haven't used it to be able to follow the following

  1. Create a Stimulus JS controller to submit the form when select changes
import { Controller } from "stimulus"
export default class extends Controller {
  submit() {
    // add some code that submits a form. An easy way to do it is to use Rails UJS to do it but it's no longer recommended by the community since it'll be deprecated soon.
    Rails.fire(this.element, 'submit');
  }
}
  1. Add your stimulus controller to the form.

Note that the controller and action keywords are added to the form_with method and the first f.select method.

<div class="study-search">
  <%= form_with(url: show_subjects_path(:site_id), method: :get, data: {controller: 'form'}, html: {style: 'font-size:16px'}) do |f| %>
    <%= f.label 'Select Study' %>
    <%= f.select :id, Study.all.collect {|study| [study.title, study.id] },  {:include_blank => "--Choose--"}, { data: {action: 'form#submit' }%>
    <%= f.label 'Select Site' %>
    <%= f.select :site_id, Site.where(study_id: params[:site_id]).collect {|site| [site.name, site.id] },  {:include_blank => "--Choose--"}%>
    <div class="btn btn-sm", style="display: inline">
      <%= f.submit "GO" %>
    </div>
  <% end %>
</div>

This will essentially mimic the user clicking the Go button whenever the first select changes

Option 3: Use a Turbo Frame

If you're in the latest version of rails and you've included the hotwire-rails gem, you'll be able to leverage all the new technologies coming to Rails and the HTML over the wire techniques that come with it.

The newest versions of turbo now handle forms (as opposed to Turbolinks). Please refer to the Turbo Frames documentation for techniques on how to do this. With a turbo frame you're essentially doing the same thing as with options 1 and 2 which is sending a GET request and getting a full page back as a response BUT with the difference that with Turbo frames you'll be able to selectively replace parts of your DOM.

I'm not an expert yet on turbo frames so I won't post an exact example.

Option 4: Use Turbo Streams

This is the most complex option but is still relatively simple as far as your environment is setup with Action Cable. What Turbo Stream does is allow you to target an update to your DOM with 5 basic dom actions: append, prepend, replace, update and remove. These operations will be streamed back to the front end from the server using a web socket connection.

In your case you probably want to replace or update the select with the new list of options.

I'm not an expert yet on turbo streams so I won't post an exact example.

Option 5: Check out StimulusReflex

From StimulusReflex's site:

[Stimulus Reflex is] A new way to craft modern, reactive web interfaces with Ruby on Rails. We extend the capabilities of both Rails and Stimulus by intercepting user interactions and passing them to Rails over real-time websockets. These interactions are processed by Reflex actions that change application state. The current page is quickly re-rendered and the changes are sent to the client using CableReady. The page is then morphed to reflect the new application state. This entire round-trip allows us to update the UI in 20-30ms without flicker or expensive page loads.

With Stimulus Reflex you get a very seamless experience to do these kinds of interactive pages but it requires an extra dependency. It's definitely worth a try but it's also a new dependency you'll bring in your code base (assuming you adopt the new Rails defaults coming in version 7 which will include Hotwire)

Option 6: 100% front-end

Using Stimulus JS you can potentially load all options from all sites when the page loads and filter using just javascript. It's doable but it's definitely more complex because you'll need to write a lot more javascript and your initial page load might be slow if you have too many sites to search from. I would not recommend this approach because it might add unnecessary complexity to your app but it is an option.

Final thoughts

Regardless of what you do, if you want for the form to be submitted when the user changes something on the front-end (like changing the value of a select field or typing something into a text field, or hovering or any other action that can be tracked in the front-end) you need to write a bit of javascript. I strongly suggest you check out StimulusJS which is part of Hotwire, the newest addition to the rails ecosystem for the front end which will be the default way of building your apps with the upcoming Rails 7 release.

I hope this gives you some pointers.

Some extra links to catch up on all things Hotwire (Stimulus, Turbo etc)

Go Rails has awesome resources on the subject https://www.youtube.com/results?search_query=go+rails+stimulus

https://www.youtube.com/watch?v=AdktV7r2BQk

https://www.youtube.com/watch?v=Q7uOPVfZ3Go

Upvotes: 6

Related Questions