I want to render the first step of a multistep form for @trade_wizard
(which has it's own controller, WizardsController
) as a partial inside ItemsController#show
, but I don't know how to build this without doubling the code from one controller into the other.
I'm rendering the first step inside the Item's show page:
<%= render "/wizards/step1" %>
@trade_wizard is handled in a special model that instantiates @trade, and then successively inherits validations from each step:
module Wizard
module Trade
STEPS = %w(step1 step2 step3).freeze
class Base
include ActiveModel::Model
attr_accessor :trade
delegate *::Trade.attribute_names.map { |attr| [attr, "#{attr}="] }.flatten, to: :trade
def initialize(trade_attributes)
@trade = ::Trade.new(trade_attributes)
end
end
class Step1 < Base
validates :trade_requester_id, :trade_recipient_id, :wanted_item_id, presence: true
validates :shares, numericality: { only_integer: true, greater_than_or_equal_to: 0,
less_than_or_equal_to: :max_shares }
def max_shares
@trade.wanted_item.shares
end
end
class Step2 < Step1
validates :collateral_item_id, presence: true
end
class Step3 < Step2
validates :agreement, presence: true
end
end
end
And then my WizardsController runs validations on each step and saves the object:
class WizardsController < ApplicationController
before_action :load_trade_wizard, except: %i(validate_step)
def validate_step
current_step = params[:current_step]
@trade_wizard = wizard_trade_for_step(current_step)
@trade_wizard.trade.attributes = trade_wizard_params
session[:trade_attributes] = @trade_wizard.trade.attributes
if @trade_wizard.valid?
next_step = wizard_trade_next_step(current_step)
create and return unless next_step
redirect_to action: next_step
else
render current_step
end
end
def create
if @trade_wizard.trade.save
session[:trade_attributes] = nil
redirect_to root_path, notice: 'Trade succesfully created!'
else
redirect_to({ action: Wizard::Trade::STEPS.first }, alert: 'There were a problem when creating the trade.')
end
end
private
def load_trade_wizard
@trade_wizard = wizard_trade_for_step(action_name)
end
def wizard_trade_next_step(step)
Wizard::Trade::STEPS[Wizard::Trade::STEPS.index(step) + 1]
end
def wizard_trade_for_step(step)
raise InvalidStep unless step.in?(Wizard::Trade::STEPS)
"Wizard::Trade::#{step.camelize}".constantize.new(session[:trade_attributes])
end
def trade_wizard_params
params.require(:trade_wizard).permit(:trade_requester_id, :trade_recipient_id, :wanted_item_id, :collateral_item_id, :shares, :agreement)
end
class InvalidStep < StandardError; end
end
In my routes I have
resource :wizard do
get :step1
get :step2
get :step3
post :validate_step
end
The error I get with this setup is First argument in form cannot contain nil or be empty
. I know why this happens - I need to define @trade_wizard
inside ItemsController#show, which I'm not doing yet, because that just results in me duplicating code from WizardsController. I don't need anyone to do my work for me, I just need a pointer for how I can build my way out of this problem.
Controllers are designed to be independent, they cannot depend on each other. This is different than views, than may be reused and composed through partials, as you are doing.
If you need to reuse behavior in controllers (which is not the same as one controller depending on another one), you may use inheritance or, following the Rails Way, concerns.
In this case, I would create a concern to setup the @trade_wizard
variable in any controller that includes the wizards/step1
partial view.