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:
<form>
should submit to
<form>
uses
name
attribute of each field in the <form>
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.