CS 312 Software Development

CS 312 - Practical Three

Goals

  • Create your first React page
  • Learn how to create a list of components using map
  • See how useState works
  • Add some simple styling

Prerequisite

  1. Visit the practical 3 page on Replit (or click the 'Start Project' link for Practical 3 on our team page).
  2. Click the GitHub classroom link to accept the assignment.
  3. Return to the assignment on Repl.it and go to the Version Control panel.
  4. Click the button to create a new git repository (not the one for connecting to an existing repo).
  5. Open the shell and follow the instructions on your GitHub repository for connecting an existing repository:
    1. git remote add origin repo-name where repo-name is the name of your repository (e.g., https://github.com/csci312-s21/practical-03-ChristopherPAndrews.git)
    2. git branch -m main
    3. git push -u origin main
  6. Update the author information in the package.json file
  7. Install the module dependencies by typing npm install in the shell

Overview

In this practical, you are going to put together a simple React app that shows the courses in the CS catalog. It looks like this:

CS course catalog

Getting situated

I've provided you with the framework of the application, which was created with the create-next-app tool. You should see that this follows the same structure we saw in lecture.

The development server

Click the Run button to start the development server (it is configured to call nm run dev). As we discussed in class, there is a certain degree of translation that needs to happen to get your code runnable in the browser. The development server will handle that processing in real time as you update your code and serve your web pages as if it were a normal web server.

Once the development server spins up, you should see a page with a title and a footer.

pages

Open up src/pages/index.js. This is where we will put the application. You will see that the JSX in this file matches what you can see in the development server. Try a simple interaction: Add the following below the title:


<h2>Don't Panic!</h2>

You should see the message pop up on the web page. Tip: Repl.it auto saves your work as you go, but you can make it save immediately by using the familiar save shortcut of command-s or control-s depending on whether you use a mac or something else.

The data

We are including the course catalog data as a JSON file. You will find it in data/cs-courses.json. If you open it up, you will see that it is an array of objects, one per class. The objects have properties for object, name, description, and optionally, prerequisites. Prerequisites are listed as an array of numbers. Example:

{"number": 431,
    "name": "Computer Networks",
    "description":"Computer networks have had a profound impact on modern society. This course will investigate how computer networks are designed and how they work. Examples from the Internet as well as our own campus network will be discussed.",
    "prerequisites":[200,315]
   }

Some prerequisites can be satisfied with one of a collection of options. This is represented using another object with an or property like this:

{"number": 201,
    "name": "Data Structures",
    "description":"In this course we will study the ideas and structures helpful in designing algorithms and writing programs for solving large, complex problems. The Java programming language and object-oriented paradigm are introduced in the context of important abstract data types (ADTs) such as stacks, queues, trees, and graphs. We will study efficient implementations of these ADTs, and learn classic algorithms to manipulate these structures for tasks such as sorting and searching. Prior programming experience is expected, but prior familiarity with the Java programming language is not assumed. ",
    "prerequisites":[{"or": [145, 150]}]
   }

The components

You are going to add two components to the application: CourseEntry (which represents a single class), and CourseList(which represents the entire list of classes). I have started these already, and you will find the relevant files in src/components.

CourseEntry

Take a look at CourseEntry. I've given you a working implementation of the component with some basic functionality.

Note: I forgot a line in the starter code. You need the assignment statement that initially sets summaryClass to the empty string. If you started the practical before I found this, you will need to add it.

export default function CourseEntry({ course }) {
  let summaryClass = '';
  return (
    <details className={styles.description}>
      <summary className={summaryClass}>CSCI {course.number} - {course.name} </summary>
      <p>{course.description}</p>
    </details>);
}

This makes use of the details tag, which is a less well known HTML tag. It is a simple interactive widget that shows a text summary which can expand to show more detail.

This is a simple details example (click me)

When you click it, it expands to show more, well... details. It is actually three separate tags. There is the main details tag around everything. Then there is the summary tag which contains the collapsed view. Finally, there is whatever is holding the details themselves, usually a p tag.

 

So, you can see that CourseEntry takes in a single course as a prop. It then uses the values in that object to populate the JSX with data.

CourseList

I've given you less in the CourseList component. You can see that the component takes in a prop called courses (the full list of course objects), but that is all I've given you.

export default function CourseList({ courses }) {
  return (<></>);
}

The empty tags are a convenience in JSX that allow us to have components that are just placeholders (we can also use them to get around the "can only return one element" rule). They don't show up on the web page itself.

Make a list

Now we can flesh out CourseList. We need to somehow transform the array of course objects to a collection of CourseEntry components. Transforming an array of things to a different array of things? I hope your first thought was "I bet map gets involved here". If it was, you are quite correct (if it wasn't, its still map...).

Before the return statement, we are going to build the list of CourseEntry components. The basic structure will look like this:

const items = courses.map((course) => ());

The question is, what is that inner function returning? Well, we want it to be a CourseEntry component, and, as it turns out, we aren't restricted to using JSX only in return statements, we can put them anywhere where it makes sense to be talking about a component.

const items = courses.map((course) => (<CourseEntry />));

of course, to use CourseEntry in this component, we need to import its definition from the file where it is defined:

import CourseEntry from "./CourseEntry";

As I pointed out in class, this is the new style of import statement. This is importing the default export from the associated file. You will see that the function itself is marked with the keywords export default. We can export multiple functions or values from a file, but only one of them can be the default.

Now that we have the component imported, we can return to creating our CourseEntry components. We need to give the CourseEntry the course we want it to display, so we will add that as a prop:

const items = courses.map((course) => (<CourseEntry course={course}/>));

Notice that we are using curly brackets around the value because we are using a JavaScript value inside of the JSX.

We are almost there, but we need one more piece. When we make a list of React components, it is important that we add a key property so that React can tell which one is which. When React is diffing the virtual DOM and the actual DOM, it needs to have a quick way to tell which component is which without reading all of the text. Without a key, React falls back on simple ordering, and weird things happen if you delete an element out of the middle of a list. The React documentation on lists goes into more detail. The key can be anything, provided it is a unique identifier. Ids are a good candidate, and in this case, course number will work well (though it might not if we incorporated classes from other departments).

const items = courses.map((course) => (<CourseEntry key={course.number} course={course}/>));

Now that we have the items, we want to return them. Of course, we only want to return a single component, so we need a wrapper around all of our CourseEntry components. In this instance, a div is a good choice. Change the return statement to return a div. To put the list of CourseEntry components into the div, you just need to stick the items variable in there (remembering to surround it with curly braces since it is JavaScript -- if the web page comes up and just has the text "items" on it, you didn't remember).

We are going to style the div a little, so add className={styles.listContainer}as an attribute of the div tag.

To see the list, you need to put it on the page. Return to index.js. First we need to import the CourseList component. At the top of the page, add an import statement.

import CourseList from "../components/CourseList";

While you are doing that, take a moment to notice that the JSON file holding the course data has been imported as the value courses. This is a little trick performed by Webpack that actually parses the JSON into JavaScript if we import it like this.

Add a CourseList component to the page and pass it the courses as a prop.

You should now have a list of courses on your web page.

Showing Prerequisites

We are going to add a little bit of interaction to the page. As the user hovers over different courses, we are going to highlight the courses that serve as prerequisites. Since all of the CourseEntry components will need to see which entry is being hovered over, we are going to need a piece of state that lives above all of them in the hierarchy (i.e., in CourseList).

Let's call this piece of state currentCourse.

In the CourseList component, add this line:

const [currentCourse, setCurrentCourse] = useState();

This gives us our state variable (currentCourse) and the method for setting it. Notice that we didn't pass any arguments into useState -- in this instance, the default should be null because there isn't a current course, so we can just leave it off.

We need to import useState to use it, so put this line at the top of the file:

import { useState } from "react";

The curly brace notation is how we import items that are exported from their modules, but that aren't the default export.

Push the props down

We need to get the state information to the CourseEntry components, so we need to add some new props.

In the CourseList component, where you create the CourseEntry components, add a currentCourse property and a setCurrentCourse property to the CourseEntry component and set their values appropriately.

Finish adding the new properties by going to the CourseEntry component and adding them to the destructured props argument.

Changing the current course

We want the current course to change when we hover over it. One of the events we can listen for is the cursor moving over an element on the page. We register a callback for it with onMouseEnter, which fires the moment the cursor enters the region defined by the component.

When the cursor enters the region of our details component, we want it to call setCurrentCourse with the course associated with the component. Add the following to details:

onMouseEnter={() => setCurrentCourse(course)}

Now, as the user mouses over the list, the current course will change. To see this in action, add a console.log(currentCourse) to the CourseList component and then open up the console like I showed you in class to see the output.

Add some styling

Of course, printing things to the console is pretty useless, we want users to be able to see what they are doing. So, we will change the styling so that the summary of the current course is bolded.

We are using CSS modules for this example, so you will find the stylesheet for CourseEntry in src/styles/CourseEntry.module.css. I would like you to add a new class in that file that sets the font-weight to bold (if you aren't sure what this says, please read up on CSS).

.current {
  font-weight: bold;
}

Now we need to apply this class to the summary element in CourseEntry. As I showed in the lecture, to access a class from a CSS module, we import the module (which I've already done for you), and then we can refer to the class name as styles.class-name. So, in this instance, it will be styles.current.

We only want the style to applied when the course we are rendering is actually the current course, so we are going to set the value conditionally.

Create a new variable called summaryClass and set it to "". This is the default -- no class.

If the course is equal to currentCourse, set the variable to styles.current (don't forget to use === for the equality check).

Add a className attribute to summary and set it to summaryClass.

Check it out -- the courses should now show up bold as you mouse over them.

Add styling for prerequisites

Bolding the entry you are over hardly requires state -- we could actually do that with pure CSS. We didn't, however, because we are going to also show the prerequisites.

Go back to the CSS module, and add the following styling for prerequisites:

.prereq {
  font-weight: bold;
  background: lightslategray;
  color:white;
}

Now, back in CourseEntry, we are going to add some more code to check if this course is a prerequisite for the current course. The logic we are going to follow goes like this:

if there is a currentCourse and it has prerequisites
  iterate over each prerequisite
    if the prereq matches this course number, this course is a prereq
    else if the prereq is a compound OR object, check if the course number is in that sub-list

If the course is a prerequisite of the currentCourse, then we will set the style of the summary element to our new prereq style. I don't want you bogged down in the implementation details, so here is the code:

if (currentCourse && currentCourse.prerequisites) {
    currentCourse.prerequisites.forEach((req) => {
      if (req === course.number) {
        summaryClass = styles.prereq;
      } else if (req.or) {
        req.or.forEach((altreq) => {
          if (altreq === course.number) {
            summaryClass = styles.prereq;
          }
        });
      }
    })
  }

There we go! It isn't the most useful website, but hopefully it has been helpful in providing a sense of the process of building a React app, as well as showing you some of the benefits of the component based approach to putting together websites.

Submitting

Go ahead and commit your changes. You are welcome to use the Replit interface to "commit & push". This should push the changes out to GitHub and you can follow the directions to submit your repository to Gradescope as described here.

Requirements

  • Should display all courses
  • Hovered over courses should be bolded
  • Pre-requisites should by highlighted
  • Pass all tests
  • Pass all ESLint checks

Last updated 03/28/2021