The Hotwire front-end suite of libraries can make for a robust end-user experience without a whole lot of things you have to do manually. It is very much centered around the main paradigm of HTTP, which is the request-response cycle. This puts it at odds with React-style web programming, which is actually (far too often) a distributed system rather than just a view layer. Turbo lets your server do your rendering, which is why you went to all the trouble of writing HTML in a Rails app in the first place, right?
Ok, here's a quick example, because we need to anchor some theory: You have an app that lets people sign up for lessons. Your organization offers a lot of lessons in a lot of locations, so you need to let your users pick where they are before picking a lesson. Let’s make a form that creates a Signup
object, which will model the relationship between a Lesson
and a Location
(and probably a User
, but we don’t need to worry about that for this example).
To create a Signup
, we'll need a select
dropdown to choose a Location
and then another to choose from the Lesson
s that are available there. Buuut... how to do that? We could load the options in a JSON document and insert them into the select
, but that means doing JavaScript work. Well, sure, that's not hard, but it means more code, and more code means more liability.
On the other hand (and to uncover the buried lede), what if you didn't have to write more than a single line of JS? We can make use of Turbo and our own preexisting ERB to handle this.
You can submit a form using a button
element, of course. But while the form
element specifies the method
and action
, your button
doesn't have to care about that! It can specify other ones, using the formmethod and formaction attributes.
This button
now becomes your refresh button. If you give it a formmethod
of GET
and a formaction
that loads the new
action, you can render the form again, but with different data! With Turbo, it'll drop it right in place of the existing one. You can hide the button if you want (you don't need to if you don't mind seeing it, and having the user press it means zero manual JS).
A word of caution, however! Since this is submitting a form with a GET request, all the parameters will be sent in the URL and will be visible in both the URL bar (so it’ll be in the browser history), and the HTTP logs on the server (including analytics). If you ever use this technique, be sure to strip any sensitive fields like passwords or credit card numbers before the form is submitted. But also note that this means the password and/or credit card will appear to be emptied out when the form changes. This does mean more JavaScript, but I find this to be a fair tradeoff for information security.
And so if you have this ERB:
# app/view/signups/new.html.erb
<%= turbo_frame_tag @signup do %>
<%= form_for @signup do |form| %>
<%= form.button "Refresh", id: "refresh", formmethod: "GET", formaction: new_signup_path %>
<%= form.select :location, Location.all.map { [it.name, it.id] } %>
<% if @signup.location %>
<%= form.select :lesson, (@signup.location&.lessons || {}).map { [it.name, it.id] } %>
<% end %>
<% end %>
<% end %>
and this action in your SignupsController
:
def new
@signup = Signup.new(params[:signup])
end
then, you'll render HTML that looks like this:
<turbo-frame id="new_signup">
<form class="new_signup" id="new_signup" action="/signups" accept-charset="UTF-8" method="post"><input type="hidden" name="authenticity_token" value="<AUTH_TOKEN>" autocomplete="off">
<button name="button" type="submit" id="refresh" formmethod="GET" formaction="/signups/new" hidden="hidden">Refresh</button>
<select onclick="this.form.refresh.click()" name="signup[location_id]" id="signup_location_id"><option value="1">Boston, MA</option>
<option value="2">Lowell, MA</option>
<option value="3">Manchester, NH</option>
</select>
</form>
</turbo-frame>
…and you have a form with a button that refreshes the form using the newly entered values.
Now, if you want the refresh to be automatic (and hidden), you can make this change below. It is the only JS you need to write for this feature to work. The rest is handled by Turbo. It's in an onChange
handler to keep things small and highlight how little JS you need -- if I were doing this in a real app, I would stick this in a Stimulus controller or something like it.
# app/view/signups/new.html.erb:4
- <%= form.button "Refresh", id: "refresh", formmethod: "GET", formaction: new_signup_path %>
- <%= form.select :location_id, Location.all.map { [it.name, it.id] } %>
+ <%= form.button "Refresh", id: "refresh", formmethod: "GET", formaction: new_signup_path, hidden: true %>
+ <%= form.select :location_id, Location.all.map { [it.name, it.id] }, {}, onClick: "this.form.refresh.click()" %>
So, now, when we pick a location from the select, Turbo will automatically reload the form with the lesson options for that location made available. All with only one line of handwritten JavaScript!