We’ve created sample articles from the console, but that isn’t a viable long-term solution. The users of our app will expect to add content through a web interface. In this iteration we’ll create an HTML form to submit the article, then all the backend processing to get it into the database.
Previously, we set up the
resources :articles route in
routes.rb, and that told Rails that we were going to follow the RESTful conventions for this model named Article. Following this convention, the URL for creating a new article would be http://localhost:3000/articles/new. From the articles index, click your “Create a New Article” link and it should go to this path.
Then you’ll see an “Unknown Action” error. The router went looking for an action named
new inside the
ArticlesController and didn’t find it.
First let’s create that action. Open
app/controllers/articles_controller.rb and add this method, making sure it’s inside the
ArticlesController class, but outside the existing
With that defined, refresh your browser and you should get the “Template is Missing” error.
Create a new file
app/views/articles/new.html.erb with these contents:
Refresh your browser and you should just see the heading “Create a New Article”.
It’s not very impressive so far – we need to add a form to the
new.html.erb so the user can enter in the article title and body. Because we’re following the RESTful conventions, Rails can take care of many of the details. Inside that
erb file, enter this code below your header:
What is all that? Let’s look at it piece by piece:
form_foris a Rails helper method which takes one parameter, in this case
@articleand a block with the form fields. The first line basically says “Create a form for the object named
@article, refer to the form by the name
fand add the following elements to the form…”
f.labelhelper creates an HTML label for a field. This is good usability practice and will have some other benefits for us later
f.text_fieldhelper creates a single-line text box named
f.text_areahelper creates a multi-line text box named
f.submithelper creates a button labeled “Create”
Refresh your browser and you’ll see this:
Huh? We didn’t call a method
We didn’t explicitly, but the
model_name method is called by
form_for. What’s happening here is that we’re passing
form_for. Since we haven’t created an
@article in this action, the variable just holds
form_for method calls
nil, generating the error above.
Rails uses some of the reflection techniques that we talked about earlier in order to set up the form. Remember in the console when we called
Article.new to see what fields an
Article has? Rails wants to do the same thing, but we need to create the blank object for it. Go into your
articles_controller.rb, and inside the
new method, add this line:
Then refresh your browser and your form should come up. Enter in a title, some body text, and click Create Article.
Your old friend pops up again…
We accessed the
new action to load the form, but Rails’ interpretation of REST uses a second action named
create to process the data from that form. Inside your
articles_controller.rb add this method (again, inside the
ArticlesContoller class, but outside the other methods):
Check the console’s output of
rail server and you’ll see the “Template is Missing” error.
When you click the “Create” button, what would you expect to happen? Most web applications would process the data submitted then show you the object. In this case, display the article.
We already have an action and template for displaying an article, the
show, so there’s no sense in creating another template to do the same thing.
Before we can send the client to the
show, let’s process the data. The data from the form will be accesible through the
To check out the structure and content of
params, I like to use this trick:
fail method will halt the request allowing you to examine the request
Refresh/resubmit the page in your browser.
The page will say “RuntimeError”.
Below the error information is the request information. We are interested
in the parameters (I’ve inserted line breaks for readability):
What are all those? We see the
} on the outside, representing a
Hash. Within the hash we see keys:
utf8: This meaningless checkmark is a hack to force Internet Explorer to submit the form using UTF-8. Read more on StackOverflow
authenticity_token: Rails has some built-in security mechanisms to resist “cross-site request forgery”. Basically, this value proves that the client fetched the form from your site before submitting the data.
article: Points to a nested hash with the data from the form itself
title: The title from the form
body: The body from the form
commit: This key holds the text of the button they clicked. From the server side, clicking a “Save” or “Cancel” button looks exactly the same except for this parameter.
action: Which controller action is being activated for this request
controller: Which controller class is being activated for this request
Now that we’ve seen the structure, we can access the form data to mimic the way
we created sample objects in the console. In the
create action, remove the
fail instruction and, instead, try this:
If you check the console’s output of
rails server you’ll still get the template error. Add one more line to the action, the redirect:
Refresh the page and you should go to the show for your new article. (NOTE: You’ve now created the same sample article twice)
show page has the title, but where’s the body? Add a line to the
create action to pull out the
:body key from the
params hash and store it into
Then try it again in your browser. Both the
body should show up properly.
Controllers are middlemen in the MVC framework. They should know as little as necessary about the other components to get the job done. This controller action knows too much about our model.
To clean it up, let me first show you a second way to create an instance of
Article. You can call
new and pass it a hash of attributes, like this:
Try that in your app, if you like, and it’ll work just fine.
But look at what we’re doing.
params gives us back a hash,
params[:article] gives us back the nested hash, and
params[:article][:title] gives us the string from the form. We’re hopping into
params[:article] to pull its data out and stick it right back into a hash with the same keys/structure.
There’s no point in that! Instead, just pass the whole hash:
Test and you’ll find that it… blows up! What gives?
For security reasons, it’s not a good idea to blindly save parameters
sent into us via the params hash. Luckily, Rails gives us a feature
to deal with this situation: Strong Parameters.
It works like this: You use two new methods,
They help you declare which attributes you’d like to accept. Add the below code to the bottom of your
Now, you’ll then use this method instead of the
params hash directly:
Go ahead and add this helper method to your code, and change the arguments to
new. It should look like this, in your articles_controller.rb file, when you’re done:
We can then re-use this method any other time we want to make an
We can create articles and we can display them, but when we eventually deliver this to less perfect people than us, they’re going to make mistakes. There’s no way to remove an article, let’s add that next.
We could put delete links on the index page, but instead let’s add them to the
show.html.erb template. Let’s figure out how to create the link.
We’ll start with the
link_to helper, and we want it to say the word “delete” on the link. So that’d be:
But what should
some_path be? Look at the routes table with
rake routes. The
destroy action will be the last row, but it has no name in the left column. In this table the names “trickle down,” so look up two lines and you’ll see the name
The helper method for the destroy-triggering route is
article_path. It needs to know which article to delete since there’s an
:id in the path, so our link will look like this:
Go to your browser, load the show page, click the link, and observe what happens.
Why isn’t the article being deleted? If you look at the server’s console, this is the response to our link clicking:
Compare that to what we see in the routes table:
"articles/3" matches the route pattern
articles/:id, but look at the verb. The server is seeing a
GET request, but the route needs a
DELETE verb. How do we make our link trigger a
You can’t, exactly. While most browsers support all four verbs,
DELETE, HTML links are always
GET, and HTML forms only support
POST. So what are we to do?
Rails’ solution to this problem is to fake a
DELETE verb. In your view template, you can add another attribute to the link like this:
DELETE. Try it in your browser.
Now that the router is recognizing our click as a delete, we need the action. The HTTP verb is
DELETE, but the Rails method is
destroy, which is a bit confusing.
Let’s define the
destroy method in our
ArticlesController so it:
params[:id]to find the article in the database
.destroyon that object
- Redirects to the articles index page
— Do that now on your own and test it —.
There’s one more parameter you might want to add to your
link_to call in your
Sometimes we don’t want to destroy an entire object, we just want to make some changes. We need an edit workflow.
In the same way that we used
new to display the form and
create to process that form’s data, we’ll use
edit to display the edit form and
update to save the changes.
show.html.erb, let’s add this:
edit_article route and pass in the
@article object. Try it!
The router is expecting to find an action in
edit, so let’s add this:
edit action does is find the object and display the form. Refresh and you’ll see the template missing error.
Create a file
app/views/articles/edit.html.erb but hold on before you type anything. Below is what the edit form would look like:
In the Ruby community there is a mantra of “Don’t Repeat Yourself” – but that’s exactly what I’ve done here. This view is basically the same as the
new.html.erb – the only change is the H1. We can abstract this form into a single file called a partial, then reference this partial from both
Partials are a way of packaging reusable view template code. We’ll pull the common parts out from the form into the partial, then render that partial from both the new template and the edit template.
Create a file
app/views/articles/_form.html.erb and, yes, it has to have the underscore at the beginning of the filename. Partials always start with an underscore.
app/views/articles/new.html.erb and CUT all the text from and including the
form_for line all the way to its
end. The only thing left will be your H1 line.
Add the following code to that view:
Now go back to the
_form.html.erb and paste the code from your clipboard.
Then look at your
edit.html.erb file. You already have an H1 header, so add the line which renders the partial.
Go back to your articles list and try creating a new article – it should work just fine. Try editing an article and you should see the form with the existing article’s data – it works OK until you click “Update Article.”
The router is looking for an action named
update. Just like the
new action sends its form data to the
create action, the
edit action sends its form data to the
update action. In fact, within our
update method will look very similar to
The only new bit here is the
update method. It’s very similar to
Article.new where you can pass in the hash of form data. It changes the values in the object to match the values submitted with the form. One difference from
new is that
update automatically saves the changes.
We use the same
article_params method as before so that we only
update the attributes we’re allowed to.
Now try editing and saving some of your articles.
Our operations are working, but it would be nice if we gave the user some kind of status message about what took place. When we create an article the message might say “Article ‘the-article-title’ was created”, or “Article ‘the-article-title’ was removed” for the remove action. We can accomplish this with the
The controller provides you with accessors to interact with the
flash object. Calling
flash.notice will fetch a value, and
flash.notice = "Your Message" will store the string into it.
Let’s look first at the
update method we just worked on. It currently looks like this:
We can add a flash message by inserting one line:
Try editing and saving an article through your browser. Does anything show up?
We need to add the flash messages to our view templates. The
update method redirects to the
show, so we could just add the display to our show template.
However, we will use the flash object in many actions of the application. Most of the time, it’s preferred to add it to our layout.
If you look in
app/views/layouts/application.html.erb you’ll find what is called the “application layout”. A layout is used to wrap multiple view templates in your application. You can create layouts specific to each controller, but most often we’ll just use one layout that wraps every view template in the application.
Looking at the default layout, you’ll see this:
yield is where the view template content will be injected. Just above that yield, let’s display the flash message by adding this:
This outputs the value stored in the
flash object in the attribute
With the layout modified, try changing your article, clicking save, and you should see the flash message appear at the top of the
Typical controllers will set flash messages in the
destroy actions. Insert messages into the latter two actions now.
Test out each action/flash messages, then you’re done with I1.
It’s annoying me that we keep going to
http://localhost:3000/ and seeing the Rails starter page. Let’s make the root show our articles index page.
config/routes.rb and right above the other routes (in this example, right above
resources :articles) add in this one:
http://localhost:3000 and you should see your article list.
The form-based workflow is complete, and it is common to commit and push changes after each feature. Go ahead and add/commit/push it up to GitHub:
If you are not happy with the code changes you have implemented in this iteration, you don’t have to throw the whole project away and restart it. You can use Git’s reset command to roll back to your first commit, and retry this iteration from there. To do so, in your terminal, type in: