In Rails when a resource create action fails and calls render :new, why must the URL change to the resource's index url?
Asked Answered
Q

6

62

I have a resource called Books. It's listed as a resource properly in my routes file.

I have a new action, which gives the new view the standard:

@book = Book.new

On the model, there are some attributes which are validated by presence, so if a save action fails, errors will be generated.

In my controller:

@book = Book.create
...  # some logic
if @book.save
  redirect_to(@book)
else
  render :new
end

This is pretty standard; and the rationale for using render:new is so that the object is passed back to the view and errors can be reported, form entries re-filled, etc.

This works, except every time I'm sent back to the form (via render :new), my errors show up, but my URL is the INDEX URL, which is

/books

Rather than

/books/new

Which is where I started out in the first place. I have seen several others posts about this problem, but no answers. At a minimum, one would assume it would land you at /books/create, which I also have a view file for (identical to new in this case).

I can do this:

# if the book isn't saved then
flash[:error] = "Errors!"
redirect_to new_book_path

But then the @book data is lost, along with the error messages, which is the entire point of having the form and the actions, etc.

Why is render :new landing me at /books, my index action, when normally that URL calls the INDEX method, which lists all the books?

Quadrinomial answered 23/1, 2013 at 21:52 Comment(4)
it doesn't land you at /books/new since you are creating resource by posting to /books/ When it fails it is just rendering the new action, not redirecting you to the new actionMota
Try redirect_to new_book_path(book: params[:book])Anaclinal
@Mota is right (he should make it an answer so I can upvote it :)). Create actions in "Rails view" of REST do not have a visualization, hence the normal redirect. What is wrong with the wrong url here?Backtrack
@Anaclinal close, but no cigar... this gives me a URL with /books/new/book%skldjalkdja (garbled stuff relating to the book ID itself, I believe). How about just /book/new?Quadrinomial
A
30

It actually is sending you to the create path. It's in the create action, the path for which is /books, using HTTP method POST. This looks the same as the index path /books, but the index path is using HTTP method GET. The rails routing code takes the method into account when determining which action to call. After validation fails, you're still in the create action, but you're rendering the new view. It's a bit confusing, but a line like render :new doesn't actually invoke the new action at all; it's still running the create action and it tells Rails to render the new view.

Alejandraalejandrina answered 23/1, 2013 at 23:20 Comment(3)
Is there not a way to render :new, so that the errors are passed along to the form, while landing at /books/new in the URL? It seems as though that would look better and make more sense, given that /books/new is where I started out, and where the user expects to be.Quadrinomial
To do that, you would have to redirect (see @MrYoshiji's comment above). The browser does a request to create_book_path when you submit the form. Unless you subsequently redirect to another page, the browser still thinks it's at the create URL. There's some work involved to make it really RESTful, though; if you simply redirect to new_book_path using GET, you'll have to encode the form parameters in the URL (not always ideal, and sometimes not possible with more complex/larger form data). The RESTful way would be to add a POST route for /books/new to populate the model/form data.Alejandraalejandrina
@Doon's suggestion to use JavaScript to update the displayed URL in the browser is a more efficient solution. I don't know offhand how to change the displayed URL. It's possible browsers won't honor it.Alejandraalejandrina
P
6

I just started with the Rails-Tutorial and had the same problem. The solution is just simple: If you want the same URL after submitting a form (with errors), just combine the new and create action in one action.

Here is the part of my code, which makes this possible (hope it helps someone^^)

routes.rb (Adding the post-route for new-action):

...
    resources :books
    post "books/new"
...

Controller:

...
def create
    @book = Book.new(book_params)

    if @book.save
        # save was successful
        print "Book saved!"

        else
        # If we have errors render the form again   
        render 'new'
    end
end

def new 
    if book_params
        # If data submitted already by the form we call the create method
        create
        return
    end

    @book = Book.new

    render 'new' # call it explicit
end

private

def book_params
    if params[:book].nil?  || params[:book].empty?
        return false
    else
        return params.require(:book).permit(:title, :isbn, :price)
    end
end

new.html.erb:

<%= form_for @book, :url => {:action => :new} do |f| %>
  <%= f.label :title %>
  <%= f.text_field :title %>

  <%= f.label :isbn %>
  <%= f.text_field :isbn %>

  <%= f.label :price %>
  <%= f.password_field :price %>

  <%= f.submit "Save book" %>
<% end %>
Pat answered 2/1, 2014 at 11:27 Comment(2)
Very nice solution, thanks for sharing, I have a problem though, I use the same "_form" template for both new and update actions, so I'm trying with: - action = params[:action] == "new" ? :new : @book and then: = form_for @book, :url => {:action => action} but it doesn't seem to generate the path right.Perigynous
ok, I figured that I had to use url_for @book to generate the URL, thanksPerigynous
M
2

It doesn't land you at /books/new since you are creating resource by posting to /books/. When your create fails it is just rendering the new action, not redirecting you to the new action. As @MrYoshiji says above you can try redirecting it to the new action, but this is really inefficient as you would be creating another HTTP request and round trip to the server, only to change the url. At that point if it matters you could probably use javascript change it.

Mota answered 24/1, 2013 at 3:43 Comment(0)
S
2

Just had the very same question, so maybe this might help somebody someday. You basically have to make 3 adjustments in order for this thing to work, although my solution is still not ideal.

1) In the create action:

if @book.save
  redirect_to(@book)
else
  flash[:book] = @book
  redirect_to new_book_path
end

2) In the new action:

@book = flash[:book] ? Book.new(flash[:book]): Book.new

3) Wherever you parse the flash hash, be sure to filter out flash[:book].

--> correct URL is displayed, Form data is preserved. Still, I somehow don't like putting the user object into the flash hash, I don't think that's it's purpose. Does anyboy know a better place to put it in?

Scissile answered 28/11, 2013 at 15:32 Comment(3)
I'm getting a CookieOverflow with this method as it's bigger than the 4k allowed for cookiesOcker
How would you pass the error messages with this though?Geyer
@Geyer Fortunately, flash is a hash, so you can add additional items like flash[:error] or flash[:alert] after you've set flash[:book]. The Action Controller guide has more detail on flash.Inapt
C
1

It can be fixed by using same url but different methods for new and create action.

In the routes file following code can be used.

resources :books do
  get :common_path_string, on: :collection, action: :new
  post :common_path_string, on: :collection, action: :create
end

Now you new page will render at url

books/common_path_string

In case any errors comes after validation, still the url will be same.

Also in the form instead using

books_path

use

url: common_path_string_books_path, method: :post

Choose common_path_string of your liking.

Congener answered 22/9, 2016 at 13:21 Comment(0)
K
0

If client side validation fails you can still have all the field inputs when the new view is rendered. Along with server side errors output to the client. On re-submission it will still run create action.

In books_controller.rb

def new
  @book = current_user.books.build
end

def create
  # you will need to have book_params in private
  @book = current_user.books.build(book_params)
  
  if @book.save
    redirect_to edit_book_path(@book), notice: "Book has been added successfully"
    # render edit but you can redirect to dashboard path or root path
  else
    redirect_to request.referrer, flash: { error: @book.errors.full_messages.join(", ") }
  end
end

In new.html.erb

<%= form_for @book, html: {class: 'some-class'} do |f| %>
  ...
  # Book fields
  # Can also customize client side validation by adding novalidate:true, 
  # and make your own JS validations with error outputs for each field. 
  # In the form or use browser default validation.
<% end %>
Kesler answered 4/11, 2022 at 8:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.