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.
# 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
resources :users do
resources :steps, only: [:show, :update], controller: 'steps_controllers/user_steps'
end
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
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
<%= 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 %>
<%= 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 %>
<%= 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 %>