This article will run through two common patterns in Rails apps: multi step forms, and forms with nested relationships.We’ve found that while these patterns are reasonably straightforward when you know how, they’re not obvious. So this tutorial will go through putting them together, step by step.
The parent table is the table that carries the user through whatever multi step process they’re completing. Whenever a user starts a new submission, our process is as follows:
Create a new record in the parent table.
Generate a shortcode for that record.
Pass this shortcode from step to step throughout the process.
For the purposes of this tutorial, let’s imagine our parent table is the Order model. First we’ll add a new column to the table
Copy
rails g migration add_shortcode_to_orders shortcode:stringrails db:migrate
Next, we make sure a record always has a shortcode.
Copy
# /app/models/order.rbclass Order < ApplicationRecord before_validation :set_shortcode def set_shortcode return if self.shortcode.present? loop do string = SecureRandom.alphanumeric(5).downcase self.shortcode = string break string unless Order.where(shortcode: string).first end endend
ActiveRecord validations are great, but usually they’re applied universally, not conditionally. For this flow, we want to have a way to switch between different “sets” of validations, depending on what step the user is on.
For the first step of the flow, the user will simply click on a link and get taken to step one of a form.
Copy
# config/routes.rbRails.application.routes.draw do get "/orders/new" => "orders#new", :as => "new_order"end
Copy
class OrdersController < ApplicationController def new @order = current_user.orders.new if @order.save redirect_to order_step_1_path(@order.shortcode) end endend
Now, we can add a simple link to the app. Any time a user clicks on it, a new order will be created and they’ll be redirected to the first step.
The view above will display the form to the user and let them fill it in. But we also want to cover the scenario where the user has submitted the form but some of the validations haven’t passed.
Copy
Rails.application.routes.draw do # Get request simply shows the form. Post request submits it. Both serve the step1.html.erb partial match "/orders/:shortcode/step-one" => "orders#step1", :as => "order_step1", :via => [:get,:post]end
Copy
<%= form_for @order, order_step1_path, method: "post" do |f| %> <% @order.errors.each do |error| > <%= error %> <% end > <div class="space-y-4"> <div> <%= f.label :first_name %> <%= f.text_field :first_name %> </div> <div> <%= f.submit %> </div> </div><% end %>
Update the controller action. If a user submits the form (post request), validate and save the changes, then redirect to step2.
Copy
class OrdersController < ApplicationController def step1 get_order if request.post? @order.validation_set = "step1" if @order.update(order_params) redirect_to order_step2_path end end end def get_order @order = current_user.orders.find_by(shortcode:params[:shortcode]) end def order_params params.require(:order).permit(:first_name) endend
To recap, the above code will handle three different scenarios.
The user has landed on the first step of the form but not submitted it yet. Just show them the form.
The user has submitted the form and there are validation errors. Show them the form as well as the validation errors.
The user has submitted the form and the validation has passed. Redirect them to the next step
The same pattern can be repeated for each step of the form.
class OrdersController < ApplicationController def add_item @item = @order.items.new if @item.save # Render whatever action the user is currently on render :step1 end endend
Build your form using the fields_for form helper
Copy
class Order < ApplicationRecord accepts_nested_attributes_for :order_itemsend
Copy
<%= form_for @order, url: update_order_path(@order.shortcode) do |form| %> <%= form.fields_for :order_items do |item_form| %> <%= item_form.text_field :first_name %> <% end %><% end %>
Sometimes you might only want to show inputs for a subset of the records.