How to return different partial with hotwire on select dropdwon change

In a rails 7 project, say I wanted to have a select dropdown and depending on what option selected return a different partial via hotwire with different form fields in them. What is the best way to do that? Any examples?

The overall approach:

  1. catch the event in a parent element that’s controlled by a Stimulus controller (let’s say form_controller), and
  2. make a turbo_frame (containing the new form fields) update itself based on the new selection.

For 1, you’d catch a change event from the select. data-action="change->form#updateFormPartial

I don’t like the generic names form_controller and updateFormPartial. To me I’d like to either go broad and reusable (form_partial_controller) or specific to the form’s purpose (country_details_controller), but you get the idea.

For 2, the updateFormPartial method in your Stimulus controller would change the src attribute of the turbo_frame with a query string parameter signalling the select’s value. It could event point to the current page’s URL (e.g. new_country_path), because the resulting request to the current page (with the new query string param) that’s being summoned by the change in src of the turbo_frame will automatically extract and replace the new page’s matching turbo_frame into the current one. You just need to make sure the turbo_frame’s id in both places is the same, and it’ll be updated on changing the src attribute.

Then you’d need to update your rails controller to detect the query string param and serve a different partial into that turbo_frame. Voilà.

2 Likes

Thanks for the info! I have been messing around with this but am unfamiliar with some parts of your suggestion.

<%= select_tag :occurrence, options_for_select(Scheduleevent.occurrences) , { data: {  action: "change->form_partial#updateFormPartial", url: new_scheduleevent_path({ scheduleevent: { occurrence: "ShowSubTypes($(this).val())" } })} %>


<%= turbo_frame_tag "form_partial_fields" do %>

<% end %>

form_partial_controller.js

import { Controller } from "@hotwired/stimulus"


export default class extends Controller {
  updateFormPartial(e) {
    e.preventDefault();

    const { url } = e.target.dataset;

    this.element.src = url; 
  }
}

I’m not really sure if I am on the right track or not, or how to make all this work. It does not generate errors but does not affect the page either. What am I doing wrong?

You would not need any custom Hotwire for this, except perhaps some stimulus to trigger the on change event on the dropdown. What you could do instead is simply render different partials based on the select value.

Let’s say that the stimulus controller makes a request against whatever#select, and passes in the selected value, nothing special here you can work with this value via say params[:form_name][:selected]. In your rails controller, you would optionally do some basic validation against a whitelist that the selection is valid, and pass the validated value as an ivar, say @selected to your view template, which is select.html.erb.

In the template, you would do something like this:

<% turbo_frame_tag :section_two %>
  <%= render partial: @selected %>
<%- end %>

Now you do have to create the various partials based on the selected values, and you’d be done.

Thanks for the info. I understand what you are saying but I’m not that familiar with stimulus and turbo to translate that into what to set up. Would you be able to do a basic example of the form select, stimulus controller, controller and partial render? I think then I will get it ha.

In your HTML:

<select data-controller="remote-select" data-remote-select-url-value="/whatever/select" data-remote-select-name-value="whatever_form[selected]">
  <option value="a">A</option>
  <option value="b">B</option>
  <option value="c">C</option>
</select>

<%= turbo_frame_tag :section_two %>
  <h2>You have not selected anything</h2>
<%- end %>

Stimulus controller controllers/remote_select_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    url: String,
    name: String,
  }

  connect() {
    this.element.dataset.action = "change->select#update";
  }

  update() {
    const url = new URL(this.urlValue);
    url.searchParams.append(this.nameValue, this.element.value);

    Turbo.visit(url, { action: "replace" });
  }
}

Rails controller:

class WhateverController
  def select
    @selected = params[:whatever_form][:selected] # unsafe, as value is not whitelisted
  end
end

View template views/whatever/select.html.erb:

<%= turbo_frame_tag :section_two %>
  <%= render partial: @selected %>
<%- end %>

Partials:

# views/whatever/_a.html.erb

<h2>You selected A</h2>

# views/whatever/_b.html.erb

<h2>You selected B</h2>

# views/whatever/_c.html.erb

<h2>You selected C</h2>

Thanks! I am trying to get this working but changing the value of the select box does not trigger anything to happen in the stimulus controller. I can see the controller connecting, but can’t trigger the update event.

<%= form_with(model: scheduleevent, class: "contents", data: { controller: 'nested-form,', nested_form_wrapper_selector_value: '.nested-form-wrapper' }) do |form| %>

  <select data-controller="remote-select" data-remote-select-url-value="/scheduleevent/select" data-remote-select-name-value="form[occurrence]">
        <%= Scheduleevent.occurrences.each do |occurrence| %>
          <option value="<%= occurrence.second %>"><%= occurrence.first %></option>
        <% end %>

  </select>
                 
  <%= turbo_frame_tag :section_two do %>
    <h2>You have not selected anything</h2>
  <% end %>

<% end %>

remote_select_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static values = {
    url: String,
    name: String,
  }

  connect() {
    console.log("Connected to controller.");
    this.element.dataset.action = "change->select#update";
  }

  update() {
    const url = new URL(this.urlValue);
    url.searchParams.append(this.nameValue, this.element.value);

    Turbo.visit(url, { action: "replace" });
  }
}

Controller:

  def select
    @selected = params[:form][:occurrence] # unsafe, as value is not whitelisted
  end

Any thoughts? There are no errors in the console, or rails server.

I’m not 100% sure (haven’t tried this yet) but you may be short-circuiting some process that Stimulus uses to set up the element by “cutting to the chase” as you do here:

connect() {
    console.log("Connected to controller.");
    this.element.dataset.action = "change->select#update";
  }

What you may want to do instead is mutate the DOM, and let the Stimulus observer do the complete setup for you.

connect() {
    console.log("Connected to controller.");
    this.element.setAttribute('data-action', 'change->select#update');
  }

Or, since you have already set all the other attributes in the Rails-generated HTML, just put this one in there as well…

<select data-controller="remote-select" 
        data-remote-select-url-value="/scheduleevent/select" 
        data-remote-select-name-value="form[occurrence]" 
        data-action="change->select#update">
        <%= Scheduleevent.occurrences.each do |occurrence| %>
          <option value="<%= occurrence.second %>"><%= occurrence.first %></option>
        <% end %>

  </select>

It’s not clear to me why you are setting up that action observer in the connect method, it doesn’t seem to be gaining you any simplification.

Walter

I’m not sure how to do it and was following the suggestion from one of the answers above. If you know a more efficient way to accomplish what I am trying to do I am open to any suggestions.

Also shouldn’t this: data-action="change->select#update"> be this? data-action="change->remote-select#update">

Yes, that was a typo. Changing it to “remote-select” should do the trick.

It is trying to trigger something now, but I am getting this error in the console:

Error invoking action "change->remote-select#update"

TypeError: Failed to construct 'URL': Invalid URL

Any other thoughts on this? Still struggling to get this working.

URL() function require full URL instead PATH.