Build a Contact Form with Ruby on Rails (part 2)

This is part 2 of my series on building a contact form with Ruby on Rails 5. This part will focus on creating a view and a controller to manage user interactions.
The other parts of this series can be found at the following links:
There is a Github repo for this tutorial, and you should probably read part 1 first before attempting to follow any further.
Quick Recap
In part 1 we created a working Message model, and added a few validations to ensure each message has a name, email and body.
Now we're going to build the controller and the view. so that users can create and send messages to our website. In part 3 we will configure the website to email those messages to my private address.
This will require some patience as we have a lot of work to do. Let's get going.
Start The Generator
Run the following from the command line:
bin/rails g controller messages --no-helper --no-assets
This creates a few files and folders for us.
Let's begin testing the behaviour of the messages controller. Open up test/controllers/messages_controller_test.rb and add the following to it:
require 'test_helper'
class MessagesControllerTest < ActionDispatch::IntegrationTest
test "GET new" do
get new_message_url
assert_response :success
assert_select 'form' do
assert_select 'input[type=text]'
assert_select 'input[type=email]'
assert_select 'textarea'
assert_select 'input[type=submit]'
end
end
end
This tests that we can issue a successful GET request to a route named new_message_url, and that the response contains a form with various input fields.
Run bin/rails test:controllers and see the following error message:
Error:
MessagesControllerTest#test_GET_new:
NameError: undefined local variable or method `new_message_url'
// omitted
This error tells us the route we are trying to hit does not exist yet. So let's sort that out.
Routes, Bloody Routes
Open config/routes.rb and add the following:
Rails.application.routes.draw do
get 'contact-me', to: 'messages#new', as: 'new_message'
end
This creates a route named new_message_url which maps GET requests for http://localhost:3000/contact-me to an action named MessagesController#new.
You should see a different error message when you run the controller test again:
Error:
MessagesControllerTest#test_GET_new:
AbstractController::ActionNotFound: The action 'new' could not be found for MessagesController
This time Rails is telling us our messages controller does not contain a method named 'new'. So let's add the missing action.
Missing In Action
Open app/controllers/messages_controller.rb and add the following:
class MessagesController < ApplicationController
def new
end
end
Our test still fails when we run it, and we get another new error message to chew on:
Error:
MessagesControllerTest#test_GET_new:
ActionController::UnknownFormat: MessagesController#new is missing a template for this request format and variant.
// omitted
Can you guess what's wrong now? The error message tells us Rails can't find a view file for this action, which makes sense because we haven't created one yet. Let's do that next.
Building The View
Create an empty file at app/views/message/new.html.erb:
<%# Empty file: app/views/message/new.html.erb %>
Run the controller test again and you'll see it's still failing, and once more we have a new error message:
Failure:
MessagesControllerTest#test_GET_new:
Expected at least 1 element matching "form", found 0..
Expected 0 to be >= 1.
Great, these error messages are guiding us to where we want to go. This time Rails is complaining because our test expects to find an HTML form when we visit http://localhost:3000/contact-us, but we only have a blank file there right now.
Luckily for us Rails has some handy helper methods for creating HTML forms, as we will see next.
Form Follows Function
Put the following code in the empty file you created at app/views/message/new.html.erb:
<%= form_for @message, url: create_message_url do |f| %>
<%= notice %>
<%= @message.errors.full_messages.join(', ') %>
<%= f.text_field :name, placeholder: 'name' %>
<%= f.email_field :email, placeholder: 'email' %>
<%= f.text_area :body, placeholder: 'body' %>
<%= f.submit 'Send' %>
<% end %>
This creates an HTML form containing some input fields and a submit button. We've also instructed Rails to POST our form to a route named create_message_url when the submit button is clicked.
What do you think will happen when we run our controller test again? That's right, you guessed it, we will see another error message:
Error:
MessagesControllerTest#test_GET_new:
ActionView::Template::Error: undefined local variable or method `create_message_path'
It's similar to an error message we saw earlier on. Rails is telling us that the route we want to POST our form to does not exist yet. So we should open config/routes.rb and add the missing route like so:
Rails.application.routes.draw do
get 'contact-me', to: 'messages#new', as: 'new_message'
post 'contact-me', to: 'messages#create', as: 'create_message'
end
This route specifies that POST requests to http://localhost:3000/contact-me should be handled by an action called MessagesController#create.
If we run our controller test again, we'll find it is still failing, but this time we get the following error message:
Error:
MessagesControllerTest#test_GET_new:
ActionView::Template::Error: First argument in form cannot contain nil or be empty
This is happening because the code we pasted into our view uses an instance variable named @message which we have not defined yet, so its value is nil.
All we need to do in order to fix this is open up our messages controller and define the @message variable, like so:
class MessagesController < ApplicationController
def new
@message = Message.new
end
end
There we go, we've set the @message variable to be a new instance of the Message model we created in part 1 of this series.
Finally our controller test passes when we run it!
bin/rails test:controllers
// omitted
1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
What an absolute carry-on that was, it took a long time to get that test passing, but we got there and we've done some of the heavy lifting, so the rest should be slightly easier.
Try visiting http://localhost:3000/contact-me and behold your shiny new contact form. It doesn't look too great because the inputs are all on the same line, so let's take a minute to do something about that.
A Drop Of CSS
Before moving on it's worth adding some CSS code to app/assets/stylesheets/application.css to make things prettier:
/*
*= require_tree .
*= require_self
*/
input, textarea { display: block; }
Submitting The Form
The curious among you will notice an unknown action error when you hit the submit button, so let's deal with that next.
When a user submits the form, it is posted to the create_message_url route, which maps to the 'create' action of our messages controller. This currently throws an error because the messages controller does not yet contain an action named 'create'.
Let's write another test to specify how we want the 'create' action to work. Open test/controllers/messages_controller_test.rb and add the following:
require 'test_helper'
class MessagesControllerTest < ActionDispatch::IntegrationTest
# previous test omitted
test "POST create" do
post create_message_url, params: {
message: {
name: 'cornholio',
email: '[email protected]',
body: 'hai'
}
}
assert_redirected_to new_message_url
follow_redirect!
assert_match /Message received, thanks!/, response.body
end
end
Let me walk you through the code from the test.
We begin by posting some values to the create_message_url route, which simulates a user clicking the submit button after filling out the form.
Then we check that we are redirected back to the new_message_url route, and that the response body contains the words 'Message received, thanks!'.
Run the controller tests again, and observe the following error message:
Error:
MessagesControllerTest#test_POST_create:
AbstractController::ActionNotFound:
The action 'create' could not be found for MessagesController
Rails can't find an action named 'create' in our messages controller, so let's add one now:
class MessagesController < ApplicationController
# previous method omitted
def create
end
end
Our controller test will still fail because it expects to be redirected to new_message_url after a successful post:
Failure:
MessagesControllerTest#test_POST_create:
Expected response to be a <3XX: redirect>, but was a <204: No Content>
We can fix this with the following addition to our controller action:
class MessagesController < ApplicationController
# previous method omitted
def create
redirect_to new_message_url, notice: "Message received, thanks!"
end
end
The controller test will pass now when we run it, but the 'create' action is far from finished. Try clicking on the submit button without filling in any of the fields to see what I'm getting at.
Handling Blank Submissions
Let's write another test to handle invalid form submissions, as this will allow us to flesh out the rest of the behaviour we want our 'create' action to exhibit.
require 'test_helper'
class MessagesControllerTest < ActionDispatch::IntegrationTest
# previous test omitted
test "invalid POST create" do
post create_message_url, params: {
message: { name: '', email: '', body: '' }
}
assert_match /Name .* blank/, response.body
assert_match /Email .* blank/, response.body
assert_match /Body .* blank/, response.body
end
end
Do you understand what this test is saying? It just tells Rails we want to see some validation error messages on screen when the user submits a blank form.
This test will fail when we run it:
Failure:
MessagesControllerTest#test_invalid_POST_create:
Expected /Name .* blank/ to match "<html><body>You are being <a href=\"http://www.example.com/contact-me\">redirected</a>.</body></html>".
This is happening because all requests to our 'create' action currently result in a redirect to the new_message_url route, so let's modify the 'create' action to fix that:
class MessagesController < ApplicationController
# previous method omitted
def create
message_params = params.require(:message).permit(:name, :email, :body)
@message = Message.new message_params
if @message.valid?
redirect_to new_message_url, notice: "Message received, thanks!"
else
render :new
end
end
end
Woah, I've added a fair bit of code there, so I'll walk you through it.
On the first few lines we sanitize the contents of the submitted form into a local variable called message_params, and use it to construct a Message object named @message. If the Message is valid then we redirect to new_message_url, otherwise we stay inside the 'create' action and render the HTML form again.
When we run our controller tests again we find that they all pass!
bin/rails test:controllers
// omitted
3 runs, 15 assertions, 0 failures, 0 errors, 0 skips
And you should probably make sure that your model tests are still passing too. Mine are.
Red, Green, Refactor
A lot Rails developers would put that message_params local variable in a private method, like so:
class MessagesController < ApplicationController
# previous method omitted
def create
@message = Message.new message_params
if @message.valid?
redirect_to new_message_url, notice: "Message received, thanks!"
else
render :new
end
end
private
def message_params
params.require(:message).permit(:name, :email, :body)
end
end
Whatever makes you happy. Run the tests again to make sure they're still passing.
Summary
Congrats we've achieved quite a lot there. Our view is in place, our controller is quite well tested, and we can move on to part 3 where will learn how to forward received messages to a private email address.
There is a Github repo for this tutorial, and the other parts of the series can be found at the following links: