The form_for Helper

Patterns in Forms

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 PATCHing 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.

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.

url_for and form_for

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.

Moving pages with form_tag to form_for

Now that we've shown what form_for can do, the next logical step would be to update all our form_tags 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.

Our Own form_for

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.