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 name f 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 named title
  • The f.text_area helper creates a multi-line text box named body
  • 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 C:/laragon/www/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 Article.

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

Check the console’s output of rail server and you’ll see the “Template is Missing” error.

Started POST "/articles" for 127.0.0.1 at 2017-06-29 08:33:25 +0000
Processing by ArticlesController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"M963YLZDZcyPK+DBNIstpbJltZnwtr2YekPEknmhKBjWNykECZMQSKeGW2ym78o+5YGECq+2ZsUHDvcsaTRVnw==", "article"=>{"title"=>"Fourth Sample", "body"=>"This is my fourth sample article."}, "commit"=>"Create Article"}
No template found for ArticlesController#create, rendering head :no_content
Completed 204 No Content in 401ms (ActiveRecord: 0.0ms)

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"=>"YR0R1oyoDj0dVwqIX4bK6Ldi9Ywp4A0rOTFAH+dlvOWE9I+yM3h7uTX6sSXN4i1z4IbEH3bg1nZEfHOh9/DBYg==",
"article"=>{"title"=>"Fourth Sample", "body"=>"This is my fourth sample article."},
"commit"=>"Create Article"}
Toggle session dump

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

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 check the console’s output of rails server 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?

ActiveModel::ForbiddenAttributesError in ArticlesController#create
ActiveModel::ForbiddenAttributesError

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. Add the below code to the bottom of your articles_controller.rb.

private
def article_params
params.require(:article).permit(:title, :body)
end

Now, you’ll 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
#...
def create
@article = Article.new(article_params)
@article.save
redirect_to article_path(@article)
end
private
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’s console, this is the response to our link clicking:

Started GET "/articles/6" for 127.0.0.1 at 2017-06-29 09:05:24 +0000
Processing by ArticlesController#show as HTML
Parameters: {"id"=>"6"}
Article Load (0.0ms) SELECT "articles".* FROM "articles" WHERE "articles"."id" = ? LIMIT ? [["id", 6], ["LIMIT", 1]]
Rendering articles/show.html.erb within layouts/application
Rendered articles/show.html.erb within layouts/application (1.0ms)
Completed 200 OK in 67ms (Views: 56.5ms | ActiveRecord: 0.0ms)

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:

  1. Uses params[:id] to find the article in the database
  2. Calls .destroy on that object
  3. 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