Learn Ruby on Rails Book

Adding signup feature


Let's implement user sign-up functionality to our application. With this feature, new users can sign-up via UI and they will be able to handle their tasks via Dashboard.

These are the goals we are trying to achieve in this chapter:

  • We need an API for creating users with details such as name, email, and password.
  • We need a new page showing a form having fields: name, email, password, and password confirmation. The form should call the user creation API with the input data upon submission. User signup page
  • Users should be able to see a toast message when the form submission is successful. Signup Success Toastr

Technical design

To implement user sign-up, we will have to do the following:

  • From the create action in UsersController, we will create an instance of User from the request data and save it to the database.
  • If saving is successful, we will respond with JSON data containing the key called notice. Since we had already implemented the Axios interceptors, it will take care of showing the contents from the notice key in response as a Toastr notification.
  • We will create a new API connector, auth.js for calling sign-up and other authentication-related APIs.
  • We will create a new frontend page Signup for taking user credentials and sending them via API. We will use the /signup route to display that page.
  • To make things more modular, we can accept these credentials from a separate component called SignupForm.

Adding create action

Open app/controllers/users_controller.rb file and add the following content:

1class UsersController < ApplicationController
2  # previous code...
4  def create
5    @user = User.new(user_params)
6    if @user.save
7      render status: :ok, json: { notice: 'User was successfully created!' }
8    else
9      render status: :unprocessable_entity, json: {
10        errors : @user.errors.full_messages.to_sentence
11      }
12    end
13  end
15  private
17  def user_params
18    params.require(:user).permit(:name, :email, :password, :password_confirmation)
19  end

Moving response messages to i18n en.locales

Let's move our response message to locales/en.yml but this time with even more modularization. Since, the response message for User and Task are same, we can combine them using variable interpolation.

To create abstraction, the i18n provides a feature called variable interpolation that allows you to use variables in translation definitions and pass the values for these variables to the translation method.

Let's modify our response message in en.yml to use variable interpolation:

2  successfully_created: "%{entity} was successfully created!"

Now, successfully_created, in en.yml, will expect a variable named, entity. So let's go through our JSON responses and pass that variable with appropriate entity name, wherever we are using successfully_created key.

In users_controller.rb:

1def create
2  @user = User.new(user_params)
3  if @user.save
4    render status: :ok, json: { notice: t('successfully_created', entity: 'User') }
5  else
6    render status: :unprocessable_entity, json: {
7      errors : @user.errors.full_messages.to_sentence
8    }
9  end

And in tasks_controller.rb:

1def create
2    @task = Task.new(task_params.merge(creator_id: @current_user.id))
3    authorize @task
4    if @task.save
5      render status: :ok, json: { notice: t('successfully_created', entity: 'Task') }
6    else
7      errors = @task.errors.full_messages.to_sentence
8      render status: :unprocessable_entity, json: { errors: errors }
9    end
10  end

And hence, by doing this we don't have to write two separate messages.

API connector for authentication

We can create a new API connector, auth.js, which exports all authentication related API calls as functions.

Create a the file app/javascript/src/apis/auth.js and add the following lines to it:

1import axios from "axios";
3const signup = (payload) => axios.post("/users", payload);
5const authApi = {
6  signup,
9export default authApi;

Creating sign up component

We need to create a Signup component to allow users to signup.

We will be keeping all of our auth based components inside Authentication folder. To do so, run the following command:

1mkdir -p app/javascript/src/components/Authentication

Inside the directory, create a new component by running the command:

1touch app/javascript/src/components/Authentication/Signup.jsx

As discussed in the technical design, we will be abstracting signup form logic to a different component called SignupForm. To create the component that contains the form logic, run the following command:

1mkdir -p app/javascript/src/components/Authentication/Form
2touch app/javascript/src/components/Authentication/Form/SignupForm.jsx

In SignupForm.jsx, paste the following contents:

1import React from "react";
2import { Link } from "react-router-dom";
4import Input from "components/Input";
5import Button from "components/Button";
6const SignupForm = ({
7  handleSubmit,
8  setName,
9  setEmail,
10  setPassword,
11  loading,
12  setPasswordConfirmation,
13}) => {
14  return (
15    <div
16      className="flex items-center justify-center min-h-screen px-4
17    py-12 sm:px-6 lg:px-8 bg-gray-50 "
18    >
19      <div className="w-full max-w-md">
20        <h2
21          className="mt-6 text-3xl font-extrabold leading-9
22        text-center text-gray-700"
23        >
24          Sign Up
25        </h2>
26        <div className="text-center">
27          <Link
28            to="/"
29            className="mt-2 text-sm font-medium text-center
30            text-bb-purple transition duration-150 ease-in-out
31            focus:outline-none focus:underline"
32          >
33            Or Login Now
34          </Link>
35        </div>
36        <form className="mt-8" onSubmit={handleSubmit}>
37          <Input
38            label="Name"
39            placeholder="Oliver"
40            onChange={(e) => setName(e.target.value)}
41          />
42          <Input
43            type="email"
44            label="Email"
45            placeholder="oliver@example.com"
46            onChange={(e) => setEmail(e.target.value)}
47          />
48          <Input
49            type="password"
50            label="Password"
51            placeholder="********"
52            onChange={(e) => setPassword(e.target.value)}
53          />
54          <Input
55            type="password"
56            label="Password Confirmation"
57            placeholder="********"
58            onChange={(e) => setPasswordConfirmation(e.target.value)}
59          />
60          <Button type="submit" buttonText="Register" loading={loading} />
61        </form>
62      </div>
63    </div>
64  );
67export default SignupForm;

Now open Signup.jsx and add the following contents.

1import React, { useState } from "react";
3import SignupForm from "components/Authentication/Form/SignupForm";
4import authApi from "apis/auth";
6const Signup = ({ history }) => {
7  const [name, setName] = useState("");
8  const [email, setEmail] = useState("");
9  const [password, setPassword] = useState("");
10  const [passwordConfirmation, setPasswordConfirmation] = useState("");
11  const [loading, setLoading] = useState(false);
13  const handleSubmit = async (event) => {
14    event.preventDefault();
15    try {
16      setLoading(true);
17      await authApi.signup({
18        user: {
19          name,
20          email,
21          password,
22          password_confirmation: passwordConfirmation,
23        },
24      });
25      setLoading(false);
26      history.push("/");
27    } catch (error) {
28      setLoading(false);
29      logger.error(error);
30    }
31  };
32  return (
33    <SignupForm
34      setName={setName}
35      setEmail={setEmail}
36      setPassword={setPassword}
37      setPasswordConfirmation={setPasswordConfirmation}
38      loading={loading}
39      handleSubmit={handleSubmit}
40    />
41  );
44export default Signup;

We have created a Signup component and we have abstracted form logic to Signup.jsx and form components to SignupForm.jsx.

Now, we will be adding a route inside of App.jsx that renders our newly created Signup component.

To do so, open App.jsx and add the highlighted lines:

1// ----previous imports if any----
2import Signup from "components/Authentication/Signup";
4const App = () => {
5  // previous code if any
6  return (
7    <Router>
8      <Switch>
9        // -----previous code if any-----
10        <Route exact path="/signup" component={Signup} />
11      </Switch>
12    </Router>
13  );

Now let's run Rails server and visit http://localhost:3000/signup and input the details of the new user.

After filling in the details click on submit button and you will be notified whether the request was successful or not by the previously added Toastr component.

In the following chapters we will implement the feature to redirect the user to login page on successful registration.

Now, let's commit the changes:

1git add -A
2git commit -m "Added signup feature"
    to navigateEnterto select Escto close