Search
⌘K
    to navigateEnterto select Escto close

    Updating task

    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 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.

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

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

    • 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
    3  # previous code if any
    4
    5  def update
    6    task = Task.find_by(slug: params[:slug])
    7    if task && task.update(task_params)
    8      render status: :ok, json: { notice: 'Successfully updated task.' }
    9    else
    10      render status: :unprocessable_entity,
    11        json: { error: task.errors.full_messages.to_sentence }
    12    end
    13  end
    14
    15  private
    16
    17    # 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, we will return status unprocessable_entity along with the error messages.

    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: [:show, :update]
    3
    4  def show
    5    render status: :ok, json: { task: @task }
    6  end
    7
    8  def update
    9    if @task.update(task_params)
    10      render status: :ok, json: { notice: 'Successfully updated task.' }
    11    else
    12      render status: :unprocessable_entity,
    13        json: { error: @task.errors.full_messages.to_sentence }
    14    end
    15  end
    16
    17  private
    18
    19    def load_task
    20      @task = Task.find_by(slug: params[:slug])
    21      unless @task
    22        render status: :not_found, json: { error: t('task.not_found') }
    23      end
    24    end
    25
    26    def task_params
    27      params.require(:task).permit(:title)
    28    end
    29end

    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 return nil if no task is found with a matching slug, in such a case the control will go inside unless block and a 404 response will be sent along with an error that says "Task not found."

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

    Now, add the updateTask function to the ShowTask component.

    To do so, fully replace app/javascript/src/components/Tasks/ShowTask.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 ShowTask = () => {
    9  const { slug } = useParams();
    10  const [taskDetails, setTaskDetails] = useState([]);
    11  const [pageLoading, setPageLoading] = useState(true);
    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 response = await tasksApi.show(slug);
    22      setTaskDetails(response.data.task);
    23    } catch (error) {
    24      logger.error(error);
    25    } finally {
    26      setPageLoading(false);
    27    }
    28  };
    29
    30  useEffect(() => {
    31    fetchTaskDetails();
    32  }, []);
    33
    34  if (pageLoading) {
    35    return <PageLoader />;
    36  }
    37
    38  return (
    39    <Container>
    40      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-bb-gray border-b border-bb-gray">
    41        <span>Task Title : </span> {taskDetails?.title}
    42      </h1>
    43      <div className="bg-bb-env px-2 mt-2 mb-4 rounded">
    44        <i
    45          className="text-2xl text-center transition cursor-pointer duration-300ease-in-out ri-edit-line hover:text-bb-yellow"
    46          onClick={updateTask}
    47        ></i>
    48      </div>
    49    </Container>
    50  );
    51};
    52
    53export default ShowTask;

    Now, on the show task page, clicking on edit icon would render the EditTask component, where task details can be edited.

    1git add -A
    2git commit -m "Added ability to update a task"
    Previous
    Next