Back
Chapters

Updating task

Search icon
Search Book
⌘K

Now that we have everything in the right place we are all set to introduce a new feature to update a task.

Features

These are the requirements of this feature:

  • An edit button should be present for each task. User should be redirected to edit task page on clicking the edit button.

  • Edit task page should contain a form with pre filled values of the task.

  • Upon clicking the submit button in the edit form, a PATCH request should be sent with the updated task values.

  • A notification should be displayed stating whether the update operation was successfully reflected in our database or not.

  • User should be redirected to the Dashboard once a task is successfully updated.

Updating a task feature

Technical design

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

On the backend

  • Add an update route for tasks RESTful resources in the routes file.

  • Add an update action in the TasksController. When application receives a PATCH request to update a task, it will be processed by the update action.

  • Add a before_action callback to invoke load_task! method which will load the requested task before both show and update actions are called.

On the frontend

  • Add a PATCH request API connector for updating a task.

  • Create an Edit component which will receive the task slug through URL params. It will also contain the reusable Form component which will provide an edit form.

  • Update the Show component and add an updateTask function which will handle the logic to redirect the application to edit page.

  • Edit component will call the API to fetch a task and pass fetched data to the Form to pre-populate the fields.

  • Add a route in App component for rendering the Edit component.

Implementing update task

We will start by adding the update action in TasksController. Let's open /app/controllers/tasks_controller.rb and change the code as shown below:

1class TasksController < ApplicationController
2
3  # previous code if any
4
5  def update
6    task = Task.find_by!(slug: params[:slug])
7    task.update!(task_params)
8    respond_with_success("Task was successfully updated!")
9  end
10
11  private
12
13    # previous code if any

We have used the update! method to save the updated values.

If the update is successful, we will return status ok with a notice. And on failure, the exception raised will be handled inside the ApplicationController.

In our update action we are using the same line of code which we had written in our show action. The following is that line of code:

1task = Task.find_by!(slug: params[:slug])

Finding tasks separately in every action will lead to repetition of code.

We should follow the DRY principle which we had learnt in the Core Principles chapter.

Instead of finding task separately in every action, we can create a load_task! method which we can run before certain actions using the before_action filter.

By default before_action filter will call the specified method before any or specified actions are executed.

If we don't want to run the method before all the actions, then we can pass in a hash as an argument, like so:

1before_action :load_task!, only: %i[show update]

So it'll be applied only before those specified actions.

We can also pass in the hash with the except key with an array of actions to exclude. It means that before all other actions, except the excluded ones, our before_action filter method will get run.

1before_action :load_task!, except: :create

Here the advantage is that we don't need to mention all actions manually. The load_task! method will run before all actions inside the TasksController except the create action.

Let's modify our TasksController and use the before_action filter.

Add the following changes to TasksController:

1class TasksController < ApplicationController
2  before_action :load_task!, only: %i[show update]
3
4  # previous code
5
6  def show
7    respond_with_json({ task: @task })
8  end
9
10  def update
11    @task.update!(task_params)
12    respond_with_success("Task was successfully updated!")
13  end
14
15  private
16
17    def load_task!
18      @task = Task.find_by!(slug: params[:slug])
19    end
20
21    def task_params
22      params.require(:task).permit(:title)
23    end
24end

Here, the load_task! method will fetch the task using find_by! method and store it in @task variable, before running the show and update actions.

find_by! will raise an exception if no task is found with a matching slug.

Note that, we have used a bang operator with the load_task! method name because the method will return an exception n case a task is not found. It is a Ruby convention for method names to end with a bang operator if they raise an exception.

Now, update the tasks resources in config/routes.rb file, like so:

1resources :tasks, except: %i[new edit destroy], param: :slug

Note that, we will be using the except keyword over only, since we only need to exclude three actions and include the rest.

Let's now create a new component for updating task details. To do so, like before, we will abstract the API logic and form logic to different components.

First, let's add an API route to edit tasks inside app/javascript/src/apis/tasks.js.

To do so, add the following lines to tasks.js:

1import axios from "axios";
2
3const list = () => axios.get("/tasks");
4
5const show = slug => axios.get(`/tasks/${slug}`);
6
7const create = payload =>
8  axios.post("/tasks/", {
9    task: payload,
10  });
11
12const update = ({ slug, payload }) =>
13  axios.put(`/tasks/${slug}`, {
14    task: payload,
15  });
16
17const tasksApi = {
18  list,
19  show,
20  create,
21  update,
22};
23
24export default tasksApi;

Now, let's create our React components to update task details. To do so, first run the following command:

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

Inside Edit.jsx, add the following content:

1import React, { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4import Container from "components/Container";
5import Form from "./Form";
6import tasksApi from "apis/tasks";
7import PageLoader from "components/PageLoader";
8
9const Edit = ({ history }) => {
10  const [title, setTitle] = useState("");
11  const [userId, setUserId] = useState("");
12  const [loading, setLoading] = useState(false);
13  const [pageLoading, setPageLoading] = useState(true);
14  const { slug } = useParams();
15
16  const handleSubmit = async event => {
17    event.preventDefault();
18    try {
19      await tasksApi.update({
20        slug,
21        payload: { title },
22      });
23      setLoading(false);
24      history.push("/dashboard");
25    } catch (error) {
26      setLoading(false);
27      logger.error(error);
28    }
29  };
30
31  const fetchTaskDetails = async () => {
32    try {
33      const {
34        data: {
35          task: { title, user_id },
36        },
37      } = await tasksApi.show(slug);
38      setTitle(title);
39      setUserId(user_id);
40    } catch (error) {
41      logger.error(error);
42    } finally {
43      setPageLoading(false);
44    }
45  };
46
47  useEffect(() => {
48    fetchTaskDetails();
49  }, []);
50
51  if (pageLoading) {
52    return (
53      <div className="w-screen h-screen">
54        <PageLoader />
55      </div>
56    );
57  }
58
59  return (
60    <Container>
61      <Form
62        type="update"
63        title={title}
64        userId={userId}
65        setTitle={setTitle}
66        setUserId={setUserId}
67        loading={loading}
68        handleSubmit={handleSubmit}
69      />
70    </Container>
71  );
72};
73
74export default Edit;

Add the new export in Tasks/index.js file, like so:

1// previous code as it was...
2import EditTask from "./Edit";
3
4export { CreateTask, ShowTask, EditTask };

Form is the reusable UI that we had created while working on the chapter to create a task.

Here, fetchTaskDetails function is used to pre-populate the input field with the existing title of the task.

Now, we need to create a route inside of our App.jsx.

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

1// previous imports if any
2import Dashboard from "components/Dashboard";
3import { CreateTask, ShowTask, EditTask } from "components/Tasks";
4import { ToastContainer } from "react-toastify";
5
6const App = () => {
7  // previous code if any
8  return (
9    <Router>
10      <ToastContainer />
11      <Switch>
12        // previous code if any
13        <Route exact path="/tasks/:slug/edit" component={EditTask} />
14        <Route exact path="/tasks/:slug/show" component={ShowTask} />
15        <Route exact path="/tasks/create" component={CreateTask} />
16        <Route exact path="/dashboard" component={Dashboard} />
17      </Switch>
18    </Router>
19  );
20};
21
22export default App;

Now, add the updateTask function to the Show component.

To do so, fully replace app/javascript/src/components/Tasks/Show.jsx with the following lines of code:

1import React, { useState, useEffect } from "react";
2import { useParams, useHistory } from "react-router-dom";
3
4import Container from "components/Container";
5import PageLoader from "components/PageLoader";
6import tasksApi from "apis/tasks";
7
8const Show = () => {
9  const [taskDetails, setTaskDetails] = useState([]);
10  const [pageLoading, setPageLoading] = useState(true);
11  const { slug } = useParams();
12
13  let history = useHistory();
14
15  const updateTask = () => {
16    history.push(`/tasks/${taskDetails.slug}/edit`);
17  };
18
19  const fetchTaskDetails = async () => {
20    try {
21      const {
22        data: { task },
23      } = await tasksApi.show(slug);
24      setTaskDetails(task);
25    } catch (error) {
26      logger.error(error);
27    } finally {
28      setPageLoading(false);
29    }
30  };
31
32  useEffect(() => {
33    fetchTaskDetails();
34  }, []);
35
36  if (pageLoading) {
37    return <PageLoader />;
38  }
39
40  return (
41    <Container>
42      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-bb-gray border-b border-bb-gray">
43        <span>Task Title : </span> {taskDetails?.title}
44      </h1>
45      <div className="bg-bb-env px-2 mt-2 mb-4 rounded">
46        <i
47          className="text-2xl text-center transition cursor-pointer duration-300ease-in-out ri-edit-line hover:text-bb-yellow"
48          onClick={updateTask}
49        ></i>
50      </div>
51    </Container>
52  );
53};
54
55export default Show;

Now, on the show task page, clicking on edit icon would render the Edit component from Tasks folder, where task details can be edited.

Moving response messages to i18n en.locales

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

1en:
2  successfully_created: "Task was successfully created!"
3  successfully_updated: "Task was successfully updated!"
4  task:
5    slug:
6      immutable: "is immutable!"

Let's update the update action of TasksController with the translation, like so:

1def update
2  task = Task.find_by!(slug: params[:slug])
3  task.update!(task_params)
4  respond_with_success(t("successfully_updated"))
5end
1git add -A
2git commit -m "Added ability to update a task"