Rails Wizards! 🎉


Users

Users is the baseline model for the database-persisted wizard approach. Every time someone hits 'new' a new (empty) record is created in the database and the user is redirected to the steps controller nested inside that unique resource's URL. This is a very straightforward approach, tried and true.

Turbo Drive has been specifically disabled for this wizard just to ensure a fully-baseline operation. It is enabled in the Boats wizard and beyond. For each step in this wizard you should see your browser's native loading bar run on each step's submit button.


First name Last name Middle name Favorite pizza Favorite ice cream Favorite sandwich Email Pet count Pet name
Bruce Wayne Piiza Iced cremes sandwiches 9 freds Show Edit Destroy
asdf asdf 123 asdf Show Edit Destroy
test test etw wrgw wrgerg 2 1234 Show Edit Destroy
Show Edit Destroy
one one o Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Daniel Herrera Torres Show Edit Destroy
Show Edit Destroy
sdf sdf sdf dfs dfs dfs 0 df Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
qw fd 4 hg Show Edit Destroy
Bob Yes Euh Refina Bj Ham 1 Tobby Show Edit Destroy
Hhh Bbbb Hhbb 1 No Show Edit Destroy
Show Edit Destroy
ds aa is pepperoni vanilla chicken 2 aa Show Edit Destroy
Baxter Callan Meat Lovers Vanila Katu 1 Mick Show Edit Destroy
aa cc bb dd ee ff 1 ww Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
John Doe Pep Vanilla Pulled Pork 1 Dog Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
fsdf dsfsd dsf Show Edit Destroy
Stan Man the Meowgharita Cockolate Blonde, brunett 1 Lulu Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
First LAAST Middle Bocata 3 Pet Show Edit Destroy
First2 tsriF First Show Edit Destroy
Romero Romero Romero Pineapple Avocado Nutella 15 Apache Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
ff ff Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Hello Pringles There Cookies With Cream 12 32 Show Edit Destroy
Show Edit Destroy
james bond Show Edit Destroy
Test McTesterson All None Some 0 Zero Show Edit Destroy
Show Edit Destroy
James Devine S 1 Smith Show Edit Destroy

New User
User Model Code (Click to see)

# Generated from
# rails g scaffold User first_name middle_name last_name favorite_pizza favorite_ice_cream favorite_sandwich pet_count:integer pet_name 

class User < ApplicationRecord
  enum form_steps: {
    names: [:first_name, :middle_name, :last_name],
    foods: [:favorite_pizza, :favorite_ice_cream, :favorite_sandwich],
    pets: [:pet_count, :pet_name]
  }
  attr_accessor :form_step

  with_options if: -> { required_for_step?(:names) } do
    validates :first_name, presence: true, length: { minimum: 2, maximum: 20}
    validates :last_name, presence: true, length: { minimum: 2, maximum: 20}
    validates :middle_name, length: { maximum: 20 }
  end

  with_options if: -> { required_for_step?(:foods) } do
    # Foods info is all optional
  end

  with_options if: -> { required_for_step?(:pets) } do
    validates :pet_count, presence: true
    validates :pet_name, presence: true, length: { minimum: 2, maximum: 20}
  end

  def required_for_step?(step)
    # All fields are required if no form step is present
    return true if form_step.nil?
  
    # All fields from previous steps are required
    ordered_keys = self.class.form_steps.keys.map(&:to_sym)
    !!(ordered_keys.index(step) <= ordered_keys.index(form_step))
  end
end

User Routes Code (Click to see)

resources :users do 
  resources :steps, only: [:show, :update], controller: 'steps_controllers/user_steps'
end

Users Controller Code (Click to see) (Mostly vanilla Rails scaffold)

class UsersController < ApplicationController
  before_action :set_user, only: %i[ show edit update destroy ]

  # GET /users or /users.json
  def index
    @users = User.all
  end

  # GET /users/1 or /users/1.json
  def show
  end

  # GET /users/new
  def new
    @user = User.new
    @user.save! validate: false                                         # Only non-vanilla-Rails code here
    redirect_to user_step_path(@user, User.form_steps.keys.first)       # Only non-vanilla-Rails code here
  end

  # GET /users/1/edit
  def edit
  end

  # POST /users or /users.json
  def create
    @user = User.new(user_params)

    respond_to do |format|
      if @user.save
        format.html { redirect_to @user, notice: "User was successfully created." }
        format.json { render :show, status: :created, location: @user }
      else
        format.html { render :new, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # PATCH/PUT /users/1 or /users/1.json
  def update
    respond_to do |format|
      if @user.update(user_params)
        format.html { redirect_to @user, notice: "User was successfully updated." }
        format.json { render :show, status: :ok, location: @user }
      else
        format.html { render :edit, status: :unprocessable_entity }
        format.json { render json: @user.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /users/1 or /users/1.json
  def destroy
    @user.destroy
    respond_to do |format|
      format.html { redirect_to users_url, notice: "User was successfully destroyed." }
      format.json { head :no_content }
    end
  end

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_user
      @user = User.find(params[:id])
    end

    # Only allow a list of trusted parameters through.
    def user_params
      params.require(:user).permit([ :first_name, :last_name, :middle_name, 
        :favorite_pizza, :favorite_ice_cream, :favorite_sandwich, :email, 
        :pet_count, :pet_name ])
    end
end

User Steps Controller Code (Click to see)

module StepsControllers
  class UserStepsController < ApplicationController
    include Wicked::Wizard

    steps *User.form_steps.keys

    def show
      @user = User.find(params[:user_id])
      render_wizard
    end

    def update
      @user = User.find(params[:user_id])
      # Use #assign_attributes since render_wizard runs a #save for us
      @user.assign_attributes user_params
      render_wizard @user
    end

    private

    def user_params
      params.require(:user).permit(User.form_steps[step]).merge(form_step: step.to_sym)
    end

    def finish_wizard_path
      user_path(@user)
    end
  end
end

Wizard Step 1 View Code (Click to see)

<%= form_with model: @user, url: wizard_path, data: { turbo: false } do |f| %>
  <% if f.object.errors.any? %>
    <div class="error_messages">
      <% f.object.errors.full_messages.each do |error| %>
        <p><%= error %></p>
      <% end %>
    </div>
  <% end %>

  <fieldset>
    <legend>User's Names</legend>

    <div>
      <%= f.label :first_name %>
      <%= f.text_field :first_name %>
    </div>

    <div>
      <%= f.label :middle_name %>
      <%= f.text_field :middle_name %>
    </div>

    <div>
      <%= f.label :last_name %>
      <%= f.text_field :last_name %>
    </div>

    <br/>

    <div>
      <%= link_to 'Nevermind', users_path %>
      <%= f.submit 'Next Step' %>
    </div>
  </fieldset>
<% end %>

Wizard Step 2 View Code (Click to see)

<%= form_with model: @user, url: wizard_path, data: { turbo: false } do |f| %>
  <% if f.object.errors.any? %>
    <div class="error_messages">
      <% f.object.errors.full_messages.each do |error| %>
        <p><%= error %></p>
      <% end %>
    </div>
  <% end %>

  <fieldset>
    <legend>User's Favorite Foods</legend>

    <div>
      <%= f.label :favorite_pizza %>
      <%= f.text_field :favorite_pizza %>
    </div>

    <div>
      <%= f.label :favorite_ice_cream %>
      <%= f.text_field :favorite_ice_cream %>
    </div>

    <div>
      <%= f.label :favorite_sandwich %>
      <%= f.text_field :favorite_sandwich %>
    </div>

    <p>
      These fields are optional so we can add a 'skip' button
    </p>

    <div>
      <%= link_to 'Previous Step', previous_wizard_path %>
      <%= f.submit 'Next Step' %>
      <%= link_to 'Skip', next_wizard_path %>
    </div>
  </fieldset>
<% end %>

Wizard Step 3 View Code (Click to see)

<%= form_with model: @user, url: wizard_path, data: { turbo: false } do |f| %>
  <% if f.object.errors.any? %>
    <div class="error_messages">
      <% f.object.errors.full_messages.each do |error| %>
        <p><%= error %></p>
      <% end %>
    </div>
  <% end %>

  <fieldset>
    <legend>User's Pet Info</legend>

    <div>
      <%= f.label :pet_count %>
      <%= f.text_field :pet_count %>
    </div>

    <div>
      <%= f.label :pet_name %>
      <%= f.text_field :pet_name %>
    </div>

    <br/>

    <div>
      <%= link_to 'Previous Step', previous_wizard_path %>
      <%= f.submit 'Complete' %>
    </div>
  </fieldset>
<% end %>