Rails Wizards! 🎉


Books

Books represent most of the same premise as Houses but use a cache-key-in-URL approach to better accomodate for the down-sides of routing in-session. This helps the outside view appear to be more database-like even though the partial model persists only in the cache. We take a randomized character approach for this setup.


Title Author name Year published Isbn Primary topic Fictionaility
a a 1999 111 1 1 Show Edit Destroy
JL Joao 2000 22 Scince sas Show Edit Destroy
dbf dsfg 1970 1 asdf asdf Show Edit Destroy
Rrr Trt 2232 23659852 Rr Ggg Show Edit Destroy
ff df 1987 2345 af Show Edit Destroy
test john 1980 333 fiction Show Edit Destroy
32 3232 2020 4 a dsds Show Edit Destroy
Some some 1990 3123 sdasdasd dasdasdasd Show Edit Destroy
eee eee 2002 1 1 1 Show Edit Destroy
thing thingy 2021 something else Show Edit Destroy
aads asdasdas 2000 1 dsadsa asdsa Show Edit Destroy
asdf 2f23f 1981 1231231233 f32f 23f23f Show Edit Destroy
Perica Pera 1999 123 Other None Show Edit Destroy
Poiuy Azerty 1985 Topic Yes Show Edit Destroy
Unga Bunga 2022 1234 Self-Improvement DER Show Edit Destroy
Test Sean 2006 123456 Fishing Non-Fiction Show Edit Destroy
Foo Bar 1999 12312341 Foo Bar Show Edit Destroy
sdf sdf 1980 4 dsf sdf Show Edit Destroy
aer aer 5134 0 aer aer Show Edit Destroy

New Book
Book Model Code (Click to see)

# Generated from
# rails g scaffold Book title author_name year_published:integer isbn:integer primary_topic fictionaility

class Book < ApplicationRecord
  FORM_TURBO_FRAME_ID = 'book_multi_step_form'

  enum form_steps: {
    basics: [:title, :author_name],
    organizational: [:year_published, :isbn],
    details: [:primary_topic, :fictionaility]
  }
  attr_accessor :form_step

  with_options if: -> { required_for_step?(:basics) } do
    validates :title, presence: true
    validates :author_name, presence: true
  end

  with_options if: -> { required_for_step?(:organizational) } do
    validates :year_published, presence: true, numericality: { greater_than: 1950 }
    # isbn not required
  end

  with_options if: -> { required_for_step?(:details) } do
    # primary topic not required
    validates :fictionaility, 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

Book Routes Code (Click to see)

resources :books
resources :build_book, only: [] do
  resources :step, only: [:update, :show], controller: 'steps_controllers/book_steps'
end

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

class BooksController < ApplicationController
  before_action :set_book, only: %i[ show edit update destroy ]

  # GET /books or /books.json
  def index
    @books = Book.all
  end

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

  # GET /books/new
  def new
    book_builder_key = Random.urlsafe_base64(6)                                       # Only non-vanilla-Rails code here
    Rails.cache.fetch(book_builder_key) { Hash.new }                                  # Only non-vanilla-Rails code here
    redirect_to build_book_step_path(book_builder_key, Book.form_steps.keys.first)    # Only non-vanilla-Rails code here
  end

  # GET /books/1/edit
  def edit
  end

  # POST /books or /books.json
  def create
    @book = Book.new(book_params)

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

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

  # DELETE /books/1 or /books/1.json
  def destroy
    @book.destroy
    respond_to do |format|
      format.html { redirect_to books_url, notice: "Book was successfully destroyed." }
      format.json { head :no_content }
    end
  end

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

    # Only allow a list of trusted parameters through.
    def book_params
      params.require(:book).permit(:title, :author_name, :year_published, :isbn, :primary_topic, :fictionaility)
    end
end

Book Steps Controller Code (Click to see)

module StepsControllers
  class BookStepsController < ApplicationController
    include Wicked::Wizard

    steps *Book.form_steps.keys

    def show
      book_attrs = Rails.cache.read params[:build_book_id]
      @book = Book.new book_attrs
      render_wizard
    end

    def update
      book_attrs = Rails.cache.read(params[:build_book_id]).merge book_params
      @book = Book.new book_attrs

      if @book.valid?
        Rails.cache.write params[:build_book_id], book_attrs
        redirect_to_next next_step
      else
        render_wizard nil, status: :unprocessable_entity
      end
    end

    private

    def book_params
      params.require(:book).permit(Book.form_steps[step]).merge(form_step: step.to_sym)
    end

    def finish_wizard_path
      book_attrs = Rails.cache.read(params[:build_book_id])
      @book = Book.new book_attrs
      @book.save!
      Rails.cache.delete params[:build_book_id]
      book_path(@book)
    end
  end
end

Wizard Step 1 View Code (Click to see)

<%= turbo_frame_tag dom_id(@book) do %>
  <%= form_with model: @book, 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>Book Basics</legend>

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

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

      <br/>

      <div>
        <%= link_to 'Nevermind', books_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(@book) do %>
  <%= form_with model: @book, 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>Book Organizing Details</legend>

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

      <div>
        <%= f.label :isbn %>
        <%= f.number_field :isbn %>
      </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(@book) do %>
  <%= form_with model: @book, 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>Book Details</legend>

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

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

      <br/>

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