Dynamic forms in Hotwire
Related videos
In this tutorial, we will explore how to use Turbo Frames for creating dynamic form fields in a Rails application. We will create a form that allows users to sign up as an individual or a company. Depending on their choice, the form fields will be updated accordingly.
Scenario
Our form will initially have the following fields for signing up as an individual:
- Name
- Date of Birth
- Password
When the user chooses to sign up as a company, the form should remove the 'Date of Birth' field and replace 'Name' with 'Company Name' and add a 'Tax Number' field.
Setup
First, we start by creating an Account
model with an enum for account_type
. Enums in Rails allow you to define a list of possible values for a given attribute. In our case, the account type can be either 'individual' or 'company'. Enums also provide helper methods like individual?
and company?
to check the account type.
# app/models/account.rb
class Account < ApplicationRecord
enum account_type: {
individual: "individual",
company: "company"
}
end
We've already created the form with the necessary fields, and we will now add radio buttons to toggle between signing up as an 'Individual' or a 'Company'.
<!-- app/views/registrations/new.html.erb -->
<%= f.label :account_type, value: "individual", class: "flex items-center" do %>
<%= f.radio_button :account_type, "individual", class: radio_button_class %>
<span class="ml-1.5 sm:ml-2.5"><span class="hidden sm:inline">an </span>individual</span>
<% end %>
<%= f.label :account_type, value: "company", class: "flex items-center" do %>
<%= f.radio_button :account_type, "company", class: radio_button_class %>
<span class="ml-1.5 sm:ml-2.5"><span class="hidden sm:inline">a </span>company</span>
<% end %>
Our form will now show the additional fields for company sign-up, but they will not be hidden or shown dynamically based on the radio button selected.
Adding TurboFrame tags
We will wrap the top fields in a TurboFrame tag to make the form fields dynamic.
<!-- app/views/registrations/new.html.erb -->
<%= turbo_frame_tag "account_types" do %>
<!-- All the different Fields to toggle -->
<% if @account.company? %>
<!-- Company fields -->
<% else %>
<!-- Individual fields -->
<% end %>
<% end %>
By wrapping the fields inside a TurboFrame, we can load and update them independently from the rest of the page. To do this, we will use a StimulusJS NavigateController
to listen for radio button changes and trigger the TurboFrame navigation.
First, create a new StimulusJS controller called navigate_controller.js
// app/javascript/controllers/navigate_controller.js
import { Controller } from "@hotwired/stimulus";
/*
* Usage
* =====
*
* add data-controller="navigate" to the turbo frame you want to navigate
*
* Action (add to radio input):
* data-action="change->navigate#to"
* data-url="/new?input=yes"
*
*/
export default class extends Controller {
to(e) {
e.preventDefault();
const { url } = e.target.dataset;
this.element.src = url;
}
}
Next, attach the Navigate controller to the TurboFrame tag.
<!-- app/views/registrations/new.html.erb -->
<%= turbo_frame_tag "account_types", data: { controller: "navigate" } do %>
<!-- All the different Fields to toggle -->
<% end %>
Finally, we add a data-action
attribute to each radio button and set the proper URL for individual and company account creation:
<!-- app/views/registrations/new.html.erb -->
<%= f.label :account_type, value: "individual", class: "flex items-center" do %>
<%= f.radio_button :account_type, "individual", class: radio_button_class, data: { action: "change->navigate#to", url: new_registration_path({ account: { account_type: "individual" } }) } %>
<span class="ml-1.5 sm:ml-2.5"><span class="hidden sm:inline">an </span>individual</span>
<% end %>
<%= f.label :account_type, value: "company", class: "flex items-center" do %>
<%= f.radio_button :account_type, "company", class: radio_button_class, data: { action: "change->navigate#to", url: new_registration_path({ account: { account_type: "company" } }) } %>
<span class="ml-1.5 sm:ml-2.5"><span class="hidden sm:inline">a </span>company</span>
<% end %>
Now, clicking the radio buttons will trigger the navigate
method of our controller and load the appropriate form fields inside the TurboFrame.
Updating the Controller
Our Rails controller needs to handle the dynamic URL parameters for the different account types.
# app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController
def new
# We construct the new instance here using the account params
# which allows our URLs to pass in query params to the new
# form, switching the fields appropriately.
@account = Account.new(account_params)
end
def create
@account = Account.new(account_params)
if @account.save
redirect_to signup_success_path
else
render :new, status: :unprocessable_entity
end
end
private
def account_params
params.require(:account).permit(
:account_type, :name, :company_number, :date_of_birth, :email, :password
)
rescue
{}
end
end
This will ensure that the correct account type is set when navigating between the individual and company radio buttons.
Conclusion
This tutorial demonstrated using Turbo Frames and a sprinkling of stimulus to create dynamic form fields in a Rails application. By leveraging Turbo Frames, we could easily update and load the form fields independently from the rest of the page based on the user's selected account type.
Feel free to leave any questions or comments below, and share any alternative methods you've used for accomplishing dynamic form fields in Rails.