Skip to content
Dave Strus edited this page Oct 26, 2015 · 2 revisions

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="&#x2713;" />
    <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="&#x2713;" />

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.

submit(value=nil, options={})

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