to navigateEnterto select Escto close

    Adding a new task using create action

    Up until now we have been using the Rails console to create a task. In this chapter we are going to learn how we can achieve the same using our application's UI.


    These are the basic requirements of this feature:

    • A form component which contains a title field and a submit button.

    The picture below depicts how this feature will appear after it is implemented. Note that in the picture, create form also has a field to select the assigned user. We will only be adding the title field for now because we haven't yet created a user.

    We will add the select field once we are through with user creation.

    Adding a task using create action feature

    Technical design

    To implement this feature, we need to introduce the following changes:

    On the backend

    • Add a create action in TasksController which will handle the POST request from frontend side with task params. Create action will contain the logic to create a task and respond with a valid JSON message.

    • Add a create route for RESTful tasks resources which will route POST request to the create action in the TasksController.

    On the frontend

    • Add a POST request API in the tasks API collection for task creation.

    • Add a TaskForm component which will contain Input and Button components for title field and submit button respectively.

    • Add a CreateTask component which will contain the TaskForm and logic to submit the form once submit button inside the TaskForm is clicked.

    • Create a handleSubmit function inside CreateTask which will be called when the form is submitted. This function will pass the task params as API payload and make a POST request using the POST request API.

    • Add a route in App component to render the CreateTask component.

    Implementing create action

    We will now add create action in the TasksController.

    Here, we need to implement the creation of a task in a way that if the record creation is successful a 200 response code with a relevant notice is returned, and if the record creation fails, a 422 response code with the error message is returned.

    Open /app/controllers/tasks_controller.rb and modify the code as shown below:

    1class TasksController < ApplicationController
    3  def index
    4   tasks = Task.all
    5   render status: :ok, json: { tasks: tasks }
    6  end
    8  def create
    9    task =
    10    if
    11      render status: :ok, json: { notice: 'Task was successfully created' }
    12    else
    13      errors = task.errors.full_messages.to_sentence
    14      render status: :unprocessable_entity, json: { error: errors  }
    15    end
    16  end
    18  private
    20    def task_params
    21      params.require(:task).permit(:title)
    22    end

    In the above code, you can see that we have used errors, full_messages, and to_sentence helpers.

    The errors helper method provides us all the errors that might've occurred while performing any ActiveRecord operation on a particular object.

    Whenever such an error occurs, it can be accessed like any other attribute of the object. For example, in the above code we are accessing it using task.errors.

    Calling task.errors will return an error object which is an instance of ActiveRecord::Errors. Thus we can make use of the public instance methods which come as part of this class to format the errors in our desired manner.

    There is a method called full_messages, which will provide us the full error messages in an array.

    We used the to_sentence helper to convert the array of errors returned from errors.full_messages to a comma-separated sentence where the last element is joined by the connector word, which by default is and.

    Singular vs plural naming conventions

    Let's take a look at the create action and see if there are any naming issues. Take some time to think about .

    The issue is that to_sentence converts an array of errors to a single sentence. Hence we should use a singular variable name.

    We should have used errors if we weren't using the to_sentence method. In that case, full_messages method would have returned an array of errors and the variable should have been name errors.

    Update the create action like so:

    1def create
    2  task =
    3  if
    4    render status: :ok, json: { notice: 'Task was successfully created' }
    5  else
    6    error = task.errors.full_messages.to_sentence
    7    render status: :unprocessable_entity, json: { error: error  }
    8  end

    You should be mindful of naming variables correctly. If a variable contains multiple entities like an array of objects then it should always be plural even if it contains a single array. For example, an array of task records should be stored as tasks and not task.

    Similarly variables that contain a single entity should be singular. For example, a single task record should be stored as task.

    Adding a route for creating a new task

    Now, open /app/config/routes.rb and make the necessary change:

    1Rails.application.routes.draw do
    2  resources :tasks, only: %i[index create], param: :slug

    If you are wondering what the %i[] notation is, then it's a way of creating an array of symbols where elements are separated by space.

    [:Ruby, :Python, :PHP] is same as %i[Ruby Python PHP]. As we can see the latter version is much cleaner to look at and also it's easier to modify elements.

    Please note that %i[] is only for array of symbols. If it's an array of strings, then use the %w[] notation. That is ["Ruby", "Python", "PHP"] is same as %w[Ruby Python PHP].

    Now we will modify the Task form to send a post request to create a new task.

    Sending a POST request to create a task

    Now, we will handle the client side logic to create a new task. To do that, we will be abstracting out the API logic and UI Form logic to different components. To do so, run the following commands:

    1mkdir -p app/javascript/src/components/Tasks/Form
    2touch app/javascript/src/components/Tasks/Form/TaskForm.jsx

    In TaskForm.jsx, add the following content:

    1import React from "react";
    3import Input from "components/Input";
    4import Button from "components/Button";
    6const TaskForm = ({
    7  type = "create",
    8  title,
    9  setTitle,
    10  loading,
    11  handleSubmit,
    12}) => {
    13  return (
    14    <form className="max-w-lg mx-auto" onSubmit={handleSubmit}>
    15      <Input
    16        label="Title"
    17        placeholder="Todo Title (Max 50 Characters Allowed)"
    18        value={title}
    19        onChange={e => setTitle(, 50))}
    20      />
    21      <Button
    22        type="submit"
    23        buttonText={type === "create" ? "Create Task" : "Update Task"}
    24        loading={loading}
    25      />
    26    </form>
    27  );
    30export default TaskForm;

    Here, we are using the reusable Input and Button component that we had created before. Also, TaskForm is going to be a reusable form component that we will be using not only while creating a task but also updating a task (which comes in a future chapter).

    We have backend validation for title to allow a maximum of 50 characters. However, we have added frontend validation in the onChange method to ensure that from the user's perspective things are very clear.

    Now, we will be creating our CreateTask component that handles the API logic to create a task. To do so, run the following command:

    1touch app/javascript/src/components/Tasks/CreateTask.jsx

    In CreateTask.jsx, add the following content:

    1import React, { useState } from "react";
    2import Container from "components/Container";
    3import TaskForm from "components/Tasks/Form/TaskForm";
    4import tasksApi from "apis/tasks";
    6const CreateTask = ({ history }) => {
    7  const [title, setTitle] = useState("");
    8  const [loading, setLoading] = useState(false);
    10  const handleSubmit = async event => {
    11    event.preventDefault();
    12    setLoading(true);
    13    try {
    14      await tasksApi.create({ task: { title } });
    15      setLoading(false);
    16      history.push("/dashboard");
    17    } catch (error) {
    18      logger.error(error);
    19      setLoading(false);
    20    }
    21  };
    23  return (
    24    <Container>
    25      <TaskForm
    26        setTitle={setTitle}
    27        loading={loading}
    28        handleSubmit={handleSubmit}
    29      />
    30    </Container>
    31  );
    34export default CreateTask;

    We have used history.push to redirect our application to dashboard if a task is successfully created.

    The history object is provided by the react-router-dom package and it is passed as a prop into each component rendered by the Router.

    It contains the browser session history inside a stack. It has various other properties. One such property is the location property. It always contains the last entry in the history stack which is also the current location.

    The history object has various methods as well which can be used to manually control the browser history. Like the push method we have used.

    The push method accepts a path and pushes this path into the history stack thus updating the current location.

    You can more about the react-router-dom package and the functionalities it provides, from its official documentation.

    We will now add an API route to create a task using POST request in app/javascript/src/apis/tasks.js. In tasks.js, add the following lines:

    1import axios from "axios";
    3const list = () => axios.get("/tasks");
    5const create = payload =>"/tasks/", payload);
    7const tasksApi = {
    8  list,
    9  create,
    12export default tasksApi;

    Let's create a route to render the CreateTask component in App.jsx.

    1// previous imports if any
    2import CreateTask from "components/Tasks/CreateTask";
    3import Dashboard from "components/Dashboard";
    5const App = () => {
    6  // previous code if any
    7  return (
    8    <Router>
    9      <Switch>
    10        <Route exact path="/tasks/create" component={CreateTask} />
    11        <Route exact path="/dashboard" component={Dashboard} />
    12      </Switch>
    13    </Router>
    14  );
    16// previous code if any

    Visit http://localhost:3000 and click the Create button in NavBar and this time enter a new task and hit "Submit" button. We should see our new task in the tasks list.

    Application flow for creating the task

    The flow of all the operations are as following:

    1. Click the link Create on NavBar. The react-router will then render the CreateTask form. Once the form is submitted, the data is send using axios POST request to the create action in tasks controller, where the task_params method will get the strong parameters we had passed in.

    2. Controller will then try to create a new record in database using

    3. If the task creation is successful, the controller will return a status ok with a notice message, and if the creation fails the controller will return status unprocessable_entity with the error messages.

    4. If the task record creation is successful, then, the user will be redirected to /dashboard which is the task listing page.

    Moving response messages to i18n en.locales

    Let's move the response messages to en.yml:

    2  successfully_created: "Task was successfully created!"
    3  task:
    4    slug:
    5      immutable: "is immutable!"

    Let's use that to show response:

    1def create
    2  @task =
    3  if
    4    render status: :ok, json: { notice:  t('successfully_created') }
    5  else
    6    error = task.errors.full_messages.to_sentence
    7    render status: :unprocessable_entity, json: { error: error  }
    8  end

    Let's commit the changes:

    1git add -A
    2git commit -m "Implemented create action"