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

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

This is part 1 of my series on building a contact form with Ruby on Rails 5. This part will focus on creating a model for the messages we receive through the form.

The remaining parts of this series can be found at the following links:

There is a Github repo for what we're about to build and it's probably best if you are using Rails 5 like me.

What Are We Going To Do?

The objective is to get a functioning contact form up and running in a Rails 5 app, and we are going to do it in a number of loose steps::

  • build a model to validate any messages we receive
  • build an HTML form and a create controller to manage user interactions
  • configure ActionMailer to forward messages to my private email address

Let's Do This People

First of all we need to get some kind of Rails 5 app on the go:

rails new contact-form

Don't forget to cd into it:

cd contact-form-demo

A Table-less Model

I don't want to save my fan mail to the database, because there won't be enough room for it all, so I need a table-less model. Run the following from the command prompt:

bin/rails g model Message --no-migration --no-fixture

Now open up the newly created Message model, found at app/models/message.rb, and change it from this:

class Message < ApplicationRecord
end

To this:

class Message
end

Ahh, that's better, our model no long inherits from ApplicationRecord, and we're officially riding bareback.

Testing, Testing, 1-2-3

The model generator that we ran earlier also created a test file at test/models/message_test.rb. Open it up, we're going to write some tests.

What should we test first? Well, I'd like each message to have a name, email and body, so let's start off there:

require 'test_helper'

class MessageTest < ActiveSupport::TestCase

  test 'responds to name, email and body' do 
    msg = Message.new

    assert msg.respond_to?(:name),  'does not respond to :name'
    assert msg.respond_to?(:email), 'does not respond to :email'
    assert msg.respond_to?(:body),  'does not respond to :body'
  end
end

Now run the test with the following:

bin/rails test

It fails because we have not defined any attributes on the Message class yet:

Failure:
MessageTest#test_responds_to_name,_email_and_body:
does not respond to :name

We can fix this by adding the following to our model:

class Message
  attr_accessor :name, :email, :body
end

If we run the test again, it should pass.

Congrats, we're well and truly rocking out now. We have a passing test, and we're ready to move on. Let's see what's next.

Assigning Values

What will happen if we try to assign values to our name, email and body attributes? Let's write another test to find out:

require 'test_helper'

class MessageTest < ActiveSupport::TestCase

  # previous test omitted

  test 'should be valid when all attributes are set' do
    attrs = { 
      name: 'stephen',
      email: 'stephen@example.org',
      body: 'kthnxbai'
    }

    msg = Message.new attrs
    assert msg.valid?, 'should be valid'
  end
end

Run the test again, and you'll see the following error message:

bin/rails test:models

Error:
MessageTest#test_should_be_valid_when_all_attributes_are_set:
ArgumentError: wrong number of arguments (given 1, expected 0)

Ouch! What's going on here, and more importantly, how do we fix this?

Emulating ActiveRecord With ActiveModel

When we removed ActiveRecord from our Message class, we lost the ability to instantiate new objects in the following manner:

msg = Message.new({some_attr: 'abc', another_attr: 'xyz'})

We could fix this by copying the initialize method from ActiveRecord::Base and hacking about with it until everything works the way we want, but we don't need to do that. There's an easier way. You see, ActiveRecord has a little friend, called ActiveModel, who will help us out if we ask nicely. Add the following to your message class:

class Message
  include ActiveModel::Model
  attr_accessor :name, :email, :body
end

With this in place, we can run our tests again, and you should find that they're all passing!

The reason this works is because the ActiveModel::Model module has an initialize method that's similar to the one we lost when we removed ActiveRecord, see for yourself:

module ActiveModel
  module Model
    # omitted

    def initialize(params={})
      params.each do |attr, value|
        self.public_send(\"\#{attr}=\", value)
      end if params
    end

    # omitted

  end
end

Great, it's nice to know how things work, so I encourage you to look deeper into the Rails source code any time you feel the need. I think we have enough to move on to the next part, there's still plenty to do.

Don't Send Me No Blank Emails

I'd say that a blank message, with no name, email or body, should be considered invalid, and our test suite should verify that for us. Open up your message test again, and add the following to it:

require 'test_helper'

class MessageTest < ActiveSupport::TestCase
  # previous tests omitted

  test 'name, email and body are required by law' do
    msg = Message.new

    refute msg.valid?, 'Blank Mesage should be invalid'

    assert_match /blank/, msg.errors[:name].to_s
    assert_match /blank/, msg.errors[:email].to_s
    assert_match /blank/, msg.errors[:body].to_s
  end
end

That looks good, we start off by creating a new message object without any attributes, and then we refute its validity, or in other words, we assert that it is invalid. We then assert that each of the missing attributes has an associated error message containing the word blank, as in Name can't be blank.

Our test fails with the following error message:

// some output omitted for brevity

Failure:
MessageTest#test_name,_email_and_body_are_required_by_law:
Blank Mesage should be invalid

Can you see what's wrong? We've created a new message without any attributes, but Rails is failing to flag it as invalid. It does not know that we want to force the user to fill those fields in, so we need a way to convey our expectations to Rails.

Validating Expectations

The validates method comes from the ActiveModel::Model module that we included earlier, and it allows us to verify the state of our message objects. We can use it as follows:

class Message
  include ActiveModel::Model
  attr_accessor :name, :email, :body
  validates :name, :email, :body, presence: true
end

Do you get what's happening here? We've used the presence validator to tell Rails that each message must have a name, email and body in order to be considered valid.

Our tests should all be passing now.

bin/rails test:models

// output omitted

3 runs, 11 assertions, 0 failures, 0 errors, 0 skips

Great, we've completed the first part of our task.

Summary

We've test-driven the Message model, and have some basic validations in place. We're ready to move on to part 2 where we will build the controller and the view for this contact form.

Don't forget that there is a Github repo in case you need to look closer at the code, and the other parts of this tutorial are available at the following links: