Rails forms follow very specific conventions. If we reexamine the resources that we've been working with so far, we'll see a repeated pattern.
Whenever we use a form to POST
new data to the DB, our URL is of the form /resources
.
That means that if we want to use a form to create a new resource, we would POST
to a URL like:
/comments
.
If we need to update a resource, we have to specify which type of resource we wish to update (e.g. comments or posts). And we also have to specify which item out of this collection of resources we want to update via the id number.
An example of a URL for PATCH
ing our data (updating it), would then look like: /comments/1
. Keep this in mind while we move forward.
So far we've been working with form_tag
to dynamically create our HTML forms used in our Rails app. There is another type of form helper that can be quite useful when working with Active Record model resources in our application. This form helper is called form_for
.
The form helper form_for
is very similar to form_tag
. The difference here is that the method signature doesn't require a URL. When we use form_tag
, we need to specify which action we want to submit data to, and to do that we specify a URL string with url_for
. form_for
does this for us, all we need to do is pass a certain type of model, and form_for
generates the correct URL for us by using url_for
behind the scenes. Let's convert our new post form to use form_for
instead of form_tag
:
<!-- views/posts/new.html.erb -->
<!-- ... -->
<%= render 'form' %>
<!-- ... -->
There are definitely some differences in the code above when compared to what we currently have set up. We'll have to update the form fields partial to get all of this to work. Before, we only included our form_fields
in that partial, this was because we were using form_tag
, with form_for
we may include the entire form. We'll be renaming our partial to _form.html.erb
.
<!-- views/posts/_form.html.erb -->
<%= form_for @post do |f| %>
<%= f.label 'Title' %>
<%= f.text_field :title %>
<br /> <br />
<%= f.label 'Body' %>
<%= f.text_area :body %>
<br /> <br />
<%= f.label 'Author' %>
<%= f.text_field :author %>
<br /> <br />
<%= submit_tag "#{@post.new_record? ? 'Create' : 'Update'} Post" %>
<% end %>
Notice how there is now a block parameter f
. That is a FormBuilder
object. It allows us to specify form helpers in a more condensed manner. form_for
knows that we are using the Post
Active Record object stored in @post
. All we need to do for each form helper is specify the method we wish to call on @post
. For instance, let's consider the code, <%= f.text_field :title %>
. This gets turned into the HTML code:
<input type="text" name="post[title]" id="post_title">
And if post.title
does contain a value, then that will show up as well in the value
attribute,
the above code would become:
<input type="text" name="post[title]" id="post_title" value="text value here">
This feature is especially useful when certain validations fail and the page gets re-rendered.
In that case the value will remain filled in, just as if it had been set to @post.title
directly.
We've mentioned that form_for
uses url_for
behind the scenes to determine which
action to route to. Rails can do this when supplied with an Active Record object in conjunction
with url_for
. Let's take a deeper look at how this really works using the rails console.
$ bundle exec rails c
# include the helpers in order to have access to `url_for`
include Rails.application.routes.url_helpers
# set host in default_url_options:
default_url_options[:host] = "localhost:3000"
# A new Post record
post = Post.new
url_for(post)
# => "http://localhost/posts"
# An existing Post record
post = Post.first
url_for(post)
# => "http://localhost/posts/1"
# A new comment on an existing Post
post = Post.first
comment = Comment.new
url_for([post,comment])
# => http://localhost/posts/1/comments
# An existing comment on a Post
post = Post.first
comment = post.comments.second
url_for([post,comment])
#=> "http://localhost/posts/1/comments/2"
form_for
takes the same type of arguments as url_for
; if we want a form related to a new Post
or an existing one, we directly pass in a Post
object as the first argument to form_for
.
That will get passed to url_for
behind the scenes and a valid URL to the correct action will get generated for our form.
Now that we've shown what form_for
can do, the next logical step would be to update
all our form_tag
s to form_for
. As long as we have an Active Record model to work with, using form_for
makes a bit more sense than form_tag
. It does more for us with less code. Below are the remaining forms that we can now move to form_for
.
<!-- app/views/posts/show.html.erb -->
<!-- form used to delete a comment -->
<%= form_for [@post, comment], method: "delete" do %>
<%= submit_tag "Delete Comment" %>
<% end %>
<!-- form used to create a comment -->
<%= form_for [@post, @comment] do |f| %>
<%= f.label 'Comment' %>
<%= f.text_area :body %>
<br /><br />
<%= f.label :author %>
<%= f.text_field :author %>
<br /><br />
<%= f.submit %>
<% end %>
<!-- app/views/posts/edit.html.erb -->
<%= render "shared/errors", obj: @post %>
<%= render 'form' %>
<br />
<%= my_link_to 'Back to Post', posts_path %>
See how we're using the form partial now? Rails does a bit of work for us in that we may use the same partial for both new posts and existing posts. If @post
is a new Post
object, then the HTTP verb is set appropriately to POST
. If @post
is an existing object, then Rails injects a hidden input tag with a method attribute into the form, and that input tag will have a method of patch
. That will let Rails know to set the HTTP verb to PATCH
. Setting the HTTP verb correctly is key to sending the form data to the correct controller action.
Similar to how we created a my_form_tag
method, let's now make a my_form_for
method. Putting this together will help us understand how form helpers really work. We really only need to convert one of our forms to see how these form helpers are working. The comments creation form seems like a good choice, so let's go with that one.
<!-- app/views/posts/show.html.erb -->
<%= my_form_for [@post, @comment] do |f| %>
<%= f.my_label 'Comment' %>
<%= f.my_text_area :body %>
<%= f.my_label 'Your Name' %>
<%= f.my_text_field :author %>
<%= f.my_submit %>
<% end %>
Here, we don't figure out a path and pass it in to the form helper, we pass the array of objects instead. Remember that when we have nested routes, we have to pass in two models. In this case, the first model object helps us identify which post we want. The second model object helps us identify which comment out of all the comments is related to that one post. The ids of each model are inserted into the URL so that we can navigate to the correct page.
Next, let's create those custom form helpers we use above. This can be done by defining them in the ApplicationHelper
file.
### app/helpers/application_helper.rb ###
module ApplicationHelper
# ...
def my_form_for(records, &block)
# grab the record we're building the form for
# either the lone record OR last record of the array
@record = records.is_a?(Array) ? records.last : records
# it's critically important that this is set as an
# instance variable, as this is how we get at the record
# later for e.g. the field values
# here we use `capture` again, this time passing `self`
# as the context for the block to be executed in. this
# `self` will be the view, and is necessary so that the
# @record will be accessible for the later calls
fields = capture(self, &block)
# tack on the hidden `_method` field if needed
unless @record.new_record?
fields += my_hidden_field_tag('_method', 'patch')
end
# build the path string, then the <form>
path = url_for(records)
attrs = "method='post' action='#{path}'"
"<form #{attrs}> #{my_authenticity_token_field} #{fields} </form>".html_safe
end
def my_label(text)
my_label_tag(text)
end
# method to build name attr values
# w/ format: "record_class_name[attr_name]"
def name_for(record, attr_name)
record_class_name = record.class.to_s.underscore
"#{record_class_name}[#{attr_name}]"
end
def my_text_area(attr_name)
# build the name string
name = name_for(@record, attr_name)
# dynamically grab the attr value off of the @record
value = @record.read_attribute(attr_name)
# create and return the HTML string for the tag
# using our tag helper from before
my_text_area_tag(name, value)
end
# very similar to `my_text_area`
def my_text_field(attr_name)
name = name_for(@record, attr_name)
value = @record.read_attribute(attr_name)
my_text_field_tag(name, value)
end
def my_submit
# decide what the submit text should be
text = if @record.new_record?
"Create #{@record.class}"
else
"Update #{@record.class}"
end
# build and return the tag string
my_submit_tag(text)
end
end
Keeping track of the object we're building the form for in @record
, we're able to push a lot of the logic to our helper methods and keep our view nice and clean.
And, as you might guess, Rails provides these helper methods like these to us, just as it did with the _tag
helpers earlier.
Our own my_form_for
form helper works very similar to Rails' built in form_for
helper! Here is how the Rails form helper would look like:
<!-- app/views/posts/show.html.erb -->
<%= form_for [@post, @comment] do |f| %>
<%= f.label :body, 'Comment:' %>
<%= f.text_area :body %>
<%= f.label :author, 'Your Name:' %>
<%= f.text_field :author %>
<%= f.submit %>
<% end %>
The label field implementation is a bit different. Here it takes two arguments and the first is the attribute name
the label corresponds to and the second is the text we want displayed for the label. We want to pass the attribute name here because f.label
will build an appropriate for
attribute to match its field's id
. For example, this results in author label and field HTML like this:
<label for="comment_author">Your Name:</label>
<input id="comment_author" type="text" name="comment[author]" />
<!-- the matching `for` and `id` values -->
In this chapter we've covered quite a bit. We've slowly added in more functionality to our forms with form_for
, and we've even made our own form_for
as my_form_for
. Hopefully you're starting to see what's behind all the magic of our Rails form helpers.