Building Our Own Form Helpers

my_form_tag

We'll build our own form helpers to better understand what the form helpers do. We'll start with our own version of form_tag - we'll call it my_form_tag instead.

To build a my_form_tag helper, we'll need to be aware of a number of different things:

  1. the path the <form> should submit to
  2. the HTTP verb the <form> uses
  3. the name attribute of each field in the <form>
  4. populating each field with a value, if needed

The first two have to do with the <form> itself, and the last two have to do with the fields. We'll define a my_form_tag helper to build the <form>, and then define helpers like my_text_area_tag to build the fields.

But before we define the helpers, let's see what we want our view to look like. For this exercise, we'll replace the new comment <form> in posts/show with a call to our yet-undefined my_form_tag method.

<!-- app/views/posts/show.html.erb -->
<html>
  <body>
    <!-- ... -->

    <div class="post">

      <!-- ... -->

      <div class="comments">

        <!-- ... -->

        <%= my_form_tag post_comments_path(@post.id) do %>

          <% unless @comment.new_record? %>
            <%= my_hidden_field_tag '_method', 'patch' %>
          <% end %>

          <%= my_label_tag 'Comment' %>
          <%= my_text_area_tag 'comment[body]', @comment.body %>
          <br /><br />

          <%= my_label_tag 'Author' %>
          <%= my_text_field_tag 'comment[author]', @comment.author %>
          <br /><br />

          <%= my_submit_tag %>
        <% end %>

      </div>
    </div>

    <!-- ... -->
  </body>
</html>

We must define the fields inside the <form> using a block, because this is the way we're able to nest HTML using ERB and helper methods. That is, this block allows us to nest the fields inside of the <form> element, which really just means placing the HTML strings for the fields before the closing </form> tag. We'll see how we accomplish that in just a minute.

Inside the block, we conditionally create a hidden field to set the HTTP method for the form to PATCH if we're updating the record:

<% unless @comment.new_record? %>
  <%= my_hidden_field_tag '_method', 'patch' %>
<% end %>

Our versions of input, textarea, and label helpers are all functionally the same as the ones that come with Rails. Notice, that it also reads in almost an identical manner as if we were using the form helpers provided by Rails.
Now that we know what helpers we'll be using, let's implement them:

### app/helpers/application_helper.rb ###

module ApplicationHelper

  # ...

  def my_hidden_field_tag(name, value)
    "<input name='#{name}' value='#{value}' type='hidden' />".html_safe
  end

  def my_label_tag(txt)
    "<label>#{txt}</label>".html_safe
  end

  def my_text_field_tag(name, value)
    "<input name='#{name}' value='#{value}' type='text' />".html_safe
  end

  def my_text_area_tag(name, value)
    "<textarea name='#{name}'>#{value}</textarea>".html_safe
  end

  def my_submit_tag(txt="Submit")
    "<input type='submit' value='#{txt}'>".html_safe
  end

  def my_form_tag(path, &block)
    attrs  = "method='post' action='#{path}'"
    fields = capture(&block)
    "<form #{attrs}> #{my_authenticity_token_field} #{fields} </form>".html_safe
  end

  def my_authenticity_token_field
    my_hidden_field_tag('authenticity_token', form_authenticity_token)
  end
end

We see all of our _tag helpers for our labels and fields, which each return the proper HTML element with their attributes and contents set by the parameters. And then there's my_form_tag:

def my_form_tag(path, &block)
  attrs  = "method='post' action='#{path}'"
  fields = capture(&block)
  "<form #{attrs}> #{my_authenticity_token_field} #{fields} </form>".html_safe
end

There's also attribute setting being done here, but it's worth noting how the block is handled here.

The & in front of &block is used to capture a block as a named parameter. This is syntactically necessary, because if we didn't include the & in front of the parameter, Ruby would think this was another non-block parameter.

Blocks are usually called with yield or block.call, but there's something a little different about this block: it's ERB. Because it's ERB, we have to handle it a little differently, executing it instead with the Rails-provided capture method. capture allows us to execute the ERB block, assign the HTML string returned to a variable, and place that where we want it inside of our <form> string.

If we were to execute the block normally, like this:

def my_form_tag(path, &block)
  attrs  = "method='post' action='#{path}'"
  fields = block.call # <- calling the block normally
  "<form #{attrs}> #{my_authenticity_token_field} #{fields} </form>".html_safe
end

We end up with something like this:

<!-- ... -->

<label>Comment:</label>
<textarea name='comment[body]' value=''></textarea>

<label>Your Name:</label>
<input name='comment[author]' value='' type='text' />

<input type='submit' value='Submit'>

<!-- ^ Only the authenticity token field makes it into the <form>! -->

<form method='post' action='/posts/1/comments'>
  <input name="authenticity_token" value="" type="hidden">
</form>

<!-- ... -->

This happens because executing the ERB block results in its return being placed immediately into the output HTML when the call occurs. In our helper above, this call occurs before our my_form_tag return, so the fields are output before the <form>. Not what we want! So we instead use capture to trap the block's return value in a variable, and then place it in the <form> string where it belongs.

So the capture implementation will give us a working <form>, just like we had before, but we've actually reinvented the wheel a bit here, since Rails already provides all of these methods, and they take the exact same parameters.

Our own helper now works just like Rails' form_tag helper.

This all reads pretty nice, but we're still working a little too hard. For instance, we're stuck calculating: the path for the <form>, the name attribute values for the fields, and whether or not we need a hidden field to make a PATCH request for an update. And this is all taking place in our view. It would be nice to push this logic somewhere else. But, for now we'll hold off on doing that, as it will require the use of ActiveRecord objects.