-
Notifications
You must be signed in to change notification settings - Fork 0
04 Posts New
We need a way for people to create new posts. We start with a new
action in PostsController
, and a corresponding view.
All this new action needs to do is instantiate a new post and assign it to an instance variable we can use in our view.
app/controllers/posts_controller.rb
def new
@post = Post.new
end
Now we need a corresponding view: app/views/posts/new.html.erb
In our previous app, we used scaffolding to get started on this form. No such luck this time, suckas. We're building this form from scratch!
OK, not quite from scratch. We're still going to take advantage of the built-in FormHelper
in Rails. Let's check out the documentation.
[T]o create a new person you typically set up a new instance of Person in the PeopleController#new action, @person, and in the view template pass that object to form_for:
<%= form_for @person do |f| %>
<%= f.label :first_name %>:
<%= f.text_field :first_name %><br />
<%= f.label :last_name %>:
<%= f.text_field :last_name %><br />
<%= f.submit %>
<% end %>
The HTML generated for this would be (modulus formatting):
<form action="/people" class="new_person" id="new_person" method="post">
<div style="display:none">
<input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
</div>
<label for="person_first_name">First name</label>:
<input id="person_first_name" name="person[first_name]" type="text" /><br />
<label for="person_last_name">Last name</label>:
<input id="person_last_name" name="person[last_name]" type="text" /><br />
<input name="commit" type="submit" value="Create Person" />
</form>
OK, let's look at that a step at a time. We want to create a form for the object referenced by the @post
instance variable, defined in PostController
.
app/views/posts/new.html.erb
<% form_for @post do |f| %>
<% end %>
Let's pull up http://localhost:3000/posts/new.
No route matches [GET] "/posts/new"
Whoops! Since we didn't use a generator, we don't have the route for that URL. Let's edit config/routes.rb
and add a resource route for posts. A resource route maps HTTP verbs to controller actions automatically.
resources :posts
Now if we pull it up in the browser, we will see a page with our standard layout, but the main content area, where are form is, appears entry. If we view the source, we'll see the HTML that got generated. I've reformatted it here:
<form accept-charset="UTF-8" action="/posts" class="new_post" id="new_post" method="post">
<div style="display:none"><input name="utf8" type="hidden" value="✓" />
<input name="authenticity_token" type="hidden" value="7b2QwRWDjS0s/lp2L9RDVMRG2tvckrVf1ri6ANNRBSo=" />
</div>
</form>
As you can see, invoking that one helper gives us everything we need for the form itself, albeit without any fields that take user input. Let's break it down.
<form accept-charset="UTF-8" action="/posts" class="new_post" id="new_post" method="post">
</form>
Here we have the form element itself, minus its contents. The form has a number of attributes with values that the helper generated automatically.
There are a variety of standards to encode letters, numerals, and other characters on a computer. accept-charset="UTF-8"
specifies that the UTF-8 character encoding should be used in the form submission. UTF-8 can encode every Unicode character and is backwards-compatibile with the (much) older ASCII standard, which was the most common on the Web until December 2007. More on this encoding in a moment, but the bottom line is that you don't have to worry about that encoding in your form when you use the helper.
From the action
, class
, and id
attributes, you can see that the helper has figured out that this form is meant to create new form records. That's because we passed it the @post
instance variable. Had that variable contained an existing post, rather than a new one, the helper would have made this an edit_post form, even changing the URL/action accordingly. We'll see this in action soon.
We always want to use the POST HTTP method when creating new records. The helper takes care of that.
Moving on the the elements inside the form:
<div style="display:none">
</div>
The hidden form fields are wrapped in a div
element with inline styles to ensure that it doesn't display. Seems a bit odd, doesn't it?
In the HTML 4 and XHTML Strict standards, it was officially invalid for form fields—including hidden field—to appear directly inside a form without some container element (a div
or a fieldset
, for example) in between. It never made a ton of sense, and HTML5 has removed that rule, so this is likely to disappear from Rails at some point.
<input name="utf8" type="hidden" value="✓" />
I promised we'd talk more about UTF-8! We already specified that the form should use that encoding, but actually submitting a non-ASCII character (✓, in this case) helps to guarantee that encoding it in browsers that do not correctly handle the accept-charset
attribute.
<input name="authenticity_token" type="hidden" value="7b2QwRWDjS0s/lp2L9RDVMRG2tvckrVf1ri6ANNRBSo=" />
The authenticity token, whose value is randomly generated for every session, serves to prevent a type of attack known as Cross Site Request Forgery, or CSRF. In short, it prevents other sites from illicitly creating, modifying, or deleting records from your app via form submission—so long as you aren't allowing such actions to be triggered via GET requests. If you're following the recommendations here, you can be sure of that.
So that's all very nifty, but we can't create posts without input fields. There's not even a submit button, for crying out loud!
Notice the form helper yields an object that we've called f
. This is a FormBuilder
(ActionView::Helpers::FormBuilder
) object. We can use this object to generate fields associated with our Post
model.
We could have named f
whatever we wanted. You'll definitely want to be more specific when you have more than one form in a view. post_form
is a good name. We'll stick with f
for now. Just know that it's a name that we assigned.
Let's check it out in a pry
session. In our view, add a line to enter pry
while the FormBuilder
object is in scope.
app/views/posts/new.html.erb
<%= form_for @post do |f| %>
<% binding.pry %>
<% end %>
Let's load the page in our browser so that we hit the binding.pry
statement. Then look in the Terminal tab containing your server output.
From: /Users/dstrus/elevenfifty/bluit/app/views/posts/new.html.erb @ line 2 ActionView::CompiledTemplates#_app_views_posts_new_html_erb___1954854343935055900_70325242377140:
1: <%= form_for @post do |f| %>
=> 2: <% binding.pry %>
3: <% end %>
[1] pry(#<#<Class:0x007febbe3862b8>>)>
Let's find out a little about the object assigned to f
.
[1] pry(#<#<Class:0x007febbe3862b8>>)> f.class
=> ActionView::Helpers::FormBuilder
As we expected, it's an instance of ActionView::Helpers::FormBuilder
. From the documentation, we see that we can generate the HTML for various elements we may need in our form. To return an input for title
, it's as simple as this:
[2] pry(#<#<Class:0x007febbe3862b8>>)> f.text_field :title
=> "<input id=\"post_title\" name=\"post[title]\" type=\"text\" />"
We just call f.text_field
and pass the name of the attribute as a symbol. The resulting input has an appropriate ID and a control name that will submit the form variable in an ideal, Rails-friendly way. We'll inspect this closer a little later.
We knew that a post has an attribute called title. What if we pass an arbitrary symbol to f.text_field
?
[3] pry(#<#<Class:0x007febbe3862b8>>)> f.text_field :superpower
NoMethodError: undefined method `superpower' for #<Post:0x007febbac2f788>
from /Users/dstrus/.rvm/gems/ruby-2.2.3/gems/activemodel-4.1.6/lib/active_model/attribute_methods.rb:435:in `method_missing'
It explodes, just as we'd hope. So now we know that text_field
isn't just taking that symbol and dropping it in as a string, without any further thought. It is, in fact, sending that symbol as a message to our @post
object.
If we call @post.title
, the return value is nil
.
[5] pry(#<#<Class:0x007febbe3862b8>>)> @post.title
=> nil
If, on the other hand, we call @post.superpower
, we get an error identical to the one we got from the helper.
[6] pry(#<#<Class:0x007febbe3862b8>>)> @post.superpower
NoMethodError: undefined method `superpower' for #<Post:0x007febbac2f788>
from /Users/dstrus/.rvm/gems/ruby-2.2.3/gems/activemodel-4.1.6/lib/active_model/attribute_methods.rb:435:in `method_missing'
So there's clearly more going on in text_field
than meets the eye.
The link
attribute is very similar to title
. Let's see how that looks.
[7] pry(#<#<Class:0x007febbe3862b8>>)> f.text_field :link
=> "<input id=\"post_link\" name=\"post[link]\" type=\"text\" />"
The output from the helper is indeed very similar. You may recall that title
and link
both have limited lengths at the database level. The helper isn't adding anything on the client side to keep users from submitting values that extend past those limits. We'll have to deal with that ourselves in a bit.
Next, let's think about the body
attribute. Unlike title
and link
, the body
column in the database is defined as a text
column, which can hold unlimited data as far as the database is concerned. A simple text input might not be the best form field for this one.
text_field
was used in the example in the documentation. What are our other choices?
The relevant note in the documentation reads:
The standard set of helper methods for form building are located in the
field_helpers
class attribute.
Notice that field_helpers
is a class attribute. That means that calling it on @post
(an instance) won't do us any good.
[12] pry(#<#<Class:0x007febbe3862b8>>)> @post.field_helpers
NoMethodError: undefined method `field_helpers' for #<Post:0x007febbac2f788>
from /Users/dstrus/.rvm/gems/ruby-2.2.3/gems/activemodel-4.1.6/lib/active_model/attribute_methods.rb:435:in `method_missing'
In fact, we need to call it on the FormBuilder
class itself.
[13] pry(#<#<Class:0x007febbe3862b8>>)> ActionView::Helpers::FormBuilder.field_helpers
=> [:fields_for,
:label,
:text_field,
:password_field,
:hidden_field,
:file_field,
:text_area,
:check_box,
:radio_button,
:color_field,
:search_field,
:telephone_field,
:phone_field,
:date_field,
:time_field,
:datetime_field,
:datetime_local_field,
:month_field,
:week_field,
:url_field,
:email_field,
:number_field,
:range_field]
We have all kinds of choices! The HTML element usually employed for long text fields is textarea
. We see that text_area
is one of the helpers available to us. Let's give that on a shot.
[14] pry(#<#<Class:0x007febbe3862b8>>)> f.text_area :body
=> "<textarea id=\"post_body\" name=\"post[body]\">\n</textarea>"
Bingo!
Let's check it out in a browser now. First, we need to exit pry
with ctrl+d
or exit
. This allows our server to finish rendering the page and ready to handle requests again. Mind you, the page will still appear blank, as we didn't do anything in pry to change it.
Let's edit our view to include those helpers, and to remove the binding.pry
call.
<%= form_for @post do |f| %>
<%= f.text_field :title %>
<%= f.text_field :link %>
<%= f.text_area :body %>
<% end %>
Now save and refresh the page.
We now have three fields, right in a row, with no labels. Hey, at least it's something, right?
Let's add some labels. You may have noticed that label
is one of the field helpers available to us. Feel free to play around with the label
helper in pry to see how it behaves.
To add labels to our form, we simply need to pass the name of our attribute to the label
helper, just as we did the text_field
and text_area
helpers.
Let's also go ahead and put each label/input combination in a fieldset
.
<%= form_for @post do |f| %>
<fieldset>
<%= f.label :title %>
<%= f.text_field :title %>
</fieldset>
<fieldset>
<%= f.label :link %>
<%= f.text_field :link %>
</fieldset>
<fieldset>
<%= f.label :body %>
<%= f.text_area :body %>
</fieldset>
<% end %>
It's still very plain, but functional. Well, except for the lack of a submit button. In the documentation for FormBuilder
, we can find an instance method called submit
that does what we want.
Add the submit button for the given form. When no value is given, it checks if the object is a new resource or not to create the proper label.
Let's add a submit button to the bottom of our form.
<fieldset>
<%= f.label :body %>
<%= f.text_area :body %>
</fieldset>
<%= f.submit %>
Hey, it even has the smarts to label the button "Create Post", simply by inspecting the object (@post
, in this case) associated with our FormBuilder
object.
While the form isn't going to win any beauty pageants at this point, it should do the trick. What happens will we fill it out and submit it?
Unknown action
The action 'create' could not be found for PostsController
Aw, snap. It looks like we have an appropriate route, but no corresponding action in our controller. Let's add a create action in PostsController
and simply throw in a binding.pry
.
app/controllers/posts_controller.rb
def create
binding.pry
end
Now let's refresh the page, when you are asked to confirm form resubmission, choose Continue. We do, in fact, want to resubmit the form now that we've actually got something to submit it to.
In our server output, we see that we've hit pry
.
From: /Users/dstrus/elevenfifty/bluit/app/controllers/posts_controller.rb @ line 12 PostsController#create:
11: def create
=> 12: binding.pry
13: end
[1] pry(#<PostsController>)>
Let's check out the data that got submitted by calling params
.
[1] pry(#<PostsController>)> params
=> {"utf8"=>"✓",
"authenticity_token"=>"7b2QwRWDjS0s/lp2L9RDVMRG2tvckrVf1ri6ANNRBSo=",
"post"=>
{"title"=>"My Post", "link"=>"http://google.com", "body"=>"Let me Google that for you!"},
"commit"=>"Create Post",
"action"=>"create",
"controller"=>"posts"}
Params returns a hash that uses strings as its keys. We see the values for the hidden fields "utf8"
and "authenticity_token"
. We don't need to do anything with "utf8"
, as it just forced the browser to encode our data correctly. Rails, meanwhile, has handled our "authenticity_token"
automatically.
Rails also added parameters for "controller"
and "action"
, which can come in handy at times. "commit"
, meanwhile, was the name of our submit button, which we could see by viewing the source on the rendered form.
What we're really interested in is in "post"
, which itself contains a hash.
[3] pry(#<PostsController>)> params['post']
=> {"title"=>"My Post", "link"=>"http://google.com", "body"=>"Let me Google that for you!"}
In fact, that hash looks an awful lot like the sort of hash we would pass to set the values in a Post
object. Let's instantiate one.
[5] pry(#<PostsController>)> Post.new params['post']
ActiveModel::ForbiddenAttributesError: ActiveModel::ForbiddenAttributesError
from /Users/dstrus/.rvm/gems/ruby-2.2.3/gems/activemodel-4.1.6/lib/active_model/forbidden_attributes_protection.rb:21:in `sanitize_for_mass_assignment'
FAIL.
We've gotten a ForbiddenAttributesError
. Rails 4 will not allow us to mass-assign param values to an object. We have to explicitly whitelist those attributes we wish to allow. This feature is called Strong Parameters, and it was previously available as a gem for Rails 3.
What this means for us right now, is that we have to explicitly permit the attributes we want to set. Here's how we do it:
post = Post.new(params.require(:post).permit(:title, :link, :body))
For params
, we are requiring that post
be present. Within the post
hash, we are permitting (but not requiring) title
, link
, and body
.
If one or more of the three permitted attributes aren't found in params["post"], it's not a problem. The others will be set appropriately. If params["post"] isn't found at all, however, Rails will trigger a 400 Bad Request
HTTP response. If a form is going to send any paramaters to this action, they must include the key "post"
.
Let's give it a shot in pry
.
[9] pry(#<PostsController>)> post = Post.new(params.require(:post).permit(:title, :link, :body))
=> #<Post id: nil, title: "My Post", link: "http://google.com", body: "Let me Google that for you!", created_at: nil, updated_at: nil>
[10] pry(#<PostsController>)>
Yeah! We have a Post
object with values for the three attributes we submitted in our form. We can even save it right from pry
.
[10] pry(#<PostsController>)> post.save
(7.0ms) BEGIN
SQL (3.7ms) INSERT INTO "posts" ("body", "created_at", "link", "title", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["body", "Let me Google that for you!"], ["created_at", "2014-10-31 20:46:27.529626"], ["link", "http://google.com"], ["title", "My Post"], ["updated_at", "2014-10-31 20:46:27.529626"]]
(1.5ms) COMMIT
=> true
Very good. Now let's quit pry
(Ctrl+D
) and see what happens.
Template is missing
Missing template posts/create, application/create with {:locale=>[:en], :formats=>[:html], :variants=>[], :handlers=>[:erb, :builder, :raw, :ruby, :coffee, :jbuilder]}. Searched in: * "/Users/dstrus/elevenfifty/bluit/app/views"
We don't have a view to render. So let's be sure to fix that when we take what we learned in pry
and apply it in the controller.
app/controllers/posts_controller.rb
def create
post = Post.new(params.require(:post).permit(:title, :link, :body))
post.save
end
In the create
method, we instantiate the new post just as we did in the console. Then we save the new record. If we stopped there, the record would save, but we would still get an error in the browser. We could create a new view for this action, but that doesn't really make a whole lot of sense. Let's redirect to our posts#index
page instead. We will use ActionController
's redirect_to
method.
app/controllers/posts_controller.rb
def create
post = Post.new(params.require(:post).permit(:title, :link, :body))
post.save
redirect_to '/posts/'
end
That will work, but hardcoding the path to a page within our app isn't a great idea. We should take advantage of routing, and use a path helper or a URL helper.
Since we used a resource route for posts...
config/routes.rb
resources :posts
...we have routes for the basic CRUD operations, each with its own path and URL helpers. Path helpers return a root-relative path, such as /posts/new
. URL helpers return a full URL, such as http://localhost:3000/posts/new
. For a redirect within our app, a path helper will do just fine. If we were, for example, sending an email containing the link, we would need to use the full URL.
app/controllers/posts_controller.rb
def create
post = Post.new(params.require(:post).permit(:title, :link, :body))
post.save
redirect_to posts_path
end
Let's browse to /posts/new
again and try to submit a new post. If all goes well, we should end up back on /posts/
, and our new post should be listed.
That's progress! Let commit our changes before we go any further. Don't forget to check your git status, and maybe even git diff, to make sure you're committing what you expect.
$ git add .
$ git commit -m "Create posts."