Rails: Stay on the same page after post action fails - ruby-on-rails

I'm working on an order system, I passed some information about a new order from controller (orders#confirm) to a confirmation page using instance variable (such as #detail). On the confirmation page, users are supposed to confirm the information and submit a form to create the new order (orders#create). If the post action fails, I want it to stay on the same confirmation page and preserve all the information on the page for the user:
def create
#order = Order.new(order_params)
if verify_recaptcha(model: #order) && #order.save
redirect_to items_url
else
render :confirm
end
end
The code is not working because all the variables that I passed from orders#confirm to the confirmation page are lost. I know I can recreate them, but is there any better ways to preserve those information? Thank you very much!

Within your approach, you have to rebuild the #detail object in the current action create in order to make your confirm.html.erb view to be rendered properly.
It is possible. However, I think there is a better way that you can let the user confirms the order by AJAX ( which is dead simple with Rails ) so the user can stay on the page if the confirmation failed.
In your confirm.html.erb, suppose you have a form to let user confirm, just change it to remote: true
<%= form_for #order, remote: true, format: :js do |form| %>
<%# blah blah %>
<% end %>
Modify your controller
def create
#order = Order.new(order_params)
if verify_recaptcha(model: #order) && #order.save
# Redirect when success
render js: "window.location = '#{your_desired_path}'"
else
# Display error to user.
render "ajax_create_error", status: :bad_request
end
end
Now you can create a file name ajax_create_error.js.erb to display error to the user
# app/views/your_controller_name/ajax_create_error.js.erb
alert("Cannot create order");

Related

Why controllers need instance variables to use ajax

In chapter 12.2.5 of the rails tutorial, Ajax is introduced in order to "send requests asynchronously to the server without leaving the page". The respond_to method is then used in the controller to allow browsers with enabled javascript to use Ajax or to respond with a redirect in case Ajax is disabled.
The form used to create a relationship between two users is:
<%= form_for(current_user.active_relationships.build, remote: true) do |f| %>
<div><%= hidden_field_tag :followed_id, #user.id %></div>
<%= f.submit "Follow", class: "btn btn-primary" %>
<% end %>
The corresponding action in the relationships controller is:
def create
#user = User.find(params[:followed_id])
current_user.follow(#user)
respond_to do |format|
format.html { redirect_to #user }
format.js
end
end
What gives Rails the opportunity to use Ajax is the addition of remote: true in the form_for helper method. According to the tutorial, it is necessary for the controller to use the instance variable #user instead of the local variable user for use in the form with remote: true.
Without Ajax, the resulting form would be identical except for the remote: true code absence, and in the controller the respond_to section of code would be replaced by a redirect_to; moreover, the instance variable #user can be replaced by the local variable user, so that the create action would be:
def create
user = User.find(params[:followed_id])
current_user.follow(user)
redirect_to user
end
I am wondering: why it is necessary for the controller to use the #user instance variable instead of the local variable user in order to use Ajax? I tried to use the local variable, and in effect a refresh is required to see any change. The #user variable in the form, which is used in #user.id, is defined in the show action for the users controller, because the partial for the form itself is inserted in the show.html.erb file. So, as far as I understand, there is no connection between #user in the users controller and #user in the relationships controller.
You need to declare it as instance variable (#) so it will be available in your view. Yes, using ajax you are not reloading the page, but you are sending back js instead of html. rails will respond with whatever is in create.js.erb. Notice that in this file you need access to the #user data; if you declare it as user instead of #user, it will not be accessible:
#app/views/relationships/create.js.erb
$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>");
$("#followers").html('<%= #user.followers.count %>');

How do I display the name of a deleted element in Ruby on Rails?

I have a simple Rails application where I create objects (such as posts). So far I can edit and delete them, one by one, but now I want to have a <%= notice %> echoing the name of the deleted object after confirming its deletion. Is this possible? If so, how?
This is an extremely common task in Rails, and the idiomatic solution is to forward some data about the deleted record to the subsequent GET request via the the flash array.
Your controller's destroy action should look something like this:
def destroy
#post = Post.find(params[:id])
#post.destroy
redirect_to posts_path, notice: "#{#post.name} was deleted"
end
In your index action, you'll be able to access flash[:notice] to get the string generated in the previous action.
You need to store the details you want to echo (e.g. the name) somewhere, because the object itself will be gone after a redirect. I would use the flash for that:
# in the controller
def destroy
thing = Thing.find(params[:id])
thing.destroy
redirect_to things_path, :notice => "Thing #{thing.name} was deleted"
end
# in the index view
<% if flash[:notice] %>
<div class="notice"><%= flash[:notice] %></div>
<% end %>

How does rails populate forms?

Let's say I create a scaffold:
rails g scaffold Cat name:string age:integer
and I add a presence validation on the Cat model's age attribute:
validates :age, presence: true
When I attempt to create a cat via the form, and put in the cat's name but purposely leave out the cat's age the controller bounces me back to the form but that cat's name is still present in the name field!
How is this happening?
I would have thought the
#cat = Cat.new
would replace all of the invalid cat's attributes. Maybe if it were #cat ||= Cat.new I could understand that more.
Also, how can I make this behaviour happen in a more complex rails app? I have a simple forum where topics has_many replies. I create my new replies via a form in my topic show view:
topic#show:
#reply = Reply.new
topic/show.html.erb:
<%= form_for [#toplic, #reply] do |f| %>
<%= f.text_field :name placeholder: 'Create a new name...' %><br>
<%= f.text_area :description, placeholder: 'Create a new description...', rows: 5 %><br>
<%= f.submit 'Create Discussion' %>
<% end %>
While everything works perfectly, when I purposely leave out a reply's name, though I am redirected back to the form and an error flash shows, my form is completely empty. All of the attributes have vanished? Why is this?
The key to understanding how this works is to realize that in the case of a form failure, the controller action is not rerun, but rather the template is rendered using the existing state from the action.
In a typical Rails scaffold, your create action will look like this
def create
#cat = Cat.new(cat_params) # instance variable is initialized with the form values
if #cat.save
redirect_to #cat, notice: 'Success!'
else
# in the case of form failure, we will re-render the 'new' template
# this will NOT rerun the entire 'new' action, thus the #cat variable
# will still maintain the values from the form that we gave it above
render 'new'
# note the difference if we had instead done a redirect_to; this would
# cause the CatsController#new action to be re-run which would reinitialize
# the #cat variable according to the code within the 'new' action
# redirect_to new_cat_url
end
end
For your more complex example, you'll want to follow the same procedure, making sure you just re-render the form and don't redirect to another action (which will cause the state to be lost).
# TopicsController
def show
#topic = Topic.find(params[:id])
#reply = Reply.new
end
# RepliesController
def create
#reply = Reply.new(reply_params) # init the var with the form values
if #reply.save
redirect_to #topic, notice: 'Success!'
else
# this is the key - we need to re-render the template of the previous action
# in this case, it would be the TopicsController#show template
render 'topics/show'
# Remember - if we instead do a redirect_to #topic, then we will lose the form
# values which are currently set in the #reply variable.
end
end
In short, make sure you recognize when you are redirecting to a new action versus just re-rendering a template.
One important GOTCHA to be aware of when re-rendering a template is that you must make sure that all the instance variables which exist for the controller action are available when you render the template.
For example,
# TopicsController
def show
#topic = Topic.find(params[:id])
#reply = Reply.new
#foo = Foo.new
end
# RepliesController
before_action :set_topic
def create
#reply = Reply.new(reply_params)
if #reply.save
# ...
else
# we need to remember to set up a #foo variable here otherwise it will be undefined
# when used within the 'show' template
#foo = Foo.new
render 'topics/show'
end
protected
def set_topic
#topic = Topic.find(params[:topic_id])
end
OK, so, you go go /cat/new. Rails' route for this URL runs the method CatsController#new, which renders the new.html.erb template. You put in your data, then hit submit. The action for this form is to POST to /cats, which runs the CatsController#create method. This method does this following:
#cat = Cat.new(cat_params)
It then tries to save the Cat. If it succeeds, it redirects you to the Cat's URL. If not, it re-renders the new.html.erb template. That's where the name comes from — the CatsController#update method creates its Cat from the values you put into the original form.
For a typical scaffold create action:
# POST /products
# POST /products.json
def create
#product = Product.new(params[:product])
respond_to do |format|
if #product.save
format.html { redirect_to #product, notice: 'Product was successfully created.' }
format.json { render json: #product, status: :created, location: #product }
else
format.html { render action: "new" } #will re-submit
format.json { render json: #product.errors, status: :unprocessable_entity }
end
end
end
The magic happens in the render method! which will submit the previous POST request (won't go back to the new action while a redirect will do), this way the submited values are still there. (check this SO question for more details)
That's said, this behavior relies on following the convention, however sometimes you need to give it a hand specially with some inputs (e.g selects, checkboxes, radio) might require extra setup using selected or value options

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

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?
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.
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 %>
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?
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.
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.
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 %>

AJAX with Rails 3

I'm wanting to add some AJAX functionality to my Rails app, but have no idea where to start.
Here is the method that adds an item to an order:
def add_item_to_order
if session[:order_id].nil?
#order = Order.new #Don't create an order until there is an item to be added to it.
#order.account_id = session[:user_id]
else
#order = Order.find(session[:order_id])
end
item = Item.find(params[:id])
o_item = OrderItem.new
o_item.item_id = item.id
#order.order_items << o_item
#order.total += item.sale_price
#order.save
session[:order_id] = #order.id
redirect_to order_home_path
end
This is run when the user clicks:
<%= link_to item.name, add_item_to_order_path(item.id), :class => "fixed medium green button"%>
Can anyone give me any tips on how to get started, so the the item is added to the order via AJAX?
Check on how to render javascript. In normal requests one would redirect to some action or render some view etc, for a XHR (XmlHttpRequest) you can render javascript through a server-sided js template that would be rendered. You will have to use the LegacyPrototypeHelpers provided for Rails-3 as the original helpers were only officially available for Rails-2.
A better approach(unobtrusive as Rails 3 prefers) will be to just send some data from the server. In the following example you have above I guess if you send item.id via a JSON object or some other format and then read it in the success callback of the place from where you made the XMLHttpRequest, then after getting the item.id you could create the HTML that the link_to creates and then append it to the DOM.
Great tutorial, did this myself: http://ruby.railstutorial.org/ruby-on-rails-tutorial-book Chapter 12 has some stuff on Ajax.
Important part is to set your link_to paramater data-remote to true:
<%= link_to item.name, add_item_to_order_path(item.id),
:class => "fixed medium green button" data-remote="true" method="post"%>
and in your controller you add
def add_item_to_order
# other stuff
# at the bottom:
respond_to do |format|
format.html { redirect_to order_home_path }
format.js
end
end
Then you'll need a .js.erb file to handle the format.js repsonse:
$("your_form").update("<%= escape_javascript(render('partial_page')) %>")
and a partial page file to hold the new data..

Resources