Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does Rails keep the form data when validations fail?

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?

like image 762
anemaria20 Avatar asked Nov 28 '16 15:11

anemaria20


2 Answers

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.

like image 120
Rajdeep Singh Avatar answered Oct 06 '22 00:10

Rajdeep Singh


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.

like image 22
Alex Kojin Avatar answered Oct 06 '22 00:10

Alex Kojin