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!
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).
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.
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.
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
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.
*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...
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.
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.
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:
@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.
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!
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.
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.
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.
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
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?
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?
@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
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
Great tutorial! Is there any way to combine this with your complex form episodes. Basically I'm working on a multi-step, multi-modal app and am kinda stumpped on trying to get different modals in each step of the process.
Well i started as a RoR developer now active PHP developer (but with rails 3 i definitely consider returning) i use hidden fields to accomplish this task. Since you are submitting the values going back and forward the model object is being populated with the previous values with one difference you always have to have all of the fields hidden (showing only the once that are getting populated by the step). Sorry for this late response but i hope it helps.
Anyway thanks a lot for screencasting about rails, if it wasn't for railscasts i dont think that rails would get this popularity which definitely deserves.
Coming to this 12 months later, but did want to pitch in with having tried this, I found it better to store the current step in the form, not the session, to prevent refreshes behaving oddly.
However, I see Si Wilkins posted the same 9 months ago - what's more the code snippet he linked to showed me a couple of tidyups I hadn't thought of either !
Highly recommend adding these tweaks onto the end, for anyone else following this approach, as aside from a slightly invaded model object, you do get a nice wizard approach without needed to learn a new framework library addon (not that I've found one of them for Rails3 yet)
Late to the party -- I like Ryan's approach but was troubled by the complexity of the create() method. My modified approach has a Wizard model that saves the current_step as a db field (so it's preserved across sessions) and saves an instance before starting. From there, everything is done in the upate() method, so the controller simplifies down to:
ruby
defshow
render @wizard.current_step
enddefupdate
(params[:step] == 'back') ? @wizard.step_back : @wizard.step_forward
if@wizard.update_attributes(params[:wizard])
redirect_to wizard_path, :notice => 'update succeeded'else# the reload restores the previous (valid) step as the current step
render @wizard.reload.current_step
endend
The only other substantive change is the next / prev buttons in the views, which might look like this:
are you able to do edit and update using your code? if yes then please help me to do same. I am stuck at edit and update . whenever I'm trying to edit it showing first step that is okay but after pressing the continue button form saves and not going to next step. what should i do?
Can anyone point me in the right direction getting this to work with file uploading? I've tried just sort of mashing file uploading in there, but you can't store files in a session variable (you get type error, can't dump file).
for anyone curious, my solution to this problem was to drop the deep_merge approach entirely. Instead of storing all the model information in the session_params, I just created the model in the "first step" then called update_attributes on it in the following steps, effectively "saving" it between steps. This isn't exactly the same behavior shown in this cast, but it's actually better in my case.
Hi There, thanks for the rail cast I am wondering if anyone has had experience with using ActiveModel like the #219 Rails Cast as it would be better if I could do this without the Active Record database creation, as well as possibly removing the sessions to be used in memcache instead, it seems there isn't a lot of info on this and would be very appreciative for any help someone can offer!
how is it possible to a repeat a step? I've combined this Railscast with the Railstcast dealing with nested attributes.
For example: In the first form I will create a question. In the next step I will create an answer and can choose between "Create a 2. Seconds Answer" or "Finish". If I choose to create a second or third answer this should happen in a new form until I decide to finish this record.
post published is very helpfull for me.Here you have carried out on single model.How to implement it for example like if User model has many association with Address model.I want user information in one form and Address information in Another form.Please explain me
If anyone's interested, I refactored Ryan's (always-awesome) approach to support editing/updating records, keep the code out of the controller, and fix a few issues. http://github.com/nerdcave/rails-multistep-form
hey friends i got the solution on multi-step form edit and update methods something like this bellow->
after watching this video I created two step form and it was working greatly thanks to Sir Ryan, but after while when i was went for editing and update the form it doesn't working.
It renders on the first step successfully but when i click on the continue button the form goes to update and skips the next step. That was really big issue with my app.
So after very long efforts I found solution may be it is wrong way but for me its working smoothly lets see how it works->
First of all gave the "name: some name" to all buttons n that form's each button.
then make extra view file (not partial) for last step (i had only 2 steps) so for second step as same as your partial for second step.
now then come back to your controller's update method and write code
if params[:continue_button]
render :template => '/your_controller/second_step_view'
elsif params[:save_button]
@user.update(user_params)
respond_with(@user)
else
render new_user_path #it will render u on firststep again
end
I am having trouble while uploading image. I have used carrierwave gem to upload image when the user goes to next step it gives following error if image is present
Encoding::UndefinedConversionError at /deals
"\xFF" from ASCII-8BIT to UTF-8
If there is no image the app does not gives error. any body help me tell how to avoid this error or how to avoid storing carrierwave gem in session and get it back on last step.thanks
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!
hey Jose,
can you point me to some resources as to how I can accomplish this with js/jquery. :D
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!
Great Episode. I'd suggest using with_options when having multiple validations on different steps. This would look like this http://pastie.org/994652
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).
@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.
great episode Ryan! Another alternative to this would be to use the Acts As State Machine
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.
Type-O in your final method: 'all?' Should be 'each'
Nevermind: my mistake -- your ruby-Fu is > Mine :-)
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
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.
i would love to see an episode about refactoring these actions, i always keep ending up with very long actions.. :/
Yeah... I don't know...
Store form fields in session and keep view states in model is weird.
Anyway, it works.
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)
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.
what about wizardly (http://github.com/jeffp/wizardly) and actsaswizard (http://github.com/adkron/actsaswizard) ?
Indeed a little refactorization would be great.
Thanks this is the kind of episodes that I learn a lot of how Rails works.
*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...
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.
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.
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
)
)
Hello Ryan,
Its good to use workflow + validatable for such things. Much cleaner.
http://github.com/geekq/workflow
@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 :-)
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!
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.
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.
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?
I'm getting the same error. I hope u still remember how did u overcome with this problem...
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
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.
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.
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!
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
Will be really cool the refactoring of the controller :)
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
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?
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
@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
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
Also, should validation really be done when clicking the back button?
Ryan, thanks for this screen cast!
Its possible to make multistep form with file_field element?
When i try, i have error:
can't dump File
because rails are trying to write in session!?
Any idea?
Great tutorial! Is there any way to combine this with your complex form episodes. Basically I'm working on a multi-step, multi-modal app and am kinda stumpped on trying to get different modals in each step of the process.
Well i started as a RoR developer now active PHP developer (but with rails 3 i definitely consider returning) i use hidden fields to accomplish this task. Since you are submitting the values going back and forward the model object is being populated with the previous values with one difference you always have to have all of the fields hidden (showing only the once that are getting populated by the step). Sorry for this late response but i hope it helps.
Anyway thanks a lot for screencasting about rails, if it wasn't for railscasts i dont think that rails would get this popularity which definitely deserves.
Coming to this 12 months later, but did want to pitch in with having tried this, I found it better to store the current step in the form, not the session, to prevent refreshes behaving oddly.
However, I see Si Wilkins posted the same 9 months ago - what's more the code snippet he linked to showed me a couple of tidyups I hadn't thought of either !
Highly recommend adding these tweaks onto the end, for anyone else following this approach, as aside from a slightly invaded model object, you do get a nice wizard approach without needed to learn a new framework library addon (not that I've found one of them for Rails3 yet)
Late to the party -- I like Ryan's approach but was troubled by the complexity of the create() method. My modified approach has a Wizard model that saves the current_step as a db field (so it's preserved across sessions) and saves an instance before starting. From there, everything is done in the upate() method, so the controller simplifies down to:
The only other substantive change is the next / prev buttons in the views, which might look like this:
Can you post the full code for everything? I'm also wanting to save the step to the db. Thanks!
are you able to do edit and update using your code? if yes then please help me to do same. I am stuck at edit and update . whenever I'm trying to edit it showing first step that is okay but after pressing the continue button form saves and not going to next step. what should i do?
Can anyone point me in the right direction getting this to work with file uploading? I've tried just sort of mashing file uploading in there, but you can't store files in a session variable (you get type error, can't dump file).
for anyone curious, my solution to this problem was to drop the deep_merge approach entirely. Instead of storing all the model information in the session_params, I just created the model in the "first step" then called update_attributes on it in the following steps, effectively "saving" it between steps. This isn't exactly the same behavior shown in this cast, but it's actually better in my case.
Hi There, thanks for the rail cast I am wondering if anyone has had experience with using ActiveModel like the #219 Rails Cast as it would be better if I could do this without the Active Record database creation, as well as possibly removing the sessions to be used in memcache instead, it seems there isn't a lot of info on this and would be very appreciative for any help someone can offer!
Hi,
how is it possible to a repeat a step? I've combined this Railscast with the Railstcast dealing with nested attributes.
For example: In the first form I will create a question. In the next step I will create an answer and can choose between "Create a 2. Seconds Answer" or "Finish". If I choose to create a second or third answer this should happen in a new form until I decide to finish this record.
Best Regards,
Ben
Hi
post published is very helpfull for me.Here you have carried out on single model.How to implement it for example like if User model has many association with Address model.I want user information in one form and Address information in Another form.Please explain me
Can any one suggest me for the edit action and the update action of this screencast strugling for the solution
If anyone's interested, I refactored Ryan's (always-awesome) approach to support editing/updating records, keep the code out of the controller, and fix a few issues. http://github.com/nerdcave/rails-multistep-form
Has anyone worked on an edit/update possibility for this episode?
yes i found one solution after very long battle to this type of form you can see my comment bellow how i did it.
hey friends i got the solution on multi-step form edit and update methods something like this bellow->
after watching this video I created two step form and it was working greatly thanks to Sir Ryan, but after while when i was went for editing and update the form it doesn't working.
It renders on the first step successfully but when i click on the continue button the form goes to update and skips the next step. That was really big issue with my app.
So after very long efforts I found solution may be it is wrong way but for me its working smoothly lets see how it works->
First of all gave the "name: some name" to all buttons n that form's each button.
then make extra view file (not partial) for last step (i had only 2 steps) so for second step as same as your partial for second step.
now then come back to your controller's update method and write code
that's it.
lets
can you please upload full source code I am also stuck at edit/update part. Thanks
I am having trouble while uploading image. I have used carrierwave gem to upload image when the user goes to next step it gives following error if image is present
Encoding::UndefinedConversionError at /deals
"\xFF" from ASCII-8BIT to UTF-8
If there is no image the app does not gives error. any body help me tell how to avoid this error or how to avoid storing carrierwave gem in session and get it back on last step.thanks