What is the purpose of the new controller action in Rails? - ruby-on-rails

A scaffold generates the new action like this:
def new
#product = Product.new
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #product }
end
end
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.xml { render :xml => #product, :status => :created, :location => #product }
else
format.html { render :action => "new" }
format.xml { render :xml => #product.errors, :status => :unprocessable_entity }
end
end
end
and the view renders a partial, named form. Since the new form renders with the action set to create a new product, what is the purpose of #product? I see that the create action instantiates a new object as well. Is it only used so that you can tie the form to an object, so that everything goes from action to action correctly?

You can think of #product in the new action as being an unsaved object that simply fills out the form fields that are rendered in the view. This makes new.html.erb pretty much the same as edit.html.erb and allows them to share a single partial, _form.html.erb.
When this partial is used in the new action, the fields are populated by a fresh, empty, and unsaved #product object. This is the Product.new that appears in the new action. When the partial is used in the edit action, you've got a #product object that presumably has values for all its attributes. Now, suppose you didn't use #product in the new action. The form used in new.html.erb would need to be different than the form used in edit. Good luck maintaining them if you ever add a new field to the model.
Another advantage of this approach is that you can pre-populate attributes of the new #product before they're rendered in the view. Suppose you want to use the name "new product" as the default name for each product. You can do this in the new action this way:
def new
#product = Product.new(:name => 'new product')
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #product }
end
end

One main purpose is so that you can use the same form for new and edit.
The controller passes the #product object (new or existing) and Rails sees whether it's a new record or an existing record. It makes certain decisions based on that such as to pull the record values into the input fields (existing) and which controller action to send the form on submission.

If you have a form_for, the new action is used to properly initialize the #product in form_for #product, which expects an ActiveRecord model. If I remember correctly, the Product's scope (for any controller action) ends with the request, so the create action has no knowledge of the new action, requiring another Product to be initialized.
The form_for method uses the #product variable to correctly assign the form to the resource controller to find the correct URL, id (in the case of an update), and more. You can read about it in section 2.3 of http://guides.rubyonrails.org/form_helpers.html
If you're that concerned about memory usage, you don't have to initialize #product, but then you would have to manually create your own form without using the nice resource-based form_for.

Related

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

Instantiate instance variable in helper method from controller

I have a helper which instantiates a model and renders a form. This form should be available to any view in the application
# support_form_helper
def support_form
#support_stats = SupportForm::Stat.find(get_stats_id)
#enquiry = SupportForm::Enquiry.new(stats_id: #support_stats.id)
render partial: 'support_form/enquiries/form'
end
And its rendered in the view:
# some_view.html.erb
<%= support_form %>
This is fine until I want to submit the form and validate it in the controller.
# enquiries_controller.rb
def create
#enquiry = SupportForm::Enquiry.new(params[:support_form_enquiry])
topic = #enquiry.topic
#stat = SupportForm::Stat.find(#enquiry.stats_id)
#stat.stats[topic] = #stat.stats[topic].to_i.next
respond_to do |format|
if #enquiry.valid? && #stat.save
format.html { redirect_to(root_path) }
else
format.html { redirect_to(:back) }
end
end
end
This is where I can't render the previous view with the errors attached to the invalid object. The helper gets invoked again and initializes a new #enquiries object, without the errors obviously.
How can I render the form in many views across an application and still return to the view with the object together with errors when it is invalid?
I found an answer which answers my question but its a bad idea:
Render the action that initiated update
def create
#enquiry = SupportForm::Enquiry.new(params[:support_form_enquiry])
topic = #enquiry.topic
#stat = SupportForm::Stat.find(#enquiry.stats_id)
#stat.stats[topic] = #stat.stats[topic].to_i.next
if #enquiry.valid? && #stat.save
redirect_to(root_path)
else
render Rails.application.routes.recognize_path(request.referer).values.join("/")
end
end
The problem is that there will likely be instance variables in the view that submitted the form and I would have to be able to instantiate all the instance variable in the application then.....not possible.
Currently I'm considering putting the errors in the flash hash... not something I want to do. With the original object returned i can repopulate the fields with the users input.
When you use redirect_to, rails will kick off a whole new controller & view sequence. Use
render "path/to/template/from/view/folder"`
instead.
A typical create action using this pattern would look like (for a 'post' object in this case):
def create
#post = Post.new(params[:post])
#created = #post.save
respond_to do |format|
if #created
flash[:notice] = 'Post was successfully created.'
format.html { redirect_to post_path(#post) }
format.js
else
format.html { render :action => :new }
format.js
end
end
end
Notice how if it's successfully created we do a full redirect to the "show" page for the post, but if it's not successful we just do a render.
You should probably modify your support_form helper so that it only creates a new #enquiry if it hasn't been created already:
def support_form
#support_stats = SupportForm::Stat.find(get_stats_id)
#enquiry ||= SupportForm::Enquiry.new(stats_id: #support_stats.id)
render partial: 'support_form/enquiries/form'
end
||= is shorthand for "equals itself or". If it hasn't been defined (or is nil or false) then it will fail the first part of the or and pass through to the second, where the object is created.
In your form partial, also, you should make sure you're using form_for, which will submit to the create or update action depending on whether the object has been saved already.

Form submission help needed

I'm developing a small project using Ruby on Rails where basically student can sign up for a study room. I have one modification that I'm trying to make that I can't figure out yet.
On default when I create new submission, it creates it and goes to the view screen that basically shows all the info from the submission. Here is the controller for the form:
# POST /students
# POST /students.json
def create
#student = Student.new(params[:student])
respond_to do |format|
if #student.save
format.html { redirect_to #student, notice: 'Student was successfully created.' }
format.json { render json: #student, status: :created, location: #student }
else
format.html { render action: "new" }
format.json { render json: #student.errors, status: :unprocessable_entity }
end
end
end
What I'm trying to do is when the user clicks the submit button it doesn't automatically save it to the DB, but instead it goes to the next screen where the user can review their submission and confirm the submission by clicking the button. Once they click the confirm button it should save it to the DB and show the 'Student was successfully created.' notice and take them back to the form screen.
Here is the controller for the show (which is the next page that gets displayed):
# GET /students/1
# GET /students/1.json
def show
#student = Student.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.json { render json: #student }
end
end
Some key points:
Don't use #show to display the preview. #show is supposed to display normal persist objects, while your new object is not persist yet. You also need extra element on preview such as "confirm" link/button.
It's better to use a separate #preview action, which accepts form submit from #new, and will send confirmed object to #create.
You need a vehicle to pass object in #preview, both force(to #create) and back(to #new if not satisfied). Though I don't like to use session, it seems there are not too much choices.
The example code:
def new
#student = session[:student].blank? ? Student.new : session[:student]
# ...
render
end
def preview
#student = Student.new(params[:student])
session[:student] = #student
# ....
render
end
def create
#student = session[:student]
session[:student].clear
# ...
render
end
Add
More about #3. In #create you need params to build a new object with full data to save. However in this case the form is not submitted to #create but #preview instead. So how do you get the data? There are three ways:
a. Plain display(just like #show). In #preview, build a form with hidden fields filled with data sent by #new.
b. Plain display. Use session to pass data instead of form. Much simple than #a.
c. Show both plain text and a form in #preview. User can preview the submission and edit it right in #preview.
According to your need, #b and #c are all good IMO.

Usage of "new" action necessary for json?

If I'm using my rails app to mainly respond to json, is having a "new" or "edit" action that returns an empty object in json still necessary/useful? For example:
def new
#post = Post.new
respond_to do |format|
format.json { render :json => #post }
end
end
I feel like I can just get away from making a server call by completely building the forms in html
In my opinion, yes, you can exclude that action if you don't need special default field values and are assembling your form fields manually.

Rendering an action in another controller

(Rails 2.3.5)
I have two scaffolds: Directories & Users
For a Directory show action (say Show action: "\directories\2"), I took the User\New form and made it a partial so the user can add users to the Directory. What I can't figure out is how in the create action I can return to "\directories\2\show" if there are any validation errors. Returning if the User.save is successful works fine, I just can't figure out how to format a Render action to return to the directory and display error messages and fields in the New User partial.
This works fine if a save is successful, using the same thing if there is an error will work except error_messages will not be displayed (I know that error messages are only suppose to be passed on a Render, not a redirect, but I can not figure out the syntax involved for a render action when an id parameter is involved):
format.html { redirect_to directory_path(#user.directory_id) }
Users Create Action called by partial in Direcory Show action:
def create
#user = User.new(params[:user])
respond_to do |format|
if #user.save
flash[:notice] = 'User ' + #user.name+ ' was successfully created.'
format.html { redirect_to directory_path(#user.directory_id) }
format.xml { render :xml => #user, :status => :created, :location => #user }
else
# what to do here to successfully return to 'directories\show\(#user.directory_id)'
# and what to do here that successfully passed the error_messages
end
end
end
Thanks for any help - hopefully that makes sense
To render an action from another controller you need to specify the template you want to render.
render :template => 'other_controller/view_template_name'
P.S: Keep in mind that you'll have to define any instance variables that the other controller action defines which are necessary for the view to render because rendering a template will not call the other controller's function before rendering the view.

Resources