Multi Step Forms in Rails
  • 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 called validation_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

  • Website Page