Will Little Subscribe

How to build a simple email sign up form with Ruby on Rails


Continuing our Introduction to Web & Mobile Development tutorial series, here we'll learn how to build a simple email sign up feature. Unless you have strong business reasons to require users to only sign up via an Oauth provider such as Facebook, Twitter, Google, etc., it's best to at least allow your users to sign up directly with an email address.

[Author's note: I wrote the first couple dozen tutorials in this series a few years ago and I'm in the process of updating the content to reflect the evolution of best practices in the industry. Please comment if you see anything I missed that should be updated. Thanks!]

FIRST, WRITE A SIGN UP FEATURE TEST

Moving forward in good Behavior-Driven Development (BDD) fashion, let's first start creating a test for our expected user behavior:

#spec/features/visitor_signs_up_spec.rb
require 'spec_helper'

feature 'Visitor signs up' do
  scenario 'via email' do
    visit sign_up_path
    expect(page).to have_content("First name")
  end
end

We'll continue adding lines for this test as we go, but for now, go ahead and run it and see what happens:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email (FAILED - 1)

Failures:

1) Visitor signs up via email
     Failure/Error: visit sign_up_path
     NameError:
       undefined local variable or method `sign_up_path' for #<RSpec::ExampleGroups::VisitorSignsUp:0x007fb313b25db8>
     # ./spec/features/visitor_signs_up_spec.rb:5:in `block (2 levels) in <top (required)>'
. . .

OK, here we go. Rspec doesn't know what “sign_up_path” is, so we need to go into our routes.rb file and create that route:

#config/routes.rb
. . .
  # You can have the root of your site routed with "root"
  root 'home#index'
  get 'sign-up' => 'home#sign_up'
. . .

Cool. Run your test again and you'll notice that “The action ‘sign_up' could not be found for HomeController”… So we'll need to jump into the controller and setup the action:

#app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
  def sign_up
  end
end

…and also create a quick app/views/home/sign_up.html.erb file and leave it blank for now. Run your test again:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email (FAILED - 1)

Failures:

  1) Visitor signs up via email
     Failure/Error: expect(page).to have_content("First name")
       expected to find text "First name" in "footer information will go here"
     # ./spec/features/visitor_signs_up_spec.rb:6:in `block (2 levels) in <top (required)>'

Finished in 0.07645 seconds (files took 1.39 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/features/visitor_signs_up_spec.rb:4 # Visitor signs up via email

Alright, now it's time to do a little refactoring and import our markup for the sign up page. You can fire up your “rails s” server and visit http://localhost:3000/sign-up yourself to see the “footer information will go here” text that our test saw. But yes, part of the greatness of TDD/BDD is that you don't really need to fire up your browser yourself very often (best to let the robots do the work when possible).

So, for the first step of our refactoring, let's leave the <div class=”mainContent”> element (with everything inside) in our app/views/home/index.html.erb file and move everything else over to our app/views/layouts/application.html.erb file like so:

Then, inside our app/views/home/sign_up.html.erb file we can copy/paste in the applicable markup:

Visit http://localhost:3000 and you'll see the following:

Now, let's go ahead and simplify this a bit and remove the right column (we'll let the user upload their photo later), change the class of the left column to “centered_col”, move up the submit button, and add a quick style for this class to the bottom of our app/assets/stylesheets/_layout.scss file:

/* app/assets/stylesheets/_layout.scss */
. . .
.centered_col {width: 20em; margin: 0 auto;}

So your new app/views/home/sign_up.html.erb file will look like this:

Next, let's go ahead and update our form to use first_name and last_name (instead of full name, which was a placeholder from our UI designer) and take advantage of Rails' form helpers like so:

Now let's go ahead and run our test again and you should see:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email

Finished in 0.47323 seconds (files took 1.46 seconds to load)
1 example, 0 failures

Woohoo! Now let's add a few more lines to our test to finish up the scope of this part of our tutorial for now:

#spec/features/visitor_signs_up_spec.rb
require 'spec_helper'

feature 'Visitor signs up' do
  scenario 'via email' do
    visit sign_up_path
    expect(page).to have_content("First name")
    fill_in "user_first_name", with: "John"
    fill_in "user_last_name", with: "Doe"
    fill_in "user_email", with: "john@test.com"
    fill_in "user_password", with: "test123"
    fill_in "user_password_confirmation", with: "test123"
    click_button "Create account"
    expect(User.count).to eq(1)
  end
end

Then run the test and you'll see:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email (FAILED - 1)

Failures:

  1) Visitor signs up via email
     Failure/Error: click_button "Create account"
     ActionController::RoutingError:
       No route matches [POST] "/sign-up"

Ah! It shouldn't be posting to “/sign-up”…it should be posting to “/users”. So, let's update our home controller's sign up action to create a new @user object so our view will know that @user is supposed to be a new user:

#app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
  def sign_up
    @user = User.new
  end
end

And then update our mistake in our sign_up form:

<!-- app/views/home/sign_up.html.erb -->
. . .
<%= form_for @user, class: "signupForm" do |f| %>
. . .

Then running our test again we'll see:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email (FAILED - 1)

Failures:

 1) Visitor signs up via email
     Failure/Error: expect(User.count).to eq(1)

       expected: 1
            got: 0

       (compared using ==)
     # ./spec/features/visitor_signs_up_spec.rb:13:in `block (2 levels) in <top (required)>'

Finished in 0.50894 seconds (files took 1.47 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/features/visitor_signs_up_spec.rb:4 # Visitor signs up via email

This means that while the form successfully posted, the user didn't get saved for some reason. Often times you'll be developing a web application like this and aren't sure how to debug the problem, so it's good we have a chance to walk through this together.

In this case, we can add a “puts page.html” to the bottom of our test temporarily and run it again to see what's up, or we can fire up “rails s” and enter some form data ourselves and see what happens. Every developer does it differently. There are even a variety of neat RubyGems available to help the debugging process, but we won't get into those now.

So, what you'll find as you dig into it, is that Rails is scrubbing everything but the first_name parameter when the form is posted. This is because Rails has an integrated feature called Strong Parameters that will strip out any/all parameters that aren't explicit allowed.

To fix this, let's modify the bottom of our users_controller like so:

Let's run our test again and see what happens:

$ rspec ./spec/features/visitor_signs_up_spec.rb

Visitor signs up
  via email

Finished in 0.56326 seconds (files took 1.68 seconds to load)
1 example, 0 failures

Alright! It works! This is a good stopping point for now. We're starting to get deeper into the everyday experience of a web developer here and it's important to reflect on what you just did.

Building forms and utilizing Rails' basic helpers is a big step. There is a fair amount of “magic” going on under the hood, of course, so I'd encourage you to view source and Google around some of the terms/lines you see that you don't understand so you start to better understanding of how it all works. We'll get into it more as we go, but for now…good work. Thanks for following along.

To wrap up, be sure to “git add .” and “git commit -m ‘Adding sign up form'” to make your commit. Following this with a “git push” will push it up to your remote repository for safe keeping.

- — -

Next up in our series we will add validations to our form so we make sure people enter all the fields, correct format emails, enter a strong enough password, and maybe for kicks we'll add a CAPTCHA to help prevent your app from getting nailed by automated bots. Previous post: How to merge HTML, CSS, and JavaScript into your Rails app.