-
Notes
-
Using a gem for this adds additional complexity and maintenance burden which is unnecessary given that the "state machine" component of multi step forms is actually very straight forward. So please do not use multi step wizard gems like Wizard.
-
Requirements
-
Easy to understand
-
Uses the server for state
-
Allows partial completion (user can leave & come back)
-
-
Steps
-
First, we're going to create a new ActiveRecord model that will track the user journey from beginning to end. For example, for a fintech product the model might be called
KycOnboarding
. Then, we expose a new method calledvalidation_set
using Rails'attr_accessor
. What this means is that we can pass information around on the KycOnboarding model without persisting it to the database. You'll see why that's useful later on. -
Additionally, in order to allow a user to leave their application and come back, we need to generate a "shortcode" that we can use to identify it.
-
class KycOnboarding < ApplicationRecord attr_accessor :validation_set before_validation :generate_shortcode def generate_shortcode if self.shortcode.blank? loop do self.shortcode = SecureRandom.alphanumeric(5).downcase break string unless KycOnboarding.where(shortcode: string).first end end end end
-
Forms & Conditional Validations
-
A multi step form can have dozens of inputs, all which need to be validated. We can use Active Record validations for this as we usually would in Rails. The only difference is that we want to run different validations at different steps. For example on the first step we want to validate the name, then on step 2 we validate the email, then the phone number, and so on.
-
Firstly, we're going to handle every step of the form of with a single controller action. If the user is trying to access an existing application, we fetch that. Otherwise we create a new one.
-
class KycOnboardingsController < ApplicationController def new get_kyc_onboarding end def get_kyc_onboarding if params[:shortcode].present? @kyc_onboarding = KycOnboarding.find_by_shortcode(params[:shortcode]) else @kyc_onboarding = KycOnboarding.new end end end
-
On to the views - because we're using one controller action for all steps, even if our wizard has 30 steps, they'll all get wrapped & submitted inside a single form.
-
<!-- /app/views/onboarding/form.html.erb --> <% @current_step = @kyc_onboarding.current_step %> <form method="post" action="<%= new_kyc_onboarding_path %>" > <%= render partial: "/onboarding/#{@current_step}_step" %> <input type="submit" /> </form>
-
Validations based on the step the user is on
-
We mentioned earlier that we need to have a way to tell Rails to run different validations at different steps. On the model, we've already added the
validation_set
accessor, which means we can now pass that through from the view, to the controller, and into the model. -
On the view, we're going to use a hidden input field in the form, to tell the controller which set of validations to run.
-
<!-- /app/views/onboarding/_phone_step.html.erb --> <input type="hidden" name="kyc_onboarding[validation_set]" value="phone" > <input type="text" name="kyc_onboarding[phone]" />
-
In the controller
-
class KycOnboardingsController < ApplicationController def new get_kyc_onboarding if request.post? @kyc_onboarding.assign_attributes(new_kyc_onboarding_params) @kyc_onboarding.save end end def new_kyc_onboarding_params params.require(:kyc_onboarding).permit( :validation_set, :name, :phone, :email ) end end
-
Now, because we're passing
validation_set
through from the view and assigning it in the controller, we can use it for conditional validations on the model, which looks like this. -
class KycOnboarding < ApplicationRecord validates_presence_of :name, if: proc { |kyc| kyc.validation_set == "name" } validates_presence_of :phone, if: proc { |kyc| kyc.validation_set == "phone" } validates_presence_of :name, if: proc { |kyc| kyc.validation_set == "email" } end
-