Giter VIP home page Giter VIP logo

choretrackerreactstarter's Introduction

Objectives

  • Identify pain points in existing applications
  • To utilize API endpoints in React components
  • To learn to build a series of interconnected React components that improve the user experience

Lab 11: Choretracker UX Improvements with React

As seen in lecture, we can leverage the React to allow for cool, dynamic effects on the front-end of our application. Part of the reason we may wish to use something like this would be to improve the user experience (or UX for short) of an application. Nowadays, the demand for real-time updates is higher than ever. We want to complete everything in a short of a time as we possibly can. React supports this outlook onto completing tasks.

Before going any further, it is worth keeping the React documentation open, as you may wish to refer to it to better understand the framework.

Part 1: Setup and Installation

First thing is that we have to have npm (node package manager) and yarn set up on your machine. You can check for npm with the command npm -v and see what version (if any) you have. If you don't have npm, you can either install it with a package manager (e.g., Homebrew or get it directly at https://nodejs.org/en/download. If installing npm, confirm the installation with npm -v before moving forward.

Once you have npm, you will need yarn. Again, you may already have yarn and can check with the command, yarn -v. If needed, you can install yarn with the command npm install --global yarn. Verify it's installed with the command yarn -v

For this lab, we are going to implement several features using React into Chore Tracker from before. We've provided starter code here for your convinience.

  1. Clone the starter code repository. Be sure to remove any remote connections on the repository with git remote rm origin. As always, move off the 'main' branch to a development branch (i.e., git checkout -b dev) and only save work back to main when it's good to go.

  2. In your gemfile, we have added two new gems:

    gem "shakapacker", "= 6.5"
    gem "react-rails", "= 2.6"

    These gems will make it easy for us to integrate React into an already existing Rails app. Going to the react-rails gem repo will bring up some of the documentation with this gem and could be a helpful reference.

  3. In order to speed up the lab, we have done some important setup steps for you in advance. This is for information purposes, and does not have to be repeated now. (Here for educational purposes only)

    rails webpacker:install
    
    yarn add react react-dom @babel/preset-react prop-types \
      css-loader style-loader mini-css-extract-plugin css-minimizer-webpack-plugin

    This set up webpacker and installed all of the neccessary packages for react to work properly as well as a custom javascript compiler for your project so that you can write and use js code within your ruby project.

    Additionally, we updates the babel configuration as well as the react_ujs configuration in two places within the package.json file.

      "babel": {
        "presets": [
          "./node_modules/shakapacker/package/babel/preset.js",
          "@babel/preset-react"
        ]
      },
    
      "dependencies": {
          "react_ujs": "https://github.com/67272-App-Design-Dev/react-rails/",
      }

    We also modified 'config/webpacker.yml' and changed the source_entry_path to:

    source_entry_path: packs

    After this configuration, we ran the react-rails gem generator with:

      rails generate react:install

    This created additional folders and components linking your new react packages to your project.

  4. To check that react-rails is working properly, let's use the gem to create and show a component run:

    rails g react:component HelloWorld greeting:string

    The react-rails gem is generating a very basic component which has a prop called greeting and will display it. Your component should be now created under the app/javascript/components/ directory (verify that now and look at the component generated). One thing to notice that is a little different from class is that the component explicitly calls the fragment using <React.Fragment> whereas in class, we used the common shortcut <>.

    Now to actually include the component in your views go to app/views/chores/index.html.erb and after the comment put

    <%= react_component("HelloWorld", { greeting: "Hello from react-rails." }) %>

    Run rails db:contexts to set up the database and populate it with some testing data. After that, start the server.

    You should now see the component rendered before you. If not, please see a TA/CA for assistance.

    Add and commit your work to Git if you have not done so already.

    Finally, if you are using the Brave Browser or Google Chrome (remember what Prof. H had to say about the creepiness of Chrome and just make the switch to Brave) and have not done so, install the React DevTools, as it allows for properly checking components and their state, in real-time.

    If you followed all the instructions to this point and you have the extension installed, go to the Javascript Console (an option under 'View/Developer') and select the 'Components' option. You should see something that looks like this:

Part 2: Adding the base React instance

Let's spend some time re-familiarizing ourselves with the initial pain points of the Chore Tracker application. We are taking an iterative refinement approach to developing Chore Tracker: In the first lab we were concerned with simply building a working application, and now we are ready to improve it from a UX standpoint.

This is actually a really important skill to use in industry since stakeholders are looking for results, and then improvement. The sooner you can have something working (some Minimal Viable Product), the better!

If I go to the 'Children' tab, I see a list of new children, and if I want to add one, I go to a 'new' page with its own route and controller action, and then come back to my list; perhaps not terrible as children are an independent entity in the system. Bur the same process was true of chores, even though chores depends on both Child and Tasks and I need context to set up chores (think back to our discussion of the shortcomings of visits#show in PATS_v1). We don't want to switch pages just to add a new chore and then have to come back to our show page immediately -- we just want to do it all on that one page. This is where React comes to the rescue.

To make this happen, start with the following:

  1. Generate a new component called chores via:

    rails g react:component chores
  2. Open this component and within the fragment JSX, add the following:

    <div>
      <h2>Listing chores</h2>
      <table>
        <thead>
          <tr>
            <th width="125" align="left">
              Child
            </th>
            <th width="200" align="left">
              Task
            </th>
            <th width="75">Due on</th>
            <th width="75">Status</th>
          </tr>
        </thead>
      </table>
    </div>
  3. Inside of the views/chores/index.html.erb template, remove the "HelloWorld" component and instead render this new component. Run the server, go to the chores page and verify that the heading and table show up.

  4. This sets up static elements for the index view, but we need to get data for this page. To do that, we need to set up an API call. We've given you a ChoresController under app/controllers/api/v1 and you will need to add an index action with the following:

    @chores = Chore.chronological
    render json: ChoreSerializer.new(@chores).serialized_json

    Of course, that means you have a ChoreSerializer, which right now is mostly empty. Add to that the following content:

    attribute :child_name do |object|
      object.child.name
    end
    
    attribute :task_name do |object|
      object.task.name
    end
    
    attribute :due_on
    
    attribute :status do |object|
      object.status
    end

    And don't forget to add a route in the appropriate place. (We aren't telling you how to do this; you should know it by now and this was just a friendly reminder.) Run the server and verify the route is giving you the appropriate json before proceeding further.

  5. Now that we have data for chores in json format, we need to use that to populate our page. First, as a bit of cleanup, we are going to replace the older notation of class Chores extends React.Component with just function Chores() and also get rid of the line render () { and its corresponding curly brace.

    Next we have to go fetch the data. As discussed in class, we need to handle the CSRF tag using the api components provided, so let's import what we need at the top of the page, after importing React: import { get } from "../api";

    Now we can get the data with the following. We are using useEffect as a React hook that is used to syncronize a component with an external system.

    const [chores, setChores] = React.useState([]);
    
    React.useEffect(() => {
      get("/v1/chores").then((response) => {
        React.setChores(response.data);
      });
    }, []);
  6. Now that we have the data, we can display it right after the header row in the return with the following code:

    {
      chores.map((chore) => (
        <tr key={`chore-${chore.attributes.id}`}>
          <td>{chore.attributes.child_name}</td>
          <td>{chore.attributes.task_name}</td>
          <td>{FormattedDate(chore.attributes.due_on)}</td>
          <td>{chore.attributes.status}</td>
        </tr>
      ));
    }

    Run the server and see the page is displayed. Wait! Those dates are a mess. Luckily, we have a component to format dates. Since this was discussed in class when reviewing the phase starter code, we want you to apply this now in the same way. Your page should look something like:


STOP: Show a TA that you have the basic Chores component working and formatted appropriately (including date).


Part 3: Marking chores as complete

Some of the chores listed are complete and others pending, but it would be nice to mark off completed chores right from the list. To do that, we will need to clean up the code above. It'd be nice if each row in our table was its own component. To do this, check out a new Git branch called 'refactor' so if we hose this, we can easily move back to our 'dev' branch and try again.

  1. create a ChoreItem component. This component is pretty simple and basically moving code over from our previous component. We are going to import useState directly so I don't always have to write Refactor.useState(); import with the command import { useState } from 'react'; and then add:

    function ChoreItem({ chore, choreId }) {
      const [thisChore, setThisChore] = useState(chore.attributes);
    
      return (
        <React.Fragment>
          <tr key={`chore-${choreId}`}>
            <td>{thisChore.child_name}</td>
            <td>{thisChore.task_name}</td>
            <td>{FormattedDate(thisChore.due_on)}</td>
            <td>{thisChore.status}</td>
          </tr>
        </React.Fragment>
      );
    }

    Of course, you will have to make some other adjustments (like importing FormattedDate here) and be sure to mark this function as export default.

  2. Now we go get to eliminate a lot of code on the main Chores component, replacing our loop with the following:

    {
      chores.map((chore) => <ChoreItem chore={chore} choreId={chore.id} />);
    }
  3. We want the final column to be buttons that if we push them, they will toggle between status of complete/pending. To do that, we need the following:

    • a model method that will handle the toggling in the database
    • a controller action that will utilize this
    • an API route to invoke that controller action
    • and then, the appropriate React to make the interface

    Let's get cracking on that...

  4. The model method is the easiest. We know you are model-coding ninjas at this point, so we'll make this easy and just give you the method (we leave it to you after lab to write the test for it.)

    def toggle_status
      self.completed ? self.completed = false : self.completed = true
      self.save
    end
  5. You are closing in on your API ninja status, but maybe not quite there yet, so here's the controller action you need to add to your chores API controller:

    def toggle_status
      @chore = Chore.find(params[:id])
      @chore.toggle_status
      render json: ChoreSerializer.new(@chore).serialized_json
    end

    And here's the API route you need to invoke it:

    put 'chores/:id/toggle_status', to: 'chores#toggle_status'
  6. This is a hedge, but it's not relevant to what we are doing, so we're skipping this for now. Please move along.

    Really, this is a hedge. (Definitely not ninjas in a clever disguise.) Move along.

  7. We have the groundwork laid, but need to update our ChoreItem component by making the last column a button to toggle the status. To do that, we'll create a new component called StatusButton -- you can do that manually or use the generator. Here's a start:

    function StatusButton({ choreId, status }) {
      const [thisStatus, setThisStatus] = useState(status);
    
      return <button>{thisStatus}</button>;
    }

    Again: I like to import useState so I can refer to it without the React. prefix every time I call a useState method and have done so in my code; I recommend that, but not required (although you will have to add it the prefix if you don't...)

  8. Now to make this work, I need to modify the fourth column in the ChoreItem component to be:

    <td>
      <StatusButton choreId={choreId} status={choreData.status} />
    </td>

    And you will need to import the StatusButton component. Running this will give you a button with the status displayed, but pressing it does nothing. Bummer.

  9. What we need is to activate the onClick handler, so it will respond to the button press. We can write a method called toggleStatus() which utilizes the API endpoint we created to update the database and then update our component. Try this out yourself, but if you need it, there is a solution below.

    In the meanwhile, while you try this out on your own, here's a clip from "The Tick" comic book where the ninjas are mocking our hero (The Tick). Bad move, ninjas, because the Tick is nigh-invulnerable...

    If after trying this, you need some help, here's a possible solution:

    import { put } from "../api";
    
    function StatusButton({ choreId, status }) {
      const [thisStatus, setThisStatus] = useState(status);
    
      function toggleStatus() {
        put(`/v1/chores/${choreId}/toggle_status`).then((response) => {
          const newStatus = thisStatus === "Pending" ? "Completed" : "Pending";
          setThisStatus(newStatus);
        });
      }
    
      return <button onClick={toggleStatus}>{thisStatus}</button>;
    }

    Nice thing is that now we have Chores component, which has many ChoreItem components, each of which has a StatusButton component -- a component within a component within a component. As we said in class, with React, it's components all the way down.


STOP: Show a TA that the chores in the Chores component can update their status, toogling between completed and pending.


Part 4: Adding a new chore

Adding chores is not complicated and something I could and should be able to do within this Chores component. Moreover, as I will not be leaving the page, I reduce confusion and cognitive load and can easily see a list of the chores already added.

Following on our theme of "components all the way down", let's start by creating a new component called ChoreEditor. This editor is going to need a few API endpoints, specifically with the following routes:

get 'children', to: 'chores#children'  # for select options for children
get 'tasks', to: 'chores#tasks'        # for select options for tasks
post 'create_chore', to: 'chores#create'  # to add the record to the database

Add these routes to routes.rb and then create the controller actions and serializers for children and tasks. (For serializers, we only need the :name as the :id comes automatically.) We will handle the last route later. You can do this without us providing the code (ninja level now).

  1. Armed with these, let's open up our new component and add the following code:

    import React, { useEffect, useState } from "react";
    import { get, post } from "../api";
    
    function ChoreEditor() {
      const [childOptions, setChildOptions] = useState([]);
      const [taskOptions, setTaskOptions] = useState([]);
      const [loading, setLoading] = useState();
      const [animating, setAnimating] = useState(false);
      const [child, setChild] = useState();
      const [task, setTask] = useState();
      const [dueOn, setDueOn] = useState("");
    
      // Let's get the options for our two select menus
      useEffect(() => {
        setLoading(true);
        get(`/v1/children/`).then((response) => {
          setLoading(false);
          setChildOptions(
            response.data.map((child) => {
              return {
                label: child.attributes.name,
                value: child.id,
              };
            })
          );
        });
        get(`/v1/tasks/`).then((response) => {
          setLoading(false);
          setTaskOptions(
            response.data.map((task) => {
              return {
                label: task.attributes.name,
                value: task.id,
              };
            })
          );
        });
      }, []);
    
      if (loading || childOptions?.length === 0) {
        return <div>loading...</div>;
      }
    
      return (
        <>
          <label htmlFor="children">Child</label>
    
          <label htmlFor="tasks">Task</label>
    
          <label htmlFor="due_on">Due On:</label>
    
          <button>Create Chore</button>
        </>
      );
    }
    
    export default ChoreEditor;
  2. Now we need to add the two select menus, one for Child and the other for Task. To help you out, we added some form components in components/shared/form that we can use -- in this specific case, the Select component that we import with the command: import Select from "./shared/form/Select";

  3. Under the <label> for Child, let's now add the following:

    <Select
      name="children"
      inputId="children"
      setValue={setChild}
      options={childOptions}
    />

    And then let's do something similar for Task.

  4. It'd be nice if I could actually see this component live, so to do that, return to the Chores component. First create some useState that will allow us to track whether the editor should be visible with the line const [isEditing, setIsEditing] = useState(false); right after setting the chores state.

    After that, add the following code after the display of the chores list:

    <button onClick={() => setIsEditing(true)}>Create New Chore</button>
    <br />
      {isEditing && (
        <>
          <ChoreEditor />&nbsp;&nbsp;
            <a onClick={() => setIsEditing(false)}>Cancel</a>
        </>
      )}

    This will allow the editor to display (even if it doesn't actually do anything yet) and you can see the select menus populated.

  5. Let's add in the due_on field. We could go get a component for date pickers (there are some nice ones) but in the interest of time, we will just go with a straight text field for right now. (Ugly, but easy and fast; dates will have to be added YYYY-MM-DD.) Import our textbox with the command import StringInput from "./shared/form/StringInput"; and in the form, add after its label:

    <StringInput name="due_on" id="due_on" value={dueOn} setValue={setDueOn} />
  6. We need to create a function to save the chore, but first in the controller, we need to add the following to make the route work:

     def create
       @chore = Chore.new(chore_params)
       @chore.completed = false  # by default, a new chore isn't completed yet
       @chore.save
       render json: ChoreSerializer.new(@chore).serialized_json
     end
    
     private
     def chore_params
       params.require(:chore).permit(:child_id, :task_id, :due_on)
     end
  7. Now we can call on this working endpoint with the following function, right after our code useEffect to get the data for the select options:

    function createChore() {
      setAnimating(true);
      post(`/v1/create_chore`, {
        chore: {
          child_id: child,
          task_id: task,
          due_on: dueOn,
        },
      }).then((data) => {
        if (data.errors) {
          console.log(data.errors);
        } else {
          onCreateChore(data);
        }
        setAnimating(false);
      });
    }
  8. Of course, this relies on a function onCreateChore() that we don't have. Indeed, this is going to come as a prop from the parent component; we can add that in at the top with the following edit: function ChoreEditor({ onCreateChore }) { .... With that, this component is complete, but we need to adjust the parent component to send along the right prop.

  9. In the parent component, let's edit the display of the editor to provide this prop to the ChoreEditor component with the following:

    <ChoreEditor
      onCreateChore={(chore) => {
        addChoreToDisplay(chore);
        setIsEditing(false);
      }}
    />

    What this is saying is that upon creating a new chore, we want to update the chores display with the new chore and then clear the editor out so it's not visible.

  10. We need a function then to update the display. The display will be re-rendered if I call the setChores useState function, so we can write the add chore method as follows:

    function addChoreToDisplay(chore) {
      setChores((prevChores) => [...prevChores, chore.data]);
    }

    I recommend putting this function right after useEffect call that gets the initial chores data.

Run this and see that it is indeed adding new chores and updating the list. (Aside: due to time zone issues, your date added might be +1 or -1 day off from the date entered, depending on whether in Pittsburgh or Qatar and the time of day you are doing this -- do not worry about it.)


STOP: Show a TA that the chore editor component is working properly.


choretrackerreactstarter's People

Contributors

profh avatar

Watchers

Prof. Gongora-Svartzman (Prof. GS) avatar Prof. Houda Bouamor avatar  avatar

Forkers

erinchen01

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.