Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

rendering first step of multistep form wizard as partial in another controller's show action

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.

like image 308
calyxofheld Avatar asked Jan 21 '18 10:01

calyxofheld


2 Answers

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/step1partial view.

like image 141
FedericoG Avatar answered Nov 17 '22 06:11

FedericoG


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

like image 2
Fabrizio Bertoglio Avatar answered Nov 17 '22 08:11

Fabrizio Bertoglio