Rails Wizards! 🎉


Cars

The Cars wizard implements the same id-in-URL routing as the Users wizard but adds on the Turbo Frames layer to provide a nicer, snappier UI without page-level or Turbo-Drive-level reloads on each subsequent frame


Transmission type Seat count Ride height Preferred engine type Exterior color Interior material
Manual 4 34 Sw Red Leather Show Edit Destroy
Show Edit Destroy
test 1 5 3 Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
din mor 123 4 ede Black Black Show Edit Destroy
Show Edit Destroy
12121 2112 333 1212 222 111 Show Edit Destroy
Show Edit Destroy
automatic 20 120 gasoline black black Show Edit Destroy
automatic 12 Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
asdf 1 124 123 asdf asdf Show Edit Destroy
Manual 5 31023 New sdf sdf Show Edit Destroy
8877 76 87 ytui hhh uyt Show Edit Destroy
Show Edit Destroy
qqq 1 Show Edit Destroy
dddd 4 Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Dd 3 Show Edit Destroy
Show Edit Destroy
hg 6 78 65 hg gh Show Edit Destroy
Show Edit Destroy
auto 5 Show Edit Destroy
3 237 234 234 34 3434 Show Edit Destroy
manual 8 6 running pink velvet Show Edit Destroy
Show Edit Destroy
zera 1 Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
1test 2 234 234 234 234 Show Edit Destroy
auto 5 Show Edit Destroy
Show Edit Destroy
auto 4 Show Edit Destroy
Show Edit Destroy
auto 3 3 shrug Show Edit Destroy
Show Edit Destroy
ddd 1 4 ddd ddd ddd Show Edit Destroy
Automatic 5 10 Electric Black Leather Show Edit Destroy
1 2 22 2 www ww Show Edit Destroy
Automatic and manual 4 124 Red ones Show Edit Destroy
sdf 1 Show Edit Destroy
Automatic 6 20 V8 Black Leather Show Edit Destroy
Show Edit Destroy
Show Edit Destroy
Show Edit Destroy

New Car
Car Model Code (Click to see)

# Generated from
# rails g scaffold Car transmission_type seat_count:integer ride_height:integer preferred_engine_type exterior_color interior_material

class Car < ApplicationRecord
  enum form_steps: {
    functional_parts: [:transmission_type, :seat_count],
    mechanical: [:ride_height, :preferred_engine_type],
    user_choices: [:exterior_color, :interior_material]
  }
  attr_accessor :form_step

  with_options if: -> { required_for_step?(:functional_parts) } do
    validates :transmission_type, presence: true
    validates :seat_count, presence: true
  end

  with_options if: -> { required_for_step?(:mechanical) } do
    validates :ride_height, presence: true, numericality: { greater_than: 2 }
    validates :preferred_engine_type, presence: true
  end

  with_options if: -> { required_for_step?(:user_choices) } do
    # User choices optional
  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

Car Routes Code (Click to see)

resources :cars do
  resources :steps, only: [:show, :update], controller: 'steps_controllers/car_steps'
end

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

class CarsController < ApplicationController
  before_action :set_car, only: %i[ show edit update destroy ]

  # GET /cars or /cars.json
  def index
    @cars = Car.all
  end

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

  # GET /cars/new
  def new
    @car = Car.new
    @car.save! validate: false                                      # Only non-vanilla-Rails code here
    redirect_to car_step_path(@car, Car.form_steps.keys.first)      # Only non-vanilla-Rails code here
  end

  # GET /cars/1/edit
  def edit
  end

  # POST /cars or /cars.json
  def create
    @car = Car.new(car_params)

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

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

  # DELETE /cars/1 or /cars/1.json
  def destroy
    @car.destroy
    respond_to do |format|
      format.html { redirect_to cars_url, notice: "Car was successfully destroyed." }
      format.json { head :no_content }
    end
  end

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

    # Only allow a list of trusted parameters through.
    def car_params
      params.require(:car).permit(:transmission_type, :seat_count, :ride_height, :preferred_engine_type, :exterior_color, :interior_material)
    end
end

Car Steps Controller Code (Click to see)

module StepsControllers
  class CarStepsController < ApplicationController
    include Wicked::Wizard

    steps *Car.form_steps.keys

    def show
      @car = Car.find(params[:car_id])
      render_wizard
    end

    def update
      @car = Car.find(params[:car_id])
      # Use #assign_attributes since render_wizard runs a #save for us
      @car.assign_attributes car_params
      if @car.valid?
        render_wizard @car
      else
        render_wizard @car, status: :unprocessable_entity
      end
    end

    private

    def car_params
      params.require(:car).permit(Car.form_steps[step]).merge(form_step: step.to_sym)
    end

    def finish_wizard_path
      car_path(@car)
    end
  end
end

Wizard Step 1 View Code (Click to see)

<%= turbo_frame_tag dom_id(@car) do %>
  <%= form_with model: @car, url: wizard_path 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>Functional Stuff</legend>

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

      <div>
        <%= f.label :seat_count %>
        <%= f.number_field :seat_count %>
      </div>

      <br/>

      <div>
        <%= link_to 'Nevermind', cars_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)

<%= turbo_frame_tag dom_id(@car) do %>
  <%= form_with model: @car, url: wizard_path 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>Mechanical Things</legend>

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

      <div>
        <%= f.label :ride_height %>
        <%= f.number_field :ride_height %>
      </div>

      <br/>

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

Wizard Step 3 View Code (Click to see)

<%= turbo_frame_tag dom_id(@car) do %>
  <%= form_with model: @car, url: wizard_path, 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>User Choices</legend>

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

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

      <br/>

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