I1: Form-based Workflow
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.
Creating the NEW Action and View
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 index
and show
methods:
def new
end
Starting the Template
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:
<h1>Create a New Article</h1>
Refresh your browser and you should just see the heading "Create a New Article".
Writing a Form
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:
<%= form_for(@article) do |f| %>
<ul>
<% @article.errors.full_messages.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
What is all that? Let's look at it piece by piece:
form_for
is a Rails helper method which takes one parameter, in this case@article
and 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 namef
and add the following elements to the form..."- The
f.label
helper creates an HTML label for a field. This is good usability practice and will have some other benefits for us later - The
f.text_field
helper creates a single-line text box namedtitle
- The
f.text_area
helper creates a multi-line text box namedbody
- The
f.submit
helper creates a button labeled "Create"
Does it Work?
Refresh your browser and you'll see this:
ArgumentError in Articles#new
Showing /Users/you/projects/blogger/app/views/articles/new.html.erb where line #2 raised:
First argument in form cannot contain nil or be empty
Huh? We didn't call a method model_name
?
We didn't explicitly, but the model_name
method is called by form_for
. What's happening here is that we're passing @article
to form_for
. Since we haven't created an @article
in this action, the variable just holds nil
. The form_for
method calls model_name
on nil
, generating the error above.
Setting up for Reflection
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:
@article = Article.new
Then refresh your browser and your form should come up. Enter in a title, some body text, and click CREATE.
The create
Action
Your old friend pops up again...
Unknown action
The action 'create' could not be found for ArticlesController
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):
def create
end
Refresh the page and you'll get the "Template is Missing" error.
We Don't Always Need Templates
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.
Processing the Data
Before we can send the client to the show
, let's process the data. The data from the form will be accesible through the params
method.
To check out the structure and content of params
, I like to use this trick:
def create
fail
end
The fail
method will halt the request allowing you to examine the request
parameters.
Refresh/resubmit the page in your browser.
Understanding Form Parameters
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):
{"utf8"=>"✔", "authenticity_token"=>"UDbJdVIJjK+qim3m3N9qtZZKgSI0053S7N8OkoCmDjA=",
"article"=>{"title"=>"Fourth Sample", "body"=>"This is my fourth sample article."},
"commit"=>"Create", "action"=>"create", "controller"=>"articles"}
What are all those? We see the {
and }
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 StackOverflowauthenticity_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 itselftitle
: The title from the formbody
: 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 requestcontroller
: Which controller class is being activated for this request
Pulling Out Form Data
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:
def create
@article = Article.new
@article.title = params[:article][:title]
@article.save
end
If you refresh the page in your browser you'll still get the template error. Add one more line to the action, the redirect:
redirect_to article_path(@article)
Refresh the page and you should go to the show for your new article. (NOTE: You've now created the same sample article twice)
More Body
The 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 @article
.
Then try it again in your browser. Both the title
and body
should show up properly.
Fragile Controllers
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:
def create
@article = Article.new(
title: params[:article][:title],
body: params[:article][:body])
@article.save
redirect_to article_path(@article)
end
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:
def create
@article = Article.new(params[:article])
@article.save
redirect_to article_path(@article)
end
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, require
and permit
.
They help you declare which attributes you'd like to accept. Most of
the time, they're used in a helper method. Add the below code to app/helpers/articles_helper.rb
.
def article_params
params.require(:article).permit(:title, :body)
end
Now on your articles_controller.rb add: 'include ArticlesHelper' directly below your class name.
You then use this method instead of the params
hash directly:
@article = Article.new(article_params)
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:
class ArticlesController < ApplicationController
include ArticlesHelper
#...
def create
@article = Article.new(article_params)
@article.save
redirect_to article_path(@article)
end
Now in your articles_helper.rb file it should look like this:
module ArticlesHelper
def article_params
params.require(:article).permit(:title, :body)
end
end
We can then re-use this method any other time we want to make an
Article
.
Deleting Articles
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:
<%= link_to "delete", some_path %>
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 article
.
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:
<%= link_to "delete", article_path(@article) %>
Go to your browser, load the show page, click the link, and observe what happens.
REST is about Path and Verb
Why isn't the article being deleted? If you look at the server window, this is the response to our link clicking:
Started GET "/articles/3" for 127.0.0.1 at 2012-01-08 13:05:39 -0500
Processing by ArticlesController#show as HTML
Parameters: {"id"=>"3"}
Article Load (0.1ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT 1 [["id", "3"]]
Rendered articles/show.html.erb within layouts/application (5.2ms)
Completed 200 OK in 13ms (Views: 11.2ms | ActiveRecord: 0.3ms)
Compare that to what we see in the routes table:
DELETE /articles/:id(.:format) articles#destroy
The path "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 DELETE
?
You can't, exactly. While most browsers support all four verbs, GET
, PUT
, POST
, and DELETE
, HTML links are always GET
, and HTML forms only support GET
and 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:
<%= link_to "delete", article_path(@article), method: :delete %>
Through some JavaScript tricks, Rails can now pretend that clicking this link triggers a DELETE
. Try it in your browser.
The destroy
Action
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:
- Uses
params[:id]
to find the article in the database - Calls
.destroy
on that object - Redirects to the articles index page
Do that now on your own and test it.
Confirming Deletion
There's one more parameter you might want to add to your link_to
call in your show.html.erb
:
data: {confirm: "Really delete the article?"}
This will pop up a JavaScript dialog when the link is clicked. The Cancel button will stop the request, while the OK button will submit it for deletion.
Creating an Edit Action & View
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.
Adding the Edit Link
Again in show.html.erb
, let's add this:
<%= link_to "edit", edit_article_path(@article) %>
Trigger the edit_article
route and pass in the @article
object. Try it!
Implementing the edit
Action
The router is expecting to find an action in ArticlesController
named edit
, so let's add this:
def edit
@article = Article.find(params[:id])
end
All the edit
action does is find the object and display the form. Refresh and you'll see the template missing error.
An Edit Form
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:
<h1>Edit an Article</h1>
<%= form_for(@article) do |f| %>
<ul>
<% @article.errors.full_messages.each do |error| %>
<li><%= error %></li>
<% end %>
</ul>
<p>
<%= f.label :title %><br />
<%= f.text_field :title %>
</p>
<p>
<%= f.label :body %><br />
<%= f.text_area :body %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
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 new.html.erb
and edit.html.erb
.
Creating a Form Partial
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.
Open your 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:
<%= render partial: 'form' %>
Now go back to the _form.html.erb
and paste the code from your clipboard.
Writing the Edit Template
Then look at your edit.html.erb
file. You already have an H1 header, so add the line which renders the partial.
Testing 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."
Implementing Update
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 articles_controller.rb
, the update
method will look very similar to create
:
def update
@article = Article.find(params[:id])
@article.update(article_params)
redirect_to article_path(@article)
end
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.
Adding a flash message
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 flash
object.
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.
Flash for Update
Let's look first at the update
method we just worked on. It currently looks like this:
def update
@article = Article.find(params[:id])
@article.update(article_params)
redirect_to article_path(@article)
end
We can add a flash message by inserting one line:
def update
@article = Article.find(params[:id])
@article.update(article_params)
flash.notice = "Article '#{@article.title}' Updated!"
redirect_to article_path(@article)
end
Testing the flash messages
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.
Flash messages in the 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:
<!DOCTYPE html>
<html>
<head>
<title>Blogger</title>
<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
The yield
is where the view template content will be injected. Just above that yield, let's display the flash message by adding this:
<p class="flash"><%= flash.notice %></p>
This outputs the value stored in the flash
object in the attribute :notice
.
More Flash Message Testing
With the layout modified, try changing your article, clicking save, and you should see the flash message appear at the top of the show
page.
Adding More Messages
Typical controllers will set flash messages in the update
, create
, and destroy
actions. Insert messages into the latter two actions now.
Test out each action/flash messages, then you're done with I1.
An Aside on the Site Root
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.
Open config/routes.rb
and right above the other routes (in this example, right above resources :articles
) add in this one:
root to: 'articles#index'
Now visit http://localhost:3000
and you should see your article list.
Another Save to GitHub.
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:
$ git add -A
$ git commit -m "form-based workflow feature completed"
$ git push
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:
$ git log
commit 15384dbc144d4cb99dc335ecb1d4608c29c46371
Author: your_name your_email
Date: Thu Apr 11 11:02:57 2013 -0600
first blogger commit
$ git reset --hard 15384dbc144d4cb99dc335ecb1d4608c29c46371