I have an 'create' action method in Rails and do:
def create
#movie = Movie.new(movie_params)
if #movie.save
redirect_to #movie, notice: "Movie successfully created"
else
render :new
end
end
Now, I have a few validations in place for the Movie model. In case those validations fail, and #movie.save returns false, I simply invoke the new template (without touching the new action, since render :new is the same as render template: 'new'.
I don't understand how Rails can keep the form data I already entered when it again renders that new view. What's going on behind the hood that allows it to do this?
Let's try to understand this whole process point-wise
Instance variables defined in the controller action are shared with the rendered views.
In your case I'm assuming that there's a new action something like
def new
#movie = Movie.new
end
And you have a corresponding view new.html.erb where you have created a form like this
= form_for #movie do |f|
Now, as you know the #movie object that you are passing in form_for method is defined in new action. Most of the times we don't pass any parameters to the new method in new action. The form fields are blank when you load the form because the attributes of the object(in your case #movie) are by default blank because we just initialize an empty object(Movie.new).
Let's assume your Movie model has a name attribute, Try doing this in your new action
def new
#movie = Movie.new(name: 'Hello World!')
end
Now when you will load the new action, you will see Hello World! populated in your name text field because your #movie object is initialized with this value.
Also, keep in mind that Rails Convention-Over-Configuration automatically generates the form URL in this case, by default it points to the create action. When you submit the form the request is made to the create action. This takes me to the next point.
When we submit the form all the filled in form values are sent to the action whose route matches with the form URL(in your case URL points to the create action)
In create action you are receiving parameters in the form of a hash with model attributes(Movie attributes) as keys and the filled in information as their values. The first line in your create action is
#movie = Movie.new(movie_params)
This is a very important line of code, try to understand this. Let's assume your form had only one text field, i.e., name. Now movie_params is a method that looks like this
def movie_params
params.require(:movie).permit(:name)
end
Now, the movie_params method will return a hash something like { 'name' => 'Hello World!' }, now you are passing this hash as a parameter to Movie.new method.
So now, after breaking up the code, the first line of your create action looks like
#movie = Movie.new({ name: 'Hello World!' })
That means your #movie instance variable contains an object of Movie class with name attribute set to Hello World!. Here, when after initialization, if you do #movie.name it will return Hello World!.
Now, in the second line you are calling #movie.save that returned false due to failed validation in your case as you have already mentioned in the question. As it returned false the execution will go to the else part. Now this takes me to the next point.
Calling render :action(in your case render :new) in the controller renders only the view that belongs to that action and does not execute that action code.
In your case, you called render :new, so there you are actually rendering the new.html.erb view in create action. In other words, you are just using the code in new.html.erb and not in new action. Here, render :new does not actually invoke the new action, it's still in the create action but rendering the new.html.erb view.
Now, in new.html.erb you have created a form that looks like
= form_for #movie do |f|
Now as my explained under my first point, the instance variables that are declared in the action are shared by the rendered view, in this case #movie object that you have defined in create action is shared by the rendered new.html.erb in create action. In our case, in create action the #movie object was initialized with some values that were received in the parameters(movie_params), now when new.html.erb is rendered in the else, the same #movie object is used in the form by default. You got the point right, you see the magic here?
This is how Rails works and that's why its awesome when we follow the convention! :)
https://gist.github.com/jcasimir/1210155
http://guides.rubyonrails.org/v4.2/layouts_and_rendering.html
Hope the above examples cleared your doubts, if not, feel free to drop your queries in the comment box below.
form_for helper takes data from #movie variable. In create action forms data assigns to #movie variable. When you call render :new form_for takes column's data from #movie variable.
I'm not sure how deep under the hood you want to go, but basically when you POST to the create method the data is passed to the params, the params being just a key:value pairs where the key and the value are strings, but rails has a special syntax and methods for turning into hashes. params data is passed the Movie model to be processed and the result stored in #movie. When the form is rendered the #movie date is passed back to the form - that data is used to repopulate the form.
I would recommend this blog post and the rails guidefor further reading.
I will try to explain little bit:
in method create first of all we set instance variable
#movie = Movie.new(movie_params)
#movie at this moment has fields filled with movie_params
and after validates brakes we say to Rails 'render :new' with variable #movie.
This is the same if we assign attributes into form:
= form_for Movie.new(movie_params) do ...
When you submit your form. You call create method where all values of movie_params are initializes in #movie. Now due to any reason code break then you call render new for same object (#movie). So form come up with values.
Means in whole process your #movie object persisted.
Related
I've been working on several rails tutorial and I don't understand why the method new and method create both have
#example = Example.new
What is the point for the new method to have #example = Example.new when all this controller action does is to render the new page?
New
#example = Example.new meaning to prepare your form so user can fill the data in form and #example instance will hold the value.
Create
#example = Example.new meaning create new object to save your data into database inside create usually #examle.save, this will save your data that passed from new (through params)
Within Rails' implementation of REST new and create are treated differently.
An HTTP GET to /resources/new is intended to render a form suitable for creating a new resource, which it does by calling the new action within the controller, which creates a new unsaved record and renders the form.
An HTTP POST to /resources takes the record created as part of the new action and passes it to the create action within the controller, which then attempts to save it to the database.
From the Ruby on Rails documentation about create:
create(attributes = nil) {|object| ...}
Creates an object (or multiple objects) and saves it to the database, if validations pass. The resulting object is returned whether the object was saved successfully to the database or not.
From the Ruby on Rails documentation about new:
new(attributes = nil) {|self if block_given?| ...}
New objects can be instantiated as either empty (pass no construction parameter) or pre-set with attributes but not yet saved (pass a hash with key names matching the associated table column names). In both instances, valid attribute keys are determined by the column names of the associated table — hence you can‘t have attributes that aren‘t part of the table columns.
So create instantiates the new object, validates it, and then saves it to the database. And new only creates the local object but does not attempt to validate or save it to the DB.
What is the point for the new method to have #example = Example.new when all this controller action does is to render the new page?
Because it renders the page using the attributes of the #example object.
You may be assuming that on a newly initialised object, all the attributes will have a value of nil. This is not necessarily the case, because code in the model (an after_initialize callback, or an enum, for example) may be providing default values.
The controller code itself might provide default values also in some cases, either inferred from the current user or passed in through parameters.
You will be having create method like this:
def create
#example = Example.new(example_params)
if #example.save
redirect_to #example
else
render 'new'
end
end
Notice that inside the create action render is getting used instead of redirect_to when save returns false. The render method is used so that the #example object is passed back to the new template when it is rendered.
You need to tell the user that something went wrong. To do that, you'll modify app/views/examples/new.html.erb to check for error messages:
<% if #example.errors.any? %>
<div id="error_explanation">
...
</div>
<% end %>
We check if there are any errors with #example.errors.any?, and in that case we show a list of all errors with #example.errors.full_messages.
The reason why #example = Example.new in the ExamplesController is that otherwise #example would be nil in our view, and calling #example.errors.any? would throw an error.
This one is a bit challenging so bear with me. Here is the summary. I added a custom validation to one of my models. After I added this everything works fine with all actions except the update action. If I restrict the new validator to only the create action, then the update action works fine. Below is the related code:
In my model
validate :start_must_be_before_end_time
def start_must_be_before_end_time
return if customer_start.blank? || customer_end.blank?
if customer_start > customer_end
errors.add(:customer_start, "start time must be before end time")
end
end
In my controller for the update action:
def update
#handover = Handover.find(params[:id])
if #handover.update_attributes(params[:handover])
UpdatedHandover.perform_async(#handover.id)
flash[:success] = "Handover Template Updated and Approvals Sent!"
redirect_to view_context.select_handover_cal(current_user)
else
flash[:error] = "Please correct the following errors in your form!"
render edit_handover_path(#handover.id)
end
end
So if the start time is before the end time in the create action, everything works fine. It renders the new action and displays the error. If this happens in the update action it gives me a missing template error for the edit action. The edit file is in the proper place and this works if the validator is restricted to the create action. I cannot figure out for the life of me why this is giving me so much trouble. This is rails 3.2.18. Thanks for your help!
You should pass template name to render method, not a path. So if you want to render 'edit.html.erb', pass 'edit'.
Change
render edit_handover_path(#handover.id)
to
render 'edit'
Note that if you used extra instance variables in edit template, you would need to set them in update action.
For better understanding of render:
When you use render, you pass the instantiated object (newly created or updated). When attempting to update the object, validation was triggered and, if somehow unsuccessful, you render edit, since your in memory object contains necessary validation errors.
But edit_something_path is used when you use redirect_to. When you have the object saved and you can get the persisted data from the database.
You problem can be solved in two ways:
render 'edit'
render :edit
# Your validation errors will persist
or
redirect_to edit_handover_path(#handover.id)
# Your validation errors will be gone
def new
#post = Post.new
respond_to do |format|
format.html # new.html.erb
format.json { render json: #post }
end
end
def create
#post = Post.new(params[:post])
#something else
end
Here since when we are actually creating a new post we call the method create where Page.new(params[:page]) is used, method new should only be used to call the view new.html.erb. So why we still need an instance variable #post in new method here?
You don't need any instance variables in new or any other action, but by default, Rails's scaffolding uses them. There are some minor benefits that come with using instance variables, and there's some convention around them, but frankly it's sloppy code and should not be the default. In a proper MVC framework, the controller's instance variables wouldn't even be visible to the view object.
I prefer to be explicit, use local variables, and pass them to the view as locals:
def new
post = Post.new
respond_to do |format|
format.html { render locals: { post: post } }
format.json { render json: post }
end
end
This is more explicit, and makes your intent clear. The view becomes more flexible with locals, since you don't have to worry about setting instance variables before rendering a partial from inside another view. It properly encapsulates the data and doesn't expose your post outside the action.
If you're trying to quickly prototype/spike something, you might save a few characters by using instance variables, but it's not clean code.
If you did not create a new instance variable then you wouldn't have a model to hold the data in which the model is supposed to be managing - not to mention you would be adding a lot more boiler plate for your forms instead of using the form helpers that take a model. Without using the model you're not using the MVC framework fully as intended.
Now that all being said, it's by no means required to do anything it's just following the MVC structure that Rails is built on top of. No pattern is required, there are always other solutions, it's just that the accepted method of performing this action involves a model - albeit and empty one.
Finally, I don't know when or why you'd want to respond with a new route with JSON since the new route is generally used for displaying a form to create an object while the create function is the one used to actually create a new instance of the model.
So i have the following code:
def new
#all_areas = Area.all
#area = Area.new
end
The reason i am passing in all_areas is it's required for a drop down box in the form, Using mongoid and an Area can be recursively embedded in another Area.
My form has the following code:
<% if #all_areas %>
<%= f.label :parent_area %>
<%= f.collection_select(:parent_area, #all_areas, :_id, :name, prompt: "Select a Parent...") %>
<% end %>
However when i submit 'invalid values, i.e blank name, the 'new' page does not render the select box to select a parent.
What is going on here? Is this a bug?
My create action is pretty simple, if it fails validation i just do the following:
else
render 'new'
Why is #all_areas not passed to the view the second time? i have actually fixed it by changing the code in my create action to the following:
else
#all_areas = Area.all
render 'new'
But this is quite surprising, unless i am missing something?
It's not a bug. The reason for the behaviour is that the create action is actually quite separate from the new action, so the instance variables you assigned in new don't get carried over. render 'new' only renders the view called "new", it doesn't actually call the new action.
new happens when you issue a GET request to /areas/new. create happens when you POST to /areas. Because they're separate requests, the server doesn't remember any state - in fact, you could call create without ever calling new (say if you used curl from the command line).
Basically, your approach is correct, you need to set the #all_areas instance variable in both actions. You might want to extract it out into a separate private method to avoid the duplication.
Here's what I'm trying to do.
when the user clicks new note.. I want the user to be taken to a page when they can start typing a note, and save it to the server all with AJAX..
Problem is, every time the page saves, it's making a new note.
This leads me to believe that when Rails gets the DEF NEW controller, some how I need rails to first make a NEW NOTE record and then redirect to the edit controller of that new note, where the user can create/edit the note all with AJAX.
Thoughts? Thanks.
I had the same problem once, creating the note first is probably a good idea.
Another way would be to send the user to the new action. When the first save occurs you send the new object back as a JSON object, and replace the form's action with the update url for that record as well as setting the form's method to put.
This way you don't end up with empty records in the database (with your use-case, you might want exactly that, so a User can continue a note later.)
Just my two cents.
Ok a way of implementing this could look like this:
Form
<%= form_for resource,
:remote => true,
:html => { 'id' => 'autosave' },
:url => resources_path(:format => :json) do |f| %>
...
<% end %>
Application JS
var $form = $('#autosave');
// bind to the first success event from the form
$form.one('ajax:success', function(data, status, xhr) {
// data contains our json object (your note as json)
// now we update the form
$form.attr('action', '/notes/' + data.id);
$form.attr('method', 'put');
$form.attr('data-method', 'put');
});
Controller
class ExampleController
...
def create
#
# respond with the json object if format = json,
# see the form above I'm passing the format with :url parameter in form_for
#
respond_with(resource) do |format|
format.json { render :json => resource }
end
end
end
If you really want use to use #new to create a note and save it, then you can simply do
def new
#note = Note.create # instead of Note.new
end
Rails will then display this note just like the #edit action, so the note id will be in a hidden field. Then when you send the Ajax calls, you'll be calling #edit. If you want to preserve the behavior of #new for when javascript is turned off, then you might want to create a different action.
def new
#note = Note.new
end
def new_js
#note = Note.create
end
When you load the page that has the link to new_note, include some javascript that changes the link to new_js_note. So when JS is off, you get the standard #new form. When JS is on, you get a form that is basically editing a preexisting blank note.