Search
⌘K
    to navigateEnterto select Escto close

    Showing a task

    In the last chapter, we saw how to create a new task and save it to the database. Let's add a new feature to display the newly created task.

    Features

    These are the requirements of this feature:

    • A show button should be present for each task. User should be redirected to task details page on clicking the show button.

    • Task details page should only contain the task title for now. Below is a picture showing how UI would look like once implemented.

      Do not get overwhelmed by the edit and delete buttons as well as the comment section. We will cover those features later in this book.

    Showing a task feature

    Technical design

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

    On the backend

    • Add a show route for tasks RESTful resources in the routes file.

    • We will add a show action in the TasksController. When the application receives the request to show a task, that request will be processed by the show action.

    On the frontend

    • Add a GET API connector for fetching a task inside the tasks API collection.

    • Add a ShowTask component which will receive the task slug using URL params. ShowTask component will call the API to fetch a task and display the task details.

    • In the Dashboard component, add a new showTask function which will redirect the application to a new page based on the task slug using react-router-dom.

    • Pass down showTask function as a prop to the TableRow component through the Table component.

    • Add a new Route in the App component for the task page. This route should render the ShowTask component.

    Implementing show action in TasksController

    Open /app/controllers/tasks_controller.rb and add the following lines of code:

    1class TasksController < ApplicationController
    2  def show
    3    task = Task.find_by(slug: params[:slug])
    4    if task
    5      render status: :ok, json: { task: task }
    6    else
    7      render status: :not_found, json: { error: "Task not found" }
    8    end
    9  end
    10end

    In the show action we are trying to find and respond with a task by filtering using the slug attribute.

    Note that in the above action we have used conditional statement to send an error when no matching task can be found in DB.

    Building ShowTask Component

    Let's first create our show tasks component:

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

    In ShowTask.jsx, paste the following content:

    1import React, { useState, useEffect } from "react";
    2import { useParams } 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  const fetchTaskDetails = async () => {
    14    try {
    15      const response = await tasksApi.show(slug);
    16      setTaskDetails(response.data.task);
    17    } catch (error) {
    18      logger.error(error);
    19    } finally {
    20      setPageLoading(false);
    21    }
    22  };
    23
    24  useEffect(() => {
    25    fetchTaskDetails();
    26  }, []);
    27
    28  if (pageLoading) {
    29    return <PageLoader />;
    30  }
    31
    32  return (
    33    <Container>
    34      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-bb-gray border-b border-bb-gray">
    35        <span>Task Title : </span> {taskDetails?.title}
    36      </h1>
    37    </Container>
    38  );
    39};
    40
    41export default ShowTask;

    useParams hook is provided by the react-router-dom package and it returns an object of URL parameters as key/value pairs. For example, for /tasks/:task-slug, useParams will return the following object:

    1{
    2  ("task_slug");
    3}

    Let's now create an API route to handle our show request in app/javascript/src/apis/tasks.js.

    Note: Until now we have been using highlighted lines of code to show what needs to be added to the codebase/component.

    However, from here onwards, since we will be making a lot of changes and additions to each of the components, if we haven't highlighted any lines in code block, then the best way to stay in sync is to copy the whole code snippet and replace the corresponding component/file in your local project with that content, unless specified otherwise.

    App.jsx will only require updates in most chapters, rather than full replacement.

    This will help you from not missing out on any of the changes that needs to be reflected in your local project.

    Now update tasks.js with the following content:

    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 tasksApi = {
    10  list,
    11  show,
    12  create
    13};
    14
    15export default tasksApi;

    Now we will create a showTask handler function in the Dashboard and pass it down to the Table component. Fully replace app/javascript/src/components/Dashboard/index.jsx with the following content:

    1import React, { useState, useEffect } from "react";
    2import { isNil, isEmpty, either } from "ramda";
    3
    4import Container from "components/Container";
    5import Table from "components/Tasks/Table";
    6import tasksApi from "apis/tasks";
    7import PageLoader from "components/PageLoader";
    8
    9const Dashboard = ({ history }) => {
    10  const [tasks, setTasks] = useState([]);
    11  const [loading, setLoading] = useState(true);
    12
    13  const fetchTasks = async () => {
    14    try {
    15      const response = await tasksApi.list();
    16      setTasks(response.data.tasks);
    17      setLoading(false);
    18    } catch (error) {
    19      logger.error(error);
    20      setLoading(false);
    21    }
    22  };
    23
    24  const showTask = slug => {
    25    history.push(`/tasks/${slug}/show`);
    26  };
    27
    28  useEffect(() => {
    29    fetchTasks();
    30  }, []);
    31
    32  if (loading) {
    33    return (
    34      <div className="w-screen h-screen">
    35        <PageLoader />
    36      </div>
    37    );
    38  }
    39
    40  if (either(isNil, isEmpty)(tasks)) {
    41    return (
    42      <Container>
    43        <h1 className="text-xl leading-5 text-center">
    44          You have no tasks assigned 😔
    45        </h1>
    46      </Container>
    47    );
    48  }
    49
    50  return (
    51    <Container>
    52      <Table data={tasks} showTask={showTask} />
    53    </Container>
    54  );
    55};
    56
    57export default Dashboard;

    We need to pass down showTask function as props to TableRow component and attach it to an onClick event which will be fired upon clicking the show button.

    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 }) => {
    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 data={data} showTask={showTask} />
    14            </table>
    15          </div>
    16        </div>
    17      </div>
    18    </div>
    19  );
    20};
    21
    22export default Table;

    Then, update the content of TableRow.jsx with following lines:

    1import React from "react";
    2import PropTypes from "prop-types";
    3
    4const TableRow = ({ data, showTask }) => {
    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-right cursor-pointer">
    16            <a
    17              className="text-bb-purple"
    18              onClick={() => showTask(rowData.slug)}
    19            >
    20              Show
    21            </a>
    22          </td>
    23        </tr>
    24      ))}
    25    </tbody>
    26  );
    27};
    28
    29TableRow.propTypes = {
    30  data: PropTypes.array.isRequired,
    31  showTask: PropTypes.func
    32};
    33
    34export default TableRow;

    Now we have completed adding the show action handler for a task and hooked it to the onClick event. The last step is to define a route inside of App.jsx to render our tasks show page.

    For that, add the highlighted lines to App.jsx:

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

    Now, let's go to the dashboard URL and click on the Show button, we might see that an error has occurred:

    1Routing Error
    2No route matches [GET] "/tasks/slug"

    We are getting an error because the Rails router can't find any route matching the format /tasks/:slug for a GET request or more accurately since we haven't defined show action in task resources.

    Let's solve this by adding that action into /config/routes.rb.

    Note: It is possible that you're able to access the show page for an individual task even before updating the routes.rb file without encountering any routing error.

    But the issue that you might notice is that the title of the task will remain empty as we are not able to fetch details from the backend due to absence of the corresponding API route for returning the title details.

    Adding the route in the following step will allow us to view the show page with title of the task:

    1resources :tasks, only: %i[index create show], param: :slug

    Now if we visit dashboard URL and then click on Show button corresponding to a task, we will be able to see the details of that task.

    Using Rails console to search tasks

    Let's fire up the console once again using the rails console command:

    1rails console
    2Running via Spring preloader in process 25412
    3Loading development environment (Rails 5.2.2)
    4
    5irb(main):001:0> Task.find_by(slug: "my-first-task")
    6  Task Load (0.2ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-first-task"], ["LIMIT", 1]]
    7=> #<Task id: 1, title: "My first task", created_at: "2019-02-04 13:34:04", updated_at: "2019-02-04 13:34:04", slug: "my-first-task">
    8
    9irb(main):002:0> Task.find_by(slug: "my-4th-task")
    10  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-4th-task"], ["LIMIT", 1]]
    11=> #<Task id: 5, title: "My 4th task", created_at: "2019-02-04 15:14:26", updated_at: "2019-02-04 15:14:26", slug: "my-4th-task">
    12
    13irb(main):003:0> Task.find_by(slug: "my-7th-task")
    14  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."slug" = ? LIMIT ?  [["slug", "my-7th-task"], ["LIMIT", 1]]
    15=> #nil

    As shown above, we use the find_by method and pass it with any slug.

    If an entry in the database exists with corresponding slug, then the record is fetched otherwise exception is returned.

    We can also use other attributes to look for a specific task using the where method.

    Let's try with the title attribute:

    1irb(main):004:0> Task.where(title: "My first task")
    2  Task Load (0.8ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ?  [["title", "My first task"], ["LIMIT", 11]]
    3=> #<ActiveRecord::Relation [#<Task id: 1, title: "My first task", created_at: "2019-02-04 13:34:04", updated_at: "2019-02-04 13:34:04", slug: "my-first-task">]>
    4
    5irb(main):005:0> Task.where(title: "My 4th task")
    6  Task Load (0.4ms)  SELECT  "tasks".* FROM "tasks" WHERE "tasks"."title" = ? LIMIT ?  [["title", "My 4th task"], ["LIMIT", 11]]
    7=> #<ActiveRecord::Relation [#<Task id: 4, title: "My 4th task", created_at: "2019-02-04 15:11:42", updated_at: "2019-02-04 15:11:42", slug: "my-4th-task">, #<Task id: 5, title: "My 4th task", created_at: "2019-02-04 15:14:26", updated_at: "2019-02-04 15:14:26", slug: "my-4th-task-2">]>

    Moving response messages to i18n en.locales

    Let's move the message to be shown when no task is found into en.yml:

    1en:
    2  successfully_created: "Task was successfully created!"
    3  task:
    4    not_found: "Task not found"
    5    slug:
    6      immutable: "is immutable!"

    Let's use that to show response:

    1class TasksController < ApplicationController
    2  def show
    3    task = Task.find_by(slug: params[:slug])
    4    if task
    5      render status: :ok, json: { task: task }
    6    else
    7      render status: :not_found, json: { error: t('task.not_found') }
    8    end
    9  end
    10end

    Now let's commit these changes:

    1git add -A
    2git commit -m "Added show page for a task"
    Previous
    Next