Lets say I am creating a new Foo using a form and a standard Rails restful controller, which looks something like this:
class FoosController < ApplicationController
...
def index
@foos = Foo.all
end
def new
@foo = Foo.new
end
def create
@foo = Foo.create(params[:foo])
if @foo.save
redirect_to foos_path, :notice => 'Created a foo.'
else
render 'new'
end
end
...
end
So, if I use the standard restful controller (as above), then when I'm creating the Foo I am at example.com/foos/new
, and if I submit the form and it saves correctly I'm at example.com/foos
showing the index action. However, if the form is not filled correctly the form is rendered again and error messages are shown. This is all plain vanilla.
However, if errors are shown, the form page will be rendered but the URL will be example.com/foos
, because the CREATE action posts to that url. However, one would expect to find Foos#index at example.com/foos
, not the form they just submitted now with error messages added.
This seems to be Rails standard behavior, but it doesn't make a lot of sense to me. Obviously I could redirect back to new instead of rendering new from the create action, but the problem with that is the error messages etc. would be lost along with the partially complete Foos in memory.
Is there a clean solution for this problem, a way to send people back to example.com/foos/new
when there are errors in the new Foo form they submitted?
Thanks!
You could hook into rails routing by adding this in an initializer:
https://gist.github.com/903411
Then just put the regular resources in your routes.rb:
resources :users
It should create the routes and behaviour you are looking for.
To answer your comment on another answer:
I'm wondering if there's a way, without rewriting the controller at all, to tell rails that you want the URL to match the rendered template, rather than the controller action that called it.
I don't think so; URLs are tied directly to routing, which is tied into a controller and action pair--the rendering layer doesn't touch it at all.
To answer your original question, here's information from another similar question I answered.
As you've found, by default when you specify resources :things
, the POST path for creating a new thing is at /things
. Here's the output for rake routes
:
things GET /things(.:format) {:action=>"index", :controller=>"things"}
POST /things(.:format) {:action=>"create", :controller=>"things"}
new_thing GET /things/new(.:format) {:action=>"new", :controller=>"things"}
edit_thing GET /things/:id/edit(.:format) {:action=>"edit", :controller=>"things"}
thing GET /things/:id(.:format) {:action=>"show", :controller=>"things"}
PUT /things/:id(.:format) {:action=>"update", :controller=>"things"}
DELETE /things/:id(.:format) {:action=>"destroy", :controller=>"things"}
It sounds like you want something more like this:
create_things POST /things/new(.:format) {:action=>"create", :controller=>"things"}
things GET /things(.:format) {:action=>"index", :controller=>"things"}
new_thing GET /things/new(.:format) {:action=>"new", :controller=>"things"}
edit_thing GET /things/:id/edit(.:format) {:action=>"edit", :controller=>"things"}
thing GET /things/:id(.:format) {:action=>"show", :controller=>"things"}
PUT /things/:id(.:format) {:action=>"update", :controller=>"things"}
DELETE /things/:id(.:format) {:action=>"destroy", :controller=>"things"}
Although not recommended, you can get this result with the following route:
resources :things, :except => [ :create ] do
post "create" => "things#create", :as => :create, :path => 'new', :on => :collection
end
You would also need to modify your forms to make them POST to the correct path.
You can set up the routing manually, if you're that concerned about what URL is going to show. For what you want, you can have a GET
to /foos/new
render your form, and a POST
to the same URL do the creation:
map.with_options :controller => :foos do |foo|
foo.new_foo '/foos/new', :conditions => {:method => :get}, :action => :new
foo.create_foo '/foos/new', :conditions => {:method => :post}, :action => :create
foo.foos '/foos', :conditions => {:method => :get}, :action => :index
end
This should work without requiring any changes to your controller (yay!) - all three actions from your example are taken care of. The few disclaimers:
- This is based on my routing for a 2.3.8 app - some syntax (semantics?) changes are probably required to get it into Rails 3 routing style.
- My attempts to mix this style of routing with
map.resources
have failed horribly - unless you're more familiar with this than me, or Rails 3 routing is better (both easily possible), you'll have to do this for every route to the controller.
- And finally, don't forget to add
/:id
, (.:format)
, etc. to the routes that need them (none in this example, but see #2).
Hope this helps!
Edit: One last thing - you'll need to hard-code the URL in your form_for
helper on /foos/new.html.erb
. Just add :url => create_foo_path
, so Rails doesn't try to post to /foos
, which it will by default (there might be a way to change the creation URL in the model, but I don't know of it, if there is one).
You could use Rack::Flash to store the parameters you wanted in the user's session and then redirect to your form url.
def create
@foo = Foo.new(params[:foo])
if @foo.save
redirect_to foos_path, :notice => 'Created a foo.'
else
flash[:foo] = params[:foo]
flash[:errors] = @foo.errors
redirect_to new_foo_path #sorry - can't remember the Rails convention for this route
end
end
def new
# in your view, output the contents of flash[:foo]
@foo = Foo.new(flash[:foo])
end