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.

  • Update the before_action callback in the TasksController to call the load_task method before update action is called.

On the frontend

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

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

  • EditTask component will call the API to fetch a task and pass fetched data to the TaskForm to pre populate the fields.

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

  • Pass updateTask function as a prop to the TableRow component through ListTasks and Table components.

  • Update the TableRow component and add an edit button which will call the updateTask function received through props.

  • Add a route in App component for rendering the EditTask 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  before_action :load_task, only: %i[show update]
3
4  ...
5
6  def update
7    if @task.update(task_params)
8      render status: :ok, json: { notice: 'Successfully updated task.' }
9    else
10      render status: :unprocessable_entity, json: { errors: @task.errors.full_messages.to_sentence }
11    end
12  end
13
14  private
15
16    ...

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, we will return status unprocessable_entity along with the error messages.

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

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

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) => axios.post("/tasks/", payload);
8
9const update = ({ slug, payload }) => axios.put(`/tasks/${slug}`, payload);
10
11const tasksApi = {
12  list,
13  show,
14  create,
15  update,
16};
17
18export 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/EditTask.jsx

Inside EditTask.jsx, add the following content:

1import React, { useState, useEffect } from "react";
2import { useParams } from "react-router-dom";
3
4import Container from "components/Container";
5import TaskForm from "./Form/TaskForm";
6import tasksApi from "apis/tasks";
7import PageLoader from "components/PageLoader";
8
9const EditTask = ({ 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: { task: { title, user_id: userId } },
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 response = await tasksApi.show(slug);
34      setTitle(response.data.task.title);
35      setUserId(response.data.task.user_id);
36    } catch (error) {
37      logger.error(error);
38    } finally {
39      setPageLoading(false);
40    }
41  };
42
43  useEffect(() => {
44    fetchTaskDetails();
45  }, []);
46
47  if (pageLoading) {
48    return (
49      <div className="w-screen h-screen">
50        <PageLoader />
51      </div>
52    );
53  }
54
55  return (
56    <Container>
57      <TaskForm
58        type="update"
59        title={title}
60        userId={userId}
61        setTitle={setTitle}
62        setUserId={setUserId}
63        loading={loading}
64        handleSubmit={handleSubmit}
65      />
66    </Container>
67  );
68};
69
70export default EditTask;

TaskForm is the reusable Form UI that we had created while working on creating 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 code if any
2import EditTask from "components/Tasks/EditTask";
3
4const App = () => {
5  // previous code if any
6  return (
7    <Router>
8      <Switch>
9        // previous code if any
10        <Route exact path="/tasks/:slug/edit" component={EditTask} />
11        <Route exact path="/tasks/create" component={CreateTask} />
12        <Route exact path="/dashboard" component={Dashboard} />
13      </Switch>
14    </Router>
15  );
16};
17
18export default App;

Now, add the updateTask function to app/javascript/src/components/Dashboard/index.jsx. To do so, add the following lines:

1import React, { useState, useEffect } from "react";
2import { isNil, isEmpty, either } from "ramda";
3
4import Container from "components/Container";
5import ListTasks from "components/Tasks/ListTasks";
6import tasksAPI from "apis/tasks";
7import PageLoader from "components/PageLoader";
8
9const Dashboard = ({ history }) => {
10  // --- Previous Content ---
11  const updateTask = (slug) => {
12    history.push(`/tasks/${slug}/edit`);
13  };
14
15  if (either(isNil, isEmpty)(tasks)) {
16    return (
17      <Container>
18        <h1 className="text-xl leading-5 text-center">
19          You have no tasks assigned 😔
20        </h1>
21      </Container>
22    );
23  }
24
25  return (
26    <Container>
27      <ListTasks data={tasks} updateTask={updateTask} showTask={showTask} />
28    </Container>
29  );
30};
31
32export default Dashboard;

Now let's pass in the handler function for updateTask action to our ListTasks component. To do so, make the following changes:

1import React from "react";
2import Table from "./Table";
3
4const ListTasks = ({ data, destroyTask, updateTask, showTask }) => {
5  return <Table data={data} showTask={showTask} updateTask={updateTask} />;
6};
7
8export default ListTasks;

Now, we need to pass down updateTask function as props to TableRow component. To do so, update Table.jsx with the following lines of code:

1import React from "react";
2import TableHeader from "./TableHeader";
3import TableRow from "./TableRow";
4
5const Table = ({ data, showTask, updateTask }) => {
6  return (
7    <div className="flex flex-col">
8      <div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
9        <div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
10          <div className="overflow-hidden border-b border-bb-gray-200 shadow sm:rounded-lg">
11            <table className="min-w-full divide-y divide-gray-200">
12              <TableHeader />
13              <TableRow
14                data={data}
15                showTask={showTask}
16                updateTask={updateTask}
17              />
18            </table>
19          </div>
20        </div>
21      </div>
22    </div>
23  );
24};
25
26export default Table;

Finally, update the TableRow component as below:

1import React from "react";
2import PropTypes from "prop-types";
3
4const TableRow = ({ data, showTask, updateTask }) => {
5  return (
6    <tbody className="bg-white divide-y divide-gray-200">
7      {data.map((rowData) => (
8        <tr key={rowData.id}>
9          <td
10            className="block w-64 px-6 py-4 text-sm font-medium
11            leading-8 text-bb-purple capitalize truncate"
12          >
13            {rowData.title}
14          </td>
15          <td className="px-6 py-4 text-sm font-medium leading-5 text-gray-900 whitespace-no-wrap">
16            {rowData.user_id}
17          </td>
18          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
19            <a
20              className="text-bb-purple"
21              onClick={() => showTask(rowData.slug)}
22            >
23              Show
24            </a>
25          </td>
26          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
27            <a
28              className="text-indigo-600 hover:text-indigo-900"
29              onClick={() => updateTask(rowData.id)}
30            >
31              Edit
32            </a>
33          </td>
34        </tr>
35      ))}
36    </tbody>
37  );
38};
39
40TableRow.propTypes = {
41  data: PropTypes.array.isRequired,
42  showTask: PropTypes.func,
43  updateTask: PropTypes.func,
44};
45
46export default TableRow;

Now, on the dashboard page while listing tasks, clicking on Edit would render the EditTask component that we just created where we will be able to edit task details.

1git add -A
2git commit -m "Added ability to update a task"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next