Rails Wizards! 🎉


Houses

Houses is the model we're using for a cache-persisted multi-step form that also implements Turbo Frames to create a really nice SPA-like experience. Since we're using cache persistence we won't need to worry about partial objects in the database, the ActiveRecord/DB Record will only be created once the full wizard has been completed.

The only downside here is that it requires Rails Cache to be enabled during development, which isn't necessarily the most "Railsy" thing — your cache should ultimately be dispensible and your application should work 100% functionally without it — it should purely be a transparent performance booster for the Rails app. Manifesting application logic (even if it's just letting consumers 'borrow' a little cache space for their mid-wizard form fields) into the Rails cache is generally frowned upon. While not technically breaking MVC since we're not persisting a model via the cache, the dependency on the cache for the system to operate properly has a notable impact. Please take time to assess that impact for your specific use-case.


Address Exterior color Interior color Current family last name Rooms Square feet
testaaaaaaaaaa qrewq qwer test 2 24 Show Edit Destroy
23 harry potter road white red qwe 2 2 Show Edit Destroy
1234 Test St Red Blue Moran 4 1000 Show Edit Destroy
dafsafdsafsafsafsafsa fasfdsa fas fdasfas 3232 1 Show Edit Destroy
1233 fake street Blue Green Asdf 10 2000 Show Edit Destroy
nkkkkkkkkkkkkkkk imimi ijiji nkkkkk 9 0 Show Edit Destroy
6 janan mare r034 dfg fdg there 234 234 Show Edit Destroy
3 smith street Red Blue Smith 6 1100 Show Edit Destroy
1, New street Brown White Simpson 7 2000 Show Edit Destroy
2, New street Grey White Wilson 5 3500 Show Edit Destroy
werewwerew wer wer ewrewr erewr werwerew 3 3 Show Edit Destroy
jk jljkjkjkjkj jk mk jk 9 90 Show Edit Destroy
1116 E. 9th Street brown white Goodrich 4 3300 Show Edit Destroy
sdscdddddddddddddd Blue Yellow ddfdf 5 25 Show Edit Destroy
1234 Somewhere Road Gray Cream Greatness 2 2300 Show Edit Destroy
Wonderland yellow green toto 5 2 Show Edit Destroy
weeedsfdsfsdf dsfdsf dsf sdf sdf sdf sd sdfdsfsdf sddsdsfdsf sdfsdf 3 32 Show Edit Destroy
Tvhh 45677 bbbb Hhh H Gnk 5 0 Show Edit Destroy
412 s some st gold red dsfdsfa 2 222 Show Edit Destroy
sdfasdfsdf f asdf sdf 2 33 Show Edit Destroy
Calle Falsa 123 Red Blue Weeee 2 0 Show Edit Destroy
123 Main St Brown White Torrey 14 18900 Show Edit Destroy
test rest test blue red Smith 3 100 Show Edit Destroy
kjhkjhkhiuhkiuhiu werwerew rewr kjbhkjh 2 0 Show Edit Destroy
Thimphu Techpark Test Hello test 12 12 Show Edit Destroy
Iure quis est quas a Elit architecto tem Incidunt ut iure ea Joyner 6 0 Show Edit Destroy
tes222214 2 gray green sss 12 12 Show Edit Destroy
123 sdfsdf green blue test 5 1200 Show Edit Destroy
324 asdfs sdf asdf asdf adf 3 0 Show Edit Destroy
123 Main St Blue Yellow Jones 2 200 Show Edit Destroy
aeraeraeraer aeraer aeraer aer 12 0 Show Edit Destroy
kjlkjhkjhkljhlkjh j kl ikjkjjk 3 23 Show Edit Destroy
jjbnvbjv 65 ui iytuyiyuiy iiuyiyuiy jghj 2 1500 Show Edit Destroy
123 Fake Street Green Blue Last 5 10 Show Edit Destroy
1ssdfdsdfs sdfds sdfds 1dfsfds 12 0 Show Edit Destroy
testing 123 zue bl test 2 150 Show Edit Destroy
100 test st red blue test 2 40000 Show Edit Destroy
aaaaaaaaaaaaaaaa bbb aaa bbbb 11 0 Show Edit Destroy
Non necessitatibus q Vel odit illo dolore Labore qui corrupti Mckay 2 0 Show Edit Destroy
eqeqeqeqewq eqeqw eqeq aaaa 1321 31 Show Edit Destroy
123 main st wow blue Book 123 321 Show Edit Destroy
123 Manatee Lair Water Water Seacow 2 3000 Show Edit Destroy
84 Rainey St Yellow Beige Hill 5 2200 Show Edit Destroy
123 a road green red test 2 200 Show Edit Destroy
123 elm street red red Barker 2 1000 Show Edit Destroy
123 fake wdw Black REd Fasa 1000 3 Show Edit Destroy
212 Somehwere white White Rainbow 2 100 Show Edit Destroy
113234 e main st red black asdfasdf 23 3 Show Edit Destroy
yeperry 777 blue blue yepson 55 999 Show Edit Destroy
oloookjbkjbkjbjkbkjb pojpi kjhbkjb ioiji 4123 0 Show Edit Destroy
testgsdfgsdg red red test 11 123 Show Edit Destroy
123 Main St red blue Main 2 1 Show Edit Destroy
4124`12412412412 222 111 312312312321312 1231312 312312312 Show Edit Destroy
sa33 23, efsf sdad sad sas 12 0 Show Edit Destroy
sa33 23, efsf sdad sad sas 66 0 Show Edit Destroy
1234 33333 e 3 ww 3 3 Show Edit Destroy

New House
House Model Code (Click to see)

# Generated from
# rails g scaffold House address current_family_last_name interior_color exterior_color rooms:integer square_feet:integer

class House < ApplicationRecord
  enum form_steps: {
    address_info: [:address, :current_family_last_name],
    house_info: [:interior_color, :exterior_color],
    house_stats: [:rooms, :square_feet]
  }
  attr_accessor :form_step

  with_options if: -> { required_for_step?(:address_info) } do
    validates :address, presence: true, length: { minimum: 10, maximum: 50}
    validates :current_family_last_name, presence: true, length: { minimum: 2, maximum: 30}
  end

  with_options if: -> { required_for_step?(:house_info) } do
    validates :interior_color, presence: true
    validates :exterior_color, presence: true
  end

  with_options if: -> { required_for_step?(:house_stats) } do
    validates :rooms, presence: true, numericality: { greater_than: 1 }
    validates :square_feet, presence: true
  end

  # Checks current step to enable or disable validations appropriately
  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

House Routes Code (Click to see)

resources :houses
resources :build_house, only: [:update, :show], controller: 'steps_controllers/house_steps'

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

class HousesController < ApplicationController
  before_action :set_house, only: %i[ show edit update destroy ]

  # GET /houses or /houses.json
  def index
    @houses = House.all
  end

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

  # GET /houses/new
  def new
    Rails.cache.fetch(session.id) { Hash.new }                     # Only non-vanilla-Rails code here
    redirect_to build_house_path(House.form_steps.keys.first)      # Only non-vanilla-Rails code here
  end

  # GET /houses/1/edit
  def edit
  end

  # POST /houses or /houses.json
  def create
    @house = House.new(house_params)

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

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

  # DELETE /houses/1 or /houses/1.json
  def destroy
    @house.destroy
    respond_to do |format|
      format.html { redirect_to houses_url, notice: "House was successfully destroyed." }
      format.json { head :no_content }
    end
  end

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

    # Only allow a list of trusted parameters through.
    def house_params
      params.require(:house).permit(:address, :exterior_color, :interior_color, :current_family_last_name, :rooms, :square_feet)
    end
end

House Steps Controller Code (Click to see)

module StepsControllers
  class HouseStepsController < ApplicationController
    include Wicked::Wizard

    steps *House.form_steps.keys

    def show
      house_attrs = Rails.cache.fetch session.id
      @house = House.new house_attrs
      render_wizard
    end

    def update
      house_attrs = Rails.cache.fetch(session.id).merge house_params
      @house = House.new house_attrs

      if @house.valid?
        Rails.cache.write session.id, house_attrs
        redirect_to_next next_step
      else
        render_wizard nil, status: :unprocessable_entity
      end
    end

    private

    def house_params
      params.require(:house).permit(House.form_steps[step]).merge(form_step: step.to_sym)
    end

    def finish_wizard_path
      house_attrs = Rails.cache.fetch(session.id)
      @house = House.new house_attrs
      @house.save!
      Rails.cache.delete session.id
      house_path(@house)
    end
  end
end

Wizard Step 1 View Code (Click to see)

<%= turbo_frame_tag dom_id(@house) do %>
  <%= form_with model: @house, url: wizard_path, method: :patch 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>Address Info</legend>

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

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

      <br/>

      <div>
        <%= link_to 'Nevermind', houses_path, data: { turbo_frame: :_top } %> <%# NOTE the _top target %>
        <%= f.submit 'Next Step' %>
      </div>
    </fieldset>
  <% end %>
<% end %>

Wizard Step 2 View Code (Click to see)

<div>
  <p>
    This paragraph should never be shown since it would only be rendered by a full
    page load and this workflow should be doing frame-swaps not full page loads (drives)
  </p>
</div>

<%= turbo_frame_tag dom_id(@house) do %>
  <%= form_with model: @house, url: wizard_path, method: :patch 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>House Info</legend>

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

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

      <br/>

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

Wizard Step 3 View Code (Click to see)

<%= turbo_frame_tag dom_id(@house) do %>
  <%= form_with model: @house, url: wizard_path, method: :patch, data: { turbo_frame: :_top } do |f| %> <%# NOTE the _top target %>
    <% 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>House Numbers</legend>

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

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

      <br/>

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