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.
as told from elc I would use ajax to hide and show the steps, combined with a nested form.
You create a Wizard
Model which has many steps and accepts steps
as nested attributes
. You can read more about nested forms
in the rails guide
class Wizard < ActiveRecord:Base
has_many :steps
accepts_nested_attributes_for :steps
end
The Step
Model belongs to Wizard
class Step < ActiveRecord:Base
belongs_to :wizard
end
this is your form
<%= form_for @wizard, class: 'hidden' do |f| %>
Addresses:
<ul>
<%= f.fields_for :steps do |step| %>
// include your fields
<% end %>
</ul>
<% end %>
this form performs a post
request to /wizards
, to add some ajax logic that will allow to hide some of those steps
forms you create a file in app/views/wizards
called create.js.erb
and write there your js
logic which can include any variable used in your controller as it is an erb
file.
It depends from you how you want to write this, but you can include this logic in the wizards#create
action
in some cases you may want to perform js to show the next form, other cases you want to save that object and render a new view. The concept is that http is stateless, so for every request you will recreate the @wizard instance, but the field filled from the form when it is hidden will still be re-submitted as strong params
# app/controllers/wizards_controller.rb
# ......
def create
@wizard = Wizard.new(params[:wizard])
respond_to do |format|
// you can set conditions and perform different AJAX responses based on the request you received.
format.js
format.html { render action: "new" }
end
end
I would have written more but I need to go
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With