By using my site you consent to my use of cookies. Learn More.

15 November, 2016

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

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: 'cornholio@example.org',
        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: