7
\$\begingroup\$

I implemented a multistep form with the method described by Ryan Bates in ep217 but I had some weird behavior when refreshing or moving between the steps Acts_as_good_style (old but still good) has a tip redirect when moving on that lead me to the change in the code that follows.

In a nutshell, it says that if you're in the create action you should not render new (as I was doing).

This solved the problem. But I had to manage the case of errors in the form, so I ended up with this code.

#profiles_controller.rb

def create
  # [...] save etc [...]

  # render
  if @profile.new_record?  
    # render 'new' # OLD
    session[:profile_valid] = @profile.errors.blank?  # NEW
    redirect_to new_profile_path # NEW
  else 
    # [...]
  end
end 


def new  
  @profile = Profile.new(session[:profile_params])  
  # [...]

  # rebuild errors (see create)
  # check false because first time is nil and no error have to be displayed
  @profile.valid? if session[:profile_valid] == false
  session[:profile_valid] = true
end

Where in the new action I reload the errors otherwise lost depending on session[:profile_valid], that works fine.

BUT this way doesn't look very good to me and I would appreciate to have your opinion, OR how do you manage your controllers in multispets forms?

What was the strange behavior? Refreshing or going back and forth through the steps sometimes you jump to the wrong page, not clear what the logic is, probably depends on the validations in the model and the params hash.

\$\endgroup\$
5
+200
\$\begingroup\$

I can't speak for that particular Railscast episode, but rails provides an idiomatic way to deal with validation errors. Taking your code as an example, I'd change it to look like this (using Rails 4.2):

app/controllers/profiles_controller.rb:

class ProfilesController < ActionController::Base
  def show
    @profile = Profile.find(...)
  end

  def new  
    @profile = Profile.new(profile_params)
  end

  def create
    @profile = Profile.new(profile_params)

    if @profile.save
      redirect_to profile_path(@profile)

    else
      render :new
    end
  end

  def edit
    @profile = Profile.find(...)
  end

  def update
    @profile = Profile.find(...)

    if @profile.update_attributes(profile_params)
      redirect_to profile_path(@profile)

    else
      render :edit
    end
  end

  private

  def profile_params
    # call params.require(:profile).permit(...) to whitelist attributes as needed
    params.require(:profile)
  end
end

app/views/profiles/new.html.haml:

%h2 New Profile
= render partial: 'form'

app/views/profiles/edit.html.haml:

%h2 Edit Profile
= render partial: 'form'

app/views/profiles/_form.html.haml:

= form_for(@profile) do |f|
  - # Display any form errors, if any
  - if @profile.errors.any?
    .errors
      %h2 Errors
      %ul
        - @profile.errors.full_messages do |message|
          %li= message

  - # Assuming profiles have first_name and last_name as attributes
  %section
    = f.label :first_name
    = f.text_field :first_name

  %section
    = f.label :last_name
    = f.text_field :first_name

  %section
    = f.submit

This is a very common pattern in rails apps that you can also see being used in the rails guides. The form_for helper is smart enough to generate the correct path to submit the form to depending on weather or not the @profile instance variable contains a record that has or has not been persisted. Persisted records (new ones or those with errors) will be posted to create. Records being updated will be set to whatever the update action/path for that controller is.

Whenever updating or saving a record fails, the controller will render the appropriate view (new or edit) and use the record that failed validation when rendering the form. From here, your view can inspect the record and display any errors if they exist (@profile.errors).

This won't fix an issue of a user navigating backwards and forwards through your form, but it should reduce the need to do so and it'll let you avoid weirdness you may have encountered while persisting form data within the session variable (which gets serialized and de-serialized into whatever your session store is during each request).


PS: I find that thinking of multi-step forms with more than one or two steps as state machines helps greatly in trying to conceptualize the different states and transitions you need to handle when you need to determine at what point in a form a particular session should be in. To that effect, I would highly recommend checking out the wicked gem which does a lot of this grunt work for you.

\$\endgroup\$
  • \$\begingroup\$ I like your answer! I'll wait a day or two before granting the bounty. Thanks for looking it over! \$\endgroup\$ – Phrancis Dec 19 '14 at 20:18
  • \$\begingroup\$ Thanks. Relatively simple question though, I'm surprised nobody has chimed in yet. \$\endgroup\$ – Damien Wilson Jan 2 '15 at 7:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.