Please explain form_for method in Rails - ruby-on-rails

This isn't really a troubleshooting question but rather a request for an explanation. I'm having a hard time understanding the workings of the form_for method. Could someone explain to me what this method does in this situation. Here is my code for creating a form for the comments feature on a blog application. My code works, so i just want to understand WHY it works and How it works. Thanks!!
Here is my new comment form:
<%= form_for([#post, #post.comments.build]) do |c| %>
<p>
<%= c.label :content, class: "col-md control-label" %><br>
<%= c.text_area :content, rows: "10", class: "form-control" %>
</p>
<p>
<%= c.submit %>
</p>
<% end %>
And here is my code for the comments Controller:
class CommentsController < ApplicationController
def new
#post = Post.find(params[:post_id])
end
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(comment_params)
#comment.user_id = current_user.id
#comment.save
#redirect_to post_path(#post)
redirect_to posts_path
end
private
def comment_params
params.require(:comment).permit(:content)
end
end
In particular, what does the "[#post, #post.comments.build]" parameter of form_for do?

First off, there's nothing you can do with form_for that you couldn't do with form_tag (and some extra typing).
What form_for allows you to do is easily create forms that fit with the rails conventions in terms of urls & parameter naming.
The first argument to form_for is the resource that is being edited or created. At it's simplest this might be just #post. The array form is for namespaces or nested resources.
Your example of [#post, #post.comments.build] means that this is a form for a new comment (the last element of the array is an unsaved instance of Comment) that is nested under that specific post. This will result in the form doing a POST request to /posts/1234/comments (assuming the post has an id of 1234). The corresponding nested route needs to exist for this to work.
The second thing form_for does for you is allow you to write c.text_area :content and have that automatically use the correct parameter name (comment[content]) and have the value prefilled with the current value of the comment's content attribute.

The form_for will do a post to a specific resource and help to draw the inputs.
Example 1
form_for(#post) will do a post to myapp/posts/create and draw the posts fields
Example 2
form_for([#post, #post.comments.build]) will do a post to myapp/posts/:post_id/comments/create and draw the comments fields

here [#post, #post.comments.build] means this form is for a new comment, and form will do a POST request to /posts/post_id/comments (post_id is a #post.id)

Related

How to get Rails form_for to point towards different controller

I have a form for creating new comments. This code exists in a page that is under a different controller (let's say it's app/views/posts/show.html.erb).
<%= form_for Comment.new do |f| %>
<%= f.label :content %>
<%= f.text_field :content %><br/>
<%= f.submit %>
<% end %>
The form works if I have Comment.new like above, but I want to use an instance variable like form_for #comment, similar to the first code snippet in this link: https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/FormHelper.html
In order to do so, I thought I need to define a new function like this and assign an empty comment. I tried putting this code in both the posts_controller and comments_controller.
def new
#comment = Comment.new
end
But when I replace Comment.new with #comment, I get this error: ActionView::Template::Error (First argument in form cannot contain nil or be empty):
This leads me to believe that neither of the new methods are being called. What am I doing wrong here?
My routes.rb looks like this:
Rails.application.routes.draw do
root to: 'posts#show'
resources :messages
end
if you are using show page (app/views/posts/show.html.erb) to display form
add this line in the show action of posts controller
# posts_controller
def show
#comment = Comment.new
end
and if you also want to submit your form other than the comment's create action mention the url in form_for tag
<%= form_for #comment, url: posts_path do |f| %>
<%= f.label :content %>
<%= f.text_field :content %><br/>
<%= f.submit %>
<% end %>

What is the purpose of having an instance created inside new method of Rails?

Learning rails from the guide, but what is the reason for having an empty article instance in new? When you go to new, it just renders a form for you and then when you submit, it creates it with the new instance inside the create action. If there are errors, it returns that instance to the new view (form) correct? In the docs, it says the below:
The reason why we added #article = Article.new in the
ArticlesController is that otherwise #article would be nil in our
view, and calling #article.errors.any? would throw an error.
But why would #article be nil if you already have created an instance through the create action? Why would the new action need to create another instance that has the errors? Isn't the instance created by create with the errors the one that will be rendered in new?
//controller
def new
#article = Article.new
end
def create
#article = Article.new(article_params)
if #article.save
redirect_to #article
else
render 'new'
end
end
private
def article_params
params.require(:article).permit(:title, :text)
end
Rails runs only the code inside corresponding action when processing a request. So when user goes to /articles/new only new action is triggered and when user makes HTTP POST request to /articles only create action is called. Read rails guides on ActionController
I am not an expert in explaining but I will try.
To me, Rails use this way so that we will have to write less code and rails will handle rest of the things.
Here is what you do:
Lets assume, your Article model has 2 attributes(columns) i.e title, text.
Controller:
def new
#article = Article.new
end
And in your view:
<%= form_for #article do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
So when you do this, rails knows that you are generating a form for #article which is a (empty) object of Article class.
So it generates appropriate HTML from the from helpers like
<%= f.text_field :title %> with proper id and name so that it can be mapped later in create action.
You don't have a title attribute in model then it will throw an error. So you can not do like this:
<%= f.text_field :body %>
Because rails knows, this form is for Article and your article does not have any attribute(column) for body.
when the form generates, it does not have any idea about your create action. It knows about new action only.
So this is one of the reason behind initializing a new record for form.
I hope you got my point. Maybe someone else can explain better with any other reason behind this.

form_for Ruby On Rails

I'm trying to follow a tutorial on using basic AJAX to add a record to a list in place, and I'm having issues using form form_for.
Here is my code.
<%= form_for ([#product, #product.new]) do |p| %>
<p>
<%= p.label :product_part %>
<%= p.text_field :product_part%>
</p>
<p>
<%= p.submit %>
</p>
<% end %>
The error I am getting is
undefined method `new' for nil:NilClass
I understand why I am getting the error (#products hasn't been "initialized") but I have no idea how to fix this issue (I am sure it's simple). I have seen something about putting a resource in the routes file, but I do not know for sure.
If you're trying to make a form for a new product, you should (in your controller) be setting #product to an instance of a new Product:
# app/controllers/products_controller.rb
def new
#product = Product.new
end
In your view, using [#product, #product.new] makes no sense. You can't invoke new on an instance of a product. It's very unclear why you're not simply using the following, which is the correct use of form_for with a new instance of an ActiveRecord model:
form_for #product do |p|
Do this:
#app/controllers/products_controller.rb
class ProductsController < ApplicationController
def new
#product = Product.new
render :form
end
def edit
#product = Product.find params[:id]
render :form
end
end
#app/views/products/form.html.erb
<%= form_for #product, remote: true do |f| %>
<%= p.label :product_part %>
<%= p.text_field :product_part%>
<%= f.submit %>
<% end %>
This will do everything you need for both the new and edit methods (which you raised concerns about in your comments with #meagar).
This should be corroborated with the following routes (you can see why here):
#config/routes.rb
resources :products
I would say In case you need to look the form_for helper ; to understand the behavior of the method.
The method form_for It accept the argument as record, options = {}. The value of record could be a symbol object or a newly object of respective class in your case Person.new.
Second argument could be
:url, :namespace, :method, :authenticity_token, :remote , :enforce_utf8, :html
Among them :remote => true option is used as the Ajaxify your request.
form_for is a helper that assists with writing forms. form_for takes a :remote option. It works like this:
<%= form_for(#article, remote: true) do |f| %>
....
<% end %>
This will generate the following HTML:
<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
...
</form>
Note the data-remote="true". Now, the form will be submitted by Ajax rather than by the browser's normal submit mechanism.
For more info about Form-For helper
Hope this solve your problem!!!.

Rails Form Action

I have a form like this in my html page
<%= form_for '/purchases', html: {class: "form form-horizontal validate-form", novalidate: "novalidate"} do |f| %>
But the form action is like this when i inspect the form
action = '/purchases/new'
I want the action to be just /purchases
and my controller method is like
#purchase = OrderItems.new
If you have your routes configured properly, you just need:
<%= form_for #purchase, html: {class: "form form-horizontal validate-form", novalidate: "novalidate"} do |f| %>
of course, provided you set #purchase in your new action:
#purchase = Purchase.new
I think you're getting confused between form_for and form_tag
--
form_for is for objects -
#app/controllers/purchases_controller.rb
Class PurchasesController < ApplicationController
def new
#purchase = Purchase.new
end
def create
#purchase = Purchase.new(purchase_params)
#purchase.save
end
private
def purchase_params
params.require(:purchase).permit(:purchase, :attributes)
end
end
The important thing to note with form_for is how it will build your form out of the ActiveRecord object you define. This is vitally important, and is at the root of your error:
<%= form_for #purchase do |f| %>
This will build all the different attributes of the form (including the action attribute), from the ActiveRecord object itself. This means if you populate your form_for with anything other than an object, you're going to get into trouble (as exhibited by your error)
--
form_tag is for data -
<%= form_tag your_path do %>
...
<% end %>
This might be better suited to your circumstances, as it allows you to create a "standalone" form - one which gives you the ability to send non-model-centric data to your application
We use form_tag implementations for the likes of search facilities etc
Solution
As Marek pointed out, you need to populate your form_for with an ActiveRecord object. To do this, you need first ensure you have initialized the object in your controller's new action, before passing the value to the form:
#app/controllers/purchases_controller.rb
Class PurchasesController < ActiveRecord::Base
def new
#purchase = Purchase.new
end
...
end
#app/views/purchases/new.html.erb
<%= form_for #purchase do |f| %>
I would just like to elaborate on the answers presented here. The way form_for works is it bases the form input names on the first argument passed to it. You can pass string or symbol as the first argument, and it should work, just like your example.
= form_for :purchase do |f|
= f.text_field :order
will result in a form that will submit to the current url and will contain 1 text field with the name purchase[order].
Marek's answer is correct however it doesn't answer your question. In cases where the object of the form, by convention, doesn't match the controller that will handle the request, you can pass a url option to form_for.
# controller
def new
#purchase = OrderItem.new
end
# view
= form_for #purchase, url: '/purchases' do |f|
= f.text_field :order
This will create a form with action set to /purchases but the name of the text field will be order_item[order].
FINAL NOTE
form_for is not exclusively used for active record objects as stated in the API docs

Rails call another controller's action

I have 2 models, Posts and Comments. I would like to create a button on the Post show view which directs it to Comment new action. So I create a new action in Post:
def comment
#post = Post.find(params[:id])
redirect_to new_comment_path
end
I want to save the post_id in the Comment models, so I create d hidden field in the new comment form:
<div class="field">
<%= f.hidden_field :post_id, :value => #post.id %>
<%= f.label :body %><br />
<%= f.text_field :body %>
</div>
But error appeared: "Called id for nil".
I am very new, can anyone help? Or should I use other approach?
Well you are missing to pass the value,
I had tried out this way and it works, for your example.
edited:
redirect_to :controller=>'comments', :action=>'new_comment', :post_id=>#post.id
receive as #post_id = params[:post_id]

Resources