#217
Jun 07, 2010

Multistep Forms

See how to create a multistep (wizard) form from scratch in this episode.
Download (21.8 MB, 15:14)
alternative download for iPod & Apple TV (20.9 MB, 15:14)

Resources

script/generate nifty_scaffold order shipping_name:string billing_name:string index show new
# models/order.rb
attr_writer :current_step

validates_presence_of :shipping_name, :if => lambda { |o| o.current_step == "shipping" }
validates_presence_of :billing_name, :if => lambda { |o| o.current_step == "billing" }

def current_step
  @current_step || steps.first
end

def steps
  %w[shipping billing confirmation]
end

def next_step
  self.current_step = steps[steps.index(current_step)+1]
end

def previous_step
  self.current_step = steps[steps.index(current_step)-1]
end

def first_step?
  current_step == steps.first
end

def last_step?
  current_step == steps.last
end

def all_valid?
  steps.all? do |step|
    self.current_step = step
    valid?
  end
end

# orders_controller.rb
def new
  session[:order_params] ||= {}
  @order = Order.new(session[:order_params])
  @order.current_step = session[:order_step]
end

def create
  session[:order_params].deep_merge!(params[:order]) if params[:order]
  @order = Order.new(session[:order_params])
  @order.current_step = session[:order_step]
  if @order.valid?
    if params[:back_button]
      @order.previous_step
    elsif @order.last_step?
      @order.save if @order.all_valid?
    else
      @order.next_step
    end
    session[:order_step] = @order.current_step
  end
  if @order.new_record?
    render "new"
  else
    session[:order_step] = session[:order_params] = nil
    flash[:notice] = "Order saved!"
    redirect_to @order
  end
end
<!-- orders/new.html.erb -->
<% form_for @order do |f| %>
  <%= f.error_messages %>
  <%= render "#{@order.current_step}_step", :f => f %>
  <p><%= f.submit "Continue" %></p>
  <p><%= f.submit "Back", :name => "back_button" unless @order.first_step? %></p>
<% end %>

RSS Feed for Episode Comments 52 comments

1. Ryan Jun 07, 2010 at 00:18

Once again, you hit the nail on what I'm currently doing..

Awesome thanks Ryan!


2. José Mota Jun 07, 2010 at 00:27

It's a good way of accomplishing that kind of situation. However, I'd rather suggest what you mentioned first: split the various fields into fieldsets and use Javascript; sounds much easier, the same visual process is achieved and the Rails code is kept clean.

In either way, you wanted to present a 100% Rails way of accomplishing it and you did. Nice!


3. Artur Jun 07, 2010 at 00:35

Josę: right, but when using javascript you can only do some simple js validation for a fields. For example like above - order; you cannot check actual stock, promotions etc.


4. Michael Jun 07, 2010 at 00:38

First time in 217 episodes that you are too late for me :)

Thanks for the awesome episode. Really a smart way for handeling this. Much cleaner than i did :)

Thanks!


5. Meno Jun 07, 2010 at 00:52

Great Episode. I'd suggest using with_options when having multiple validations on different steps. This would look like this http://pastie.org/994652


6. Nils Jun 07, 2010 at 01:51

First time you're screencast comes far too late. Yet i must admit that you approach is better by far. again.


7. Marc Bowes Jun 07, 2010 at 01:53

Nice for a "first attempt", but it really needs refactoring or separation of concerns. I don't like how both the model and controller are essentially providing view functionality.

I think this could be quite easily plugin/gem-ified with a Module which alters the common use case of if/else on #save. That is, an alias_method_chain on #save which is altered to return false if there are outstanding steps (where steps are defined via methods made available through said module).


8. Michael Hasenstein Jun 07, 2010 at 05:12

@Artur: Sure you can, it is called AJAX :-)

Therefore in the "real world" I'd go for the JS-based solution, no need to complicate the server side.


9. Nico Jun 07, 2010 at 05:25

Veeeery cool. :) Like this technique.


10. Dimitar Vouldjeff Jun 07, 2010 at 09:42

Very nice way of doing that kind of stuff.
Thanks!


11. Mike Jun 07, 2010 at 10:22

great episode Ryan! Another alternative to this would be to use the Acts As State Machine


12. Owain Jun 07, 2010 at 11:42

I agree with @Marc since there are a few MVC "violations" here. In particular there is a lot of controller-specific functionality imbedded in the model which would make it very difficult to re-use the model with a different controller/view e.g. taking an order via a webservice. But as a practical solution it works.

Keep it up, I look forward to my Monday coffee break.


13. Jayteesr Jun 07, 2010 at 11:50

Type-O in your final method: 'all?' Should be 'each'


14. Jayteesr Jun 07, 2010 at 11:54

Nevermind: my mistake -- your ruby-Fu is > Mine :-)


15. Mike Wyatt Jun 07, 2010 at 12:17

recently I had to do the exact same thing, only I tackled it slightly differently

instead of using :if on the validations I just check to see if a model is valid based on select attributes

the code is here if anyone is interested in the approach
http://gist.github.com/429048

and yes, most of that controller code should be pushed into the model


16. Fredrik Jun 07, 2010 at 12:56

Thanks for this! Very well planed as always, you really know your way arround rails code!


17. Aaron Jun 07, 2010 at 13:01

Hey Ryan,

I've finally caught up on Railscasts! These are awesome. I just wanted to let you know that the recent videos haven't been showing up very well using VLC on Ubuntu. I don't know what you are doing differently but it started around ep. 212.


18. doesterr Jun 07, 2010 at 15:04

i would love to see an episode about refactoring these actions, i always keep ending up with very long actions.. :/


19. Nícolas Iensen Jun 07, 2010 at 16:36

Yeah... I don't know...

Store form fields in session and keep view states in model is weird.

Anyway, it works.


20. Adam Meehan Jun 07, 2010 at 17:36

You could simplify the model code by using my grouped_validations plugin to break up the validation into step groups http://github.com/adzap/grouped_validations

You can then just define

  validation_group :shipping do
    validates_presence_of :shipping_name
  end

and in the controller

  @order.group_valid?(session[:order_step].to_sym)


21. Dom Jun 07, 2010 at 17:49

Nice epsiode.

I kind of get the whole 'mvc violation' thing, but I still like your solution. Like you said it could use some refactoring but it's really not that bad. The whole skinny controller thing is a bit ridiculous and people go a bid overboard IMHO.

I've often wondered how I would achieve this, but have never tried it because I too like the 'one form' approach. I tend to ask for as little information is required to complete the transaction, then ask for more when you need later, it appears to be less invasive to one's privacy.

I don't think the JS only solution is a good one. The wizard should be able to fall-back to Rails; when JS is not available. And doing validations in JS could mean crappy data being stored if JS was turned off.


22. Gang Jun 07, 2010 at 19:03

Wow, that's very cool, you are amazing :p, thank you.


23. Péricles Dias Jun 07, 2010 at 20:56

what about wizardly (http://github.com/jeffp/wizardly) and actsaswizard (http://github.com/adkron/actsaswizard) ?


24. Memiux Jun 07, 2010 at 20:59

Indeed a little refactorization would be great.

Thanks this is the kind of episodes that I learn a lot of how Rails works.


25. Squiddhartha Jun 07, 2010 at 21:42

*Thank you* for exploring this, even if it is literally a couple of years too late for me -- I could have used this back in 2007 and 2008! But it helps show how to avoid and/or clean up some of the egregious code I perpetrated back then, in my ignorance...


26. William FISK Jun 08, 2010 at 00:20

Would we not have problems using the session, if the user opened a new window and wanted to edit two products at the same time?

Instead of storing the variables in the session, we could equally store them in hidden fields. We could create another model object called old_product, and then store its properties in hidden fields in the form.


27. Marc Jun 08, 2010 at 00:40

When I started learning RoR this was the first project I had but I went a different route that took way longer than it should have and way to much code. This is completely simplified compared to what I have. I'll have to implement this method of the multi-step form. Thanks for doing this. Looking forward to the refactoring.


28. Matthew Todd Jun 08, 2010 at 01:18

Re: using hidden form fields as an alternative to the session:

To avoid duplicating the list of fields, I've been playing with passing *one* hidden field, compressed_order_params, containing a gzipped yaml dump of any previous parameters.

So far, I'm liking it, but these are early days. :-)

ActiveSupport provides good help. Compressing the parameters looks something like the following, with decompressing pretty much the mirror image:

ActiveSupport::Base64.encode64(
  ActiveSupport::Gzip.compress(
    hash.to_yaml
  )
)


29. edbond Jun 08, 2010 at 07:27

Hello Ryan,

Its good to use workflow + validatable for such things. Much cleaner.

http://github.com/geekq/workflow


30. dkjess Jun 08, 2010 at 08:31

@RaynB this is a nice way of doing a multistep form in rails, but your method is missing unique urls for each step. The urls are needed in order to get funnel tracking working correctly.

I know that this is not Rails specific, but would be required for a real world app.

Anyway, keep the Railscasts coming :-)


31. Robert Sanchez Jun 08, 2010 at 08:45

Ryan,

you have NO idea how long I've been waiting for an explanation for how to do this. I don't think I would've ever figured it out on my own. Thank you so much!


32. Larry Kluger Jun 08, 2010 at 09:54

Hi Ryan,

Great episode. For a production app, I suggest more investigation of storing the partially completed form in the model.

The issue is that "abandonment" is common with multi-step forms. If the partial information is stored, analysis can be done to discover when and how often the form is being abandoned.

Re @dkjess and funnel analysis--you can usually call your analysis system explicitly via javascript and tell it which funnel step the page is currently showing.


33. Ali Rizvi Jun 08, 2010 at 12:01

Do you really need self in self.current_step? Would this not work if you simply assign to current_step?

Sorry if I am missing something very basic.


34. Robert Sanchez Jun 08, 2010 at 16:37

I'm getting an error when using deep_merge! it is assuming that the sessions hash is nil for some reason. I'm getting this error:

You have a nil object when you didn't expect it!
The error occurred while evaluating nil.deep_merge!

The code is like this:

  def new
    session[:user_params] ||= {}
    @user = User.new
  end
 
  def create
    session[:user_params].deep_merge!(params[:user]) if params[:user]
    logout_keeping_session!
    @user = User.new(session[:user_params])
    @user.current_step = session[:user_step]
    if params[:back_pressed]
      @user.previous_step
    else
    @user.next_step
  end
    session[:user_step] = @user.current_step
    render "new"
end

Can anyone help?


35. Pran Jun 08, 2010 at 20:33

Great! That was a complete tutorial from A to Z. Good job!


36. GGC Jun 08, 2010 at 21:01

This guy is crazy, thanks re-again Ryan.


37. runmen Jun 11, 2010 at 11:10

session? very bad way, one textarea field that user can put a lot of text and cookie session broken.

hidden fields is more safe way (but it not work like and session if upload need)

(params[:order]||{}).each_pair do |k,v|
hidden_field_tag "order[#{k}]", v
end

at bottom or top of form, I'm not remeber params parsing order


38. Lar Van Der Jagt Jun 12, 2010 at 15:32

I generally do something similar, but prefer to wrap all the workflow logic into separate module/classes that just check the state of one or more model objects.

For example you might have an OrderWorkflow class which has a #steps method that returns an array of FirstStep/SecondStep etc. objects. Each step object implements a complete? method & the OrderWorkflow#current_step simply returns the first object that is not complete.

One advantage of that is that you can take advantage of rail's partial rendering on objects to provide different views for each step.


39. Amiruddin Nagri Jun 14, 2010 at 10:00

Ryan,

I was going throught this post
http://blog.envylabs.com/2009/08/the-rails-state-machine/

ruby has state machine built in the models,

Any particular reason you didn't use it, also if you can cover it in later screencasts.


40. Evan Jun 15, 2010 at 11:29

Thank you! Thank you! Thank you! Exactly what I needed when I needed it!

+1 vote for a refactored controller, if you want to bang out a quickie. ;) Also, I think it might be a good idea to show storing the params in the DB and using the update action rather than sessions when you do the refactored controller.

Thanks again! You're the best!


41. cedric Jun 15, 2010 at 13:27

Hi Ryan , many thanks for this.

I get it for simple model. But I can not figure out how to do with HABTM model.

thanks again


42. Wilker Jun 17, 2010 at 07:22

Will be really cool the refactoring of the controller :)


43. Vane Jun 18, 2010 at 20:05

Thank you so much, it was just what I was looking for!
Although I have so problems that I had fix:
- didn't recognized <% title "New Order" %> so I had to run script/generate nifty_layout
- I got this error:
Missing template shipping_step.erb in view path .../app/views:
so I have to put <%= render :partial => "#{@order.current_step}_step", :locals => { :f => f } %> in new.html.erb
- render "new" didn't work for me either, changed to ender :action => 'new'
-for first_step? and last_step? : self.current_step, didn't work with only current_step


44. pduersteler Jun 21, 2010 at 23:02

I experience some strange behaviours when playing around with this multistep form. Sometimes, when pressing back to change a step, it jumps two steps forward when pressing next afterwards. Somebody else who sees this in his form?


45. elvankent halı yıkama Jul 22, 2010 at 00:17

Thanks for sharing this information


48. Asus m5000np Series Laptop Battery Jul 30, 2010 at 23:19

Very nice way of doing that kind of stuff.
Thanks!


48. Christian Jul 31, 2010 at 01:08

Hi, i'm trying to lern rails. In this episode there where some lines of code which I didn't understand. Can anyone give me link to a manual or tutorial where I can read about following lines of code?

1:
<%= render :partial => "#{@order.current_step}_step" %>

What does #{} mean? Is it ruby syntax?

2:
%w[shipping billing confirmation]
I'd like to know why there is a % sign and why there are no "," between these entries.

wouldn't it be something like:
steps = [shipping, billing, confirmation] ??

I'm really at the beginning and sometimes having problems to figure out if anything new is ruby, rack, rails and so on.

So links to specific manuals would be nice.

Thanks for these episodes!
Much love from Germany


48. optical fiber Cable Aug 01, 2010 at 23:59

I am very appreciate the issue that it can benefit to the low income guys, thanking for your sharing, it is wonderful.


49. solar collector solar water heater Aug 02, 2010 at 00:02

It's one of the most important facto to fix this time.


50. Na2Th Aug 02, 2010 at 19:39

@Christian
the #{} is a ruby way to return the content of the code into a string
so
" 1+1=#{1+1} " retorna " 1+1=2 "

the %w[shipping billing confirmation] is another ruby way to define an array of strings. Is the same of ["shipping" , "billing" , "confirmation"]
Hope it helped


53. canli mac izle Aug 05, 2010 at 11:29

great!! thanks


54. cheap coogi Aug 09, 2010 at 10:19

Here we have popular Polo T-shirt.


55. Si Wilkins Aug 10, 2010 at 08:38

I like this approach, but one problem is that it breaks the browser forward/back/reload buttons. So for instance, clicking continue will take you forward one step, and then clicking 'reload' will then mistakenly take you forward another step. This can be solved by storing the current step in the view rather than the session - I've shown the changes here: http://pastie.org/1084054


55. Si Wilkins Aug 10, 2010 at 08:40

Also, should validation really be done when clicking the back button?


56. UGG Boots on sale Aug 10, 2010 at 18:39

Gooooooooooooooooooood luck ~~!!


57. crystal jewelry Aug 11, 2010 at 00:26

I like this ,thanks!


58. free directory list Aug 11, 2010 at 22:43

Ryan, could you show the view that is calling this template?


59. True Religion jeans Aug 14, 2010 at 03:02

Great article written on my life, help, http://www.cmonc.com Thank you for your article, hope to become friends.


60. oppo Aug 15, 2010 at 08:38

This was really useful. Thanks Ryan!


61. Rip Blu-ray for Mac Aug 18, 2010 at 01:28

Thanks,it's so good.
suport!


62. AVI to iPad Aug 19, 2010 at 00:48

my style,like here.


63. selma Aug 19, 2010 at 13:32

thanks


64. wholesale new era hats Aug 20, 2010 at 20:01

I recently came across your blog and have been reading along.
I thought I would leave my first comment. I don’t know what to say except that I have enjoyed reading.Nice blog,I will keep visiting this blog very often.


65. converse all star Aug 20, 2010 at 20:52

love converse all star,love yourself.High quality low price.It's fit for you.


66. Sabine Aug 23, 2010 at 02:53

thx for the blog


67. tile floor vacuum Aug 24, 2010 at 10:27

I experience some strange behaviours when playing around with this multistep form. Sometimes, when pressing back to change a step, it jumps two steps forward when pressing next afterwards. Somebody else who sees this in his form?


68. cheap clothes Aug 24, 2010 at 19:12

David Heinemeier Hansson..thanks url=http://www.icheapwholesale.com]cheap wholesale clothes[/url],,


69. Game Mechanics Aug 26, 2010 at 00:12

Thanks for the screencast Ryan!


70. louis vuitton shoes Aug 26, 2010 at 23:21

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it. My readers have about the same interets


71. Christmas tree Aug 27, 2010 at 23:26

good


72. rap Aug 29, 2010 at 08:51

Thanks for the screencast Ryan!


73. eztoo Aug 29, 2010 at 18:25

Try our products. There will be surprises.
http://www.eztoosoft.com


74. computer headphones Aug 30, 2010 at 01:03

You have NO idea how long I've been waiting for an explanation for how to do this. I don't think I would've ever figured it out on my own. Thank you so much!


75. China Ceramic balls Aug 30, 2010 at 02:38

Wire drawing wheel | Ceramic coating bearings


76. herve leger dress Aug 30, 2010 at 19:54

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it.


77. herve leger dress Aug 30, 2010 at 20:03

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it.


78. snow boots Aug 30, 2010 at 20:14

For example like above - order; you cannot check actual stock, promotions etc.


79. blu ray ripper Sep 01, 2010 at 23:36

This is really a nice guide for Newbies like me. Thank you.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source