DZone Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Snippets has posted 5883 posts at DZone. View Full User Profile

RoR Multi-step Forms

05.12.2005
| 20773 views |
  • submit to reddit
        Here's a controller code snippet for doing partial validations on Rails model objects:
  private
    def get_partial_user_from_session
      unless @session['partial_user'].nil?
        @user = @session['partial_user']
      else
        @user = User.new
      end
    end

    def save_partial_user_in_session
      unless @user.nil?
        @session['partial_user'] = @user
      end
    end
    
    # Might be a good addition to AR::Base
    def valid_for_attributes( model, attributes )
      unless model.valid?
        errors = model.errors
        our_errors = Array.new
        errors.each { |attr,error|
          if attributes.include? attr
            our_errors << [attr,error]
          end
        }
        errors.clear
        our_errors.each { |attr,error| errors.add(attr,error) }
        return false unless errors.empty?
      end
      return true
    end

To ensure the errors are cleared out properly on each request add a before_filter with the following:
    def clear_stage_errors
      unless @session['partial_user'].nil?
        @session['partial_user'].errors.clear
      end
    end

You can use 
get_partial_user_from_session
 and 
save_partial_user_in_session
 as before and after filters, respectively, so you only have to reference @user in your multi-step controller.

A clean way to arrange the view is to have each step in its own partial, and set a variable such as 
@current_stage
 in your controller, then in your view just do something like:
<%= start_form_tag({:action=> "signup"} , { :name => 'signupform' }) %>
<%= hidden_field_tag "changeCountry" %>
<%= hidden_field_tag "current_stage", "#{@current_stage}"%>

<h2>Registration Process</h2>
<%= @flash['notice'] %>

<%= render_partial "#{@current_stage}" %>
	
<%= end_form_tag %>

Your signup controller action could something similar to this:
  def signup
    case @request.method
      when :post
        @current_stage = @params['current_stage']
        if @current_stage == "stage1"
          @user.attributes = @params['user']    
            @current_stage = "stage2" if valid_for_attributes(@user,["login","attribute2"])
          end
        elsif @current_stage == "stage2"
          @user.attributes = @params['user']    
          @current_stage = "stage3" if valid_for_attributes(@user,["attribute3","attribute4"])
        elsif @current_stage == "stage3"
          @user.attributes = @params['user']
          
          if @user.save
            @session[:user] = User.authenticate(@user.login, @params['user']['password'])
            flash['notice']  = "Signup successful"
            redirect_to :action => "home"
          end
        end
      when :get
        @current_stage = "stage1"
    end
  end
    

Comments

Snippets Manager replied on Thu, 2009/07/02 - 11:31am

bmarini, I think there is a bug in your code. When I called: @order.valid_for_attributes? 'first_name', 'last_name' It always passed validation. The error was the asterisk in front of attr_names on validate_for_attributes. It was being passed an array as the first attribute and when you run map! on it, it was converting the entire array to a single string (first_namelast_name). Here's the code that works for me: module ActiveRecord module Validations module Partial def valid_for_attributes?( *attr_names ) return validate_for_attributes(attr_names) end def validate_for_attributes( attr_names ) attr_names.map! {|a| a.to_s } unless valid? our_errors = Array.new errors.each { |attr,error| if attr_names.include? attr our_errors << [attr,error] end } errors.clear our_errors.each { |attr,error| errors.add(attr,error) } return false unless errors.empty? end return true end end end end ActiveRecord::Base.class_eval do include ActiveRecord::Validations::Partial end The main difference is that if you call validate_for_attributes directly, you have to pass it an array. Chris Gunther

Snippets Manager replied on Tue, 2006/12/05 - 10:38pm

I took your partial validation idea and included it in AR Base module ActiveRecord module Validations module Partial def valid_for_attributes?( *attr_names ) return validate_for_attributes(attr_names) end def validate_for_attributes( *attr_names ) attr_names.map! {|a| a.to_s } unless valid? our_errors = Array.new errors.each { |attr,error| if attr_names.include? attr our_errors << [attr,error] end } errors.clear our_errors.each { |attr,error| errors.add(attr,error) } return false unless errors.empty? end return true end end end end ActiveRecord::Base.class_eval do include ActiveRecord::Validations::Partial end