Programming Assignment 3

Initial Due Date: 2024-10-03 11:59PM Final Due Date: 2024-10-25 4:00PM

Github Classroom Gradescope
In this assignment, you will continue your implementation of Simplepedia.
Learning Goals:
  • Practice implementing React components, including incorporating PropTypes, by adding editing functionality (to create or update articles)
  • Advance your understanding of the division of responsibilities and state between components
  • Practice "agile" development when requirements change over time
Submission: Successfully submitting your assignment is an ordered two-step process:
  1. Commit and push your changes to GitHub
  2. Submit your repository to the Gradescope assignment

Prerequisites

This assignment builds on the work of assignment 2. As such, you should not start it until you have passed all (or at least most) of the tests for assignment 2.

  1. Click the GitHub classroom link and then clone the repository GitHub classroom creates to your local computer (in your shell, execute đŸ’» git clone followed by the address of the repository).
  2. Update the package.json file with your name and e-mail
  3. Install the package dependencies with đŸ’» npm install

Once you have the dependencies installed you can start the development server with đŸ’» npm run dev.

Background

This assignment, the next part of creating Simplepedia, starts where assignment two left off. As with previous assignments an initial set of (failing) tests are provided as part of the skeleton (run with đŸ’» npm test). These tests are intended to help you as you develop your application and to ensure that the grading scripts can successfully test your assignment. Code that passes all of the provided tests is not guaranteed to be correct. However, code that fails one or more these tests does not meet the specification. You are not expected to write any additional tests for this assignment.

As a general bit of advice, worrying too much about the flow of interactions through your application can lead your astray. While information flow is important, focusing on it will have you tied in knots try pass information between components. Practice compartmentalization and trusting that other components will work. Recall the philosophy of React. Focus on “given the current state and props, what should I render?” and “how do I update that state in response to user actions”. React will take care of triggering the necessary re-rendering.

Assignment

Part 0: Port over Assignment 2

Update the Assignment 3 skeleton with the code you wrote for assignment 2. Be a little cautious as you do this, there are some changes and you don’t want to just replace the files with your old ones. Some specific changes to keep an eye out for:

  1. The article collection is now a prop of Simplepedia instead of being state. If you take a look in _app.js you will find the data’s new home.
  2. What will become common layout (e.g., title, footer) has been pulled into _app.js to reduce duplication in the other components.

Make sure that your code has all of the functionality from assignment 2 before proceeding. Note that some of the tests are the same, but many are testing for the new functionality and you many need to use skip and only to prune down to tests for the old behavior.

Part 1: Add PropTypes

Please add PropTypes to your existing modules. You will find that ESLint is now checking for PropTypes, so your code will no longer pass the linter without them.

When adding PropTypes, be as specific as possible. There are a lot of different validators. Note that these cover specifying the contents of arrays and the structure of objects. For example, for the Article component, don’t just specify that it receives an object prop, specify what fields are required in that object. If you have an Array prop, specify what type should be in the array.

As a starting point I’ve provided ArticleShape.js in components (and an example of its use in index.js for the Simplepedia component). This is a full description of the article type, and you can use it in your PropType descriptors. After you implemented your PropTypes, make sure that eslint and jest agree that you have added PropTypes for everything.

Part 2: Switch to routing (requirements change!)

Simplepedia has a critical limitation: you can’t bookmark a particular article or share a link to it. We are going to fix that with routing. (You can read about Next routing in the Next documentation, but read through this section first and check out relevant FAQ entries below).

After this change, every article will now have a unique URL. For example, the article on ‘Pierre Desrey’ will now be located at http://localhost:3000/articles/42. Rather than storing a currentArticle as state, we will the use the URL to keep track of which article we are looking at (if any).

We will use a “RESTful” API with the following initial URLs:

URL View
/articles/ Main entry point showing sections, but with no section or article selected.
/articles/[id] Show article with id and corresponding section.

Dynamic routes

In class we discussed how we could make a new .js files in pages and they would become “pages” in our single-page application. We are not going to make a new page for every single article. Instead, we are going to use a facility in Next called dynamic routes. Dynamic routes are essentially matchers. They allow us to introduce variables into the route that are available to our code.

We specify a dynamic route by putting the name in square brackets. We are going to use different forms of dynamic routing, including a special form called an “optional catch all dynamic route”. Don’t get sidetracked by all the possible features of Next.js, for this assignment, please focus on implementing the following steps:

  1. Make a directory within pages, named articles (recall routes are determined by the file structure within the pages directory).
  2. Move your current index.js to be a file named [[...id]].js, within the articles directory. This will now match /articles, /articles/42, etc. At this point (assuming you have the development serving running) you will likely see a 404 error in the browser. Try updating the URL to the http://localhost:3000/articles. Your application should still work.

The id in the file name is actually a variable, and we would like to know what it is so we can display the right article. To do that, we are going to use the useRouter hook to access the Next router state. In anticipation of adding a second page, we are going to extract the article id in _app.js.

Within _app.js:

  1. Import useRouter with import { useRouter } from "next/router";
  2. Add const router = useRouter(); to the top of the component
  3. Access the id variable with const { id } = router.query; (note the destructuring assignment) in the body of the MainApp function.

Try console logging the id and navigating to http://localhost:3000/articles/42 in your browser to make sure you can extract the id from the URL. Recall that your React components are executing in the browser, so any console.log statements in your components will actually print to the browser’s console (accessible via the browser’s Developer Tools).

Determine (and update) currentArticle from id

Create a new currentArticle variable in MainApp. Use the id you obtained from the router to look up the article in the collection. The find function would be a good choice. Note that:

  • Sometimes id will be undefined (e.g., when there is no id), in which case just set your currentArticle variable to undefined.
  • id will be a string, while the ids of the articles will be numbers, and they will need to be the same type if you use ===. As a hint, the unary + operator can be used to convert strings to numbers, e.g., try +"42" in the node interpreter.

Similar create a function setCurrentArticle(article) inside the MainApp component to use as the setter. This function will be responsible for programmatically changing the URL. To change the URL we will use the router.push() method with the route we wish to visit as the argument. Thus to view the article with the id of 42, we would invoke router.push("/articles/42"). If we want to clear the current article (i.e., someone calls the function with no arguments), we can call router.push("/articles").

Why push? The router exposes a push method because we think about the sequence of sites that we visit as a stack of pages. The “Back” button is essentially popping the last page off of the stack revealing where we were immediately before.

Then add currentArticle and setCurrentArticle to the props created in MainApp and to the props expected by the Simplepedia component ([[...id]].js). At this point you should be able to remove the current article state you previously implemented in Simplepedia (i.e., with useState). If you originally used a different name for that state than currentArticle and setCurrentArticle, update those names to be consistent with the new props you added.

At this point you should be able to visit individual articles by URL, e.g. http://localhost:3000/articles/42!

Remap application entry point

If you try to navigate to http://localhost:3000, you will get a 404 error (“This page could not be found”), since we moved the index.js file that was loaded for that URL. In our new version, /articles is really the starting point of the application. We will utilize Next’s redirect feature to redirect all requests to the root page / to /articles. Create a file in the root directory of your application named next.config.js with the following contents. Note that you will need to restart the development server for this change to take effect. When you do so, entering http://localhost:3000 should automatically redirect you to http://localhost:3000/articles.

module.exports = {
  async redirects() {
    return [
      {
        source: "/",
        destination: "/articles",
        permanent: false,
      },
    ];
  },
};

Part 3: Allow the user to create new articles

You will find a new Editor component in src/components/Editor.js. It should allow the user to create a new article (note that this is just the form to enter in a new article, it doesn’t handle the actual storage). In this part you should pass a single prop to the Editor component: a “required” callback named complete that takes an article object as its argument and creates the article (note the skeleton implements a second prop that we will use later). Check out the FAQ entry about article objects.

Make sure to use these names (and any others specified in the assignment) to facilitate automated testing. While there may be different, equally valid designs, you are expected to code to these specifications.

Your component should use an <input> of type “text” to allow the user to enter in a new title and a <textarea> to allow the user to write the body of the article. Your component should have two buttons (your editor styling does not need to match the example above). The first should be a “Save” button. When the user clicks the “Save” button, the article should be added to the collection, and you should automatically navigate ot the newly created article. The date on the article should be set to the current date and time (as an ISOString via the toISOString() method on Date objects). The second should be a “Cancel” button. When the user clicks cancel, the new article should be discarded and the primary interface should be restored. Keep in mind the newly created article might not belong to an existing section.

There is one form validation required. If the title is not set, you should not let the user save the article by disabling the “Save” button. UX best practices are to also provide a message explaining the validation error (as close in time and space to the error as possible), however for simplicity in this assignment you will just disable the “Save” button. To help the user, provide meaningful initial placeholder text in both input elements.

Recall that we use controlled components. Store the state of the values you are getting from the user and use those value and onChange call backs to keep the input elements in sync with the state (see the ColorPicker example). You should not extract the values from the underlying DOM elements. You do not need to wrap our inputs tags in a <form> tag. Doing so can create problems for saving the data.

Routing to the editor

Just as we are now using routing to switch between articles, we would like to use routing to switch to the editor. Specifically, http://localhost:3000/edit should bring up a blank editor, and http://localhost:3000/articles/42/edit should allow us to edit the article with id 42 (we will get to that shortly).

Create a new page in pages named edit.js. Inside, create a new component named SimplepediaCreator (the name is important, the tests are expecting it). Your component will be similar to Simplepedia (in pages/articles/[[...id]].js), but will have collection, setCollection, and setCurrentArticle props and render an Editor component. The setCollection prop has already been implemented in _app.js and will update the collection when passed an array.

Update the collection

Your Editor component expects a complete callback. You will write it in SimplepediaCreator. This callback should check if there is an article. If there is, add it to the collection (remember that you can’t just push it on the end of the array, you need to create a new array to trigger the rerendering). Before you add the new article to the collection, you will need to give it a unique id. Scan through the collection to find the largest id, and give your new article an id that is one more than that. Once the article is added, invoke the setCurrentArticle callback to render the newly created article. If the article argument is missing (undefined), then the user canceled. Invoke router.back() to return to the previous page.

Part 4: Add some buttons

The Simplepedia component will be responsible for switching between adding and viewing. Implement the component named ButtonBar in src/components. It should display a single button called “Add”. The component will take a single callback prop named handleClick. When the button is clicked, the component should call the function, passing “add” as an argument (i.e., handleClick("add")). Add this new component to Simplepedia underneath the Article component.

In Simplepedia write the handleClick callback so that when it receives the command “add”, it uses the router to take the user to the editor.

Part 5: Updating IndexBar

We want to ensure that the IndexBar section is consistent with the new “current article”, i.e. it is showing all the titles in the same section as the newly created article. Pass currentArticle to IndexBar as a prop (there are other ways to accomplish this goal, but this is the approach we will use in our assignment to minimize changes from assignment 2). Unfortunately, you can’t just use the first character of the title to determine the section, because then we couldn’t switch sections. Instead, we will use an effect hook to update the section state whenever (and only when) the currentArticle prop changes. We will discuss useEffect in class shortly.

The basic concept is that we register a function to be called under certain circumstances (when state or prop values change). The useEffect() function takes two arguments, the first is the function to run and the second is the list of variables to watch for changes. You will want to watch for changes to currentArticle, changing the section if appropriate. This will mean that the section will change when the article changes, but can then be also changed independently when the user selects a new section.

useEffect(() => {
  // Code to update section state appropriately
}, [currentArticle]);

It is possible for the user to create a new article with the same title as a current article. We will ignore that problem in this assignment as we will fix it in the next assignment when we introduce a server (which will validate that the title is unique).

Part 6: Allow editing of existing articles

Once you can successfully add new articles, you will adapt the interface to allow editing of articles. Add an “Edit” button to the ButtonBar to request the current article be edited. When clicked, the edit button should call handleClick with the string "edit". When handleClick is invoked with “edit”, you should route to /articles/[id]/edit, with the id of the article to be edited (e.g., /articles/42/edit).

Create the page in the file pages/articles/[id]/edit.js (i.e., a directory named [id] inside the articles directory containing a file named edit.js). Since this route is more specific than the catch all route we created earlier it will take precedence. This page will be similar to the edit.js you create previously, but the component will be named SimplepediaEditor and take an additional prop currentArticle.

Pass currentArticle to Editor as the currentArticle prop. Modify Editor to initialize the values of the form fields with the current title and body. currentArticle is not always defined to so when we initialize the state we will need to handle the two different cases, when currentArticle is defined and when it is not. In the former, for example, we would want to initialize the title state with currentArticle.title and in the latter, we would want to initialize the title state with "" (the empty string).

There is an additional subtlety. Next may render SimpleEditor before it has extracted the id from the URL (and set currentArticle). SimpleEditor should effectively reset the Editor component whenever the current article changes (i.e., each instance of Editor is specific to a particular article). To do so, we specify the key prop to Editor as key={currentArticle?.id}. This tells React that Editors for two different existing articles are distinct and should not share state.

On “Save”, the date should be updated and the changes saved (and the newly edited article displayed). On “Cancel”, the changes should be discarded leaving the article unmodified and the previous article view should be restored (displaying the original, unedited, article).

Part 7: Improve the UX

One principle in user experience (UX) design is to not allow the user to perform actions when the actions don’t make sense. We have already done this in Editor, where the “Save” button isn’t enabled unless the user has added a title. The user also shouldn’t be able to edit if there isn’t a current article. Add another prop to ButtonBar named allowEdit. When true, the “Edit” button should be visible. When false, it shouldn’t be visible. Note we are not just disabling the button, but actually not showing if there isn’t a current article.

Finishing up

Your submission should not have ESLint warnings or errors when run with đŸ’» npm run lint. Remember than you can fix many errors automatically with đŸ’» npm run lint -- --fix (although since ESLint can sometimes introduce errors during this process, we suggest committing your code before running “fix” so you can rollback any changes). The assignment skeleton includes the Prettier package and associated hooks to automatically reformat your code to a consistent standard when you commit. Thus do not be surprised if your code looks slightly different after a commit.

Submit your assignment by pushing all of your committed changes to the GitHub classroom via đŸ’» git push --all origin and then submitting your repository to Gradescope as described here. You can submit (push to GitHub and submit to Gradescope) multiple times.

Portions of your assignment will undergo automated grading. Make sure to follow the specifications exactly, otherwise the tests will fail (even if your code generally works as intended). Use the provided test suite (run with đŸ’» npm test) to get immediate feedback on whether your code follows the specification. Because of the increased complexity of a React application, Gradescope can take minutes to run all of the tests. Thus you will be more efficient testing locally and only submitting to Gradescope when you are confident your application meets the specification.

Grading

Assessment Requirements
Revision needed Some but not all tests as passing.
Meets Expectations All tests pass, including linter analysis (without excessive deactivations).
Exemplary All requirements for Meets Expectations and your implementation is clear, concise, readily understood, and maintainable.

FAQ

Do I need to implement unit testing?

We will learn later in the semester how to unit test React components. For this assignment you are not expected to implement any of your own unit tests. The skeleton includes some unit tests to assist you in your development and to ensure that the grading scripts can automatically test your submission.

What if the tests and assignment specification appear to be in conflict?

Please post to Ed so that we can resolve any conflict or confusion ASAP.

What do you mean by creating a new article object?

When part 3 talks about creating new article objects, it means a new object in the Javascript sense, e.g., something like the following:

{
  "title": "Alpha Centauri",
  "contents": "An alien diplomat with an enormous egg shaped head",
  "edited": "2017-05-08",
}

or what is sometimes called a “plain old javascript object”. It does not mean a “capital A” article as defined in src/components/Article.js. The latter is the Article React component and is only used for rendering articles, not for maintaining the article related data.

I am getting an error about no router instance found. What is going on?

Error: No router instance found. you should only use “next/router” inside the client side of your app. https://nextjs.org/docs/messages/no-router-instance

The immediate cause of this error (despite what it reports) is trying to use the router, i.e., the value returned by useRouter, before it is ready. For example invoking router.push(...)` before router is ready. It takes some time before the router is ready, specifically it is not ready for use when your components are first created.

The root cause of this error is typically invoking a handler function (that in turn performs operations on the router) during rendering as opposed to providing it as a callback. In the following example

<button onClick={complete()}>Cancel</button>

complete is being invoked (with no arguments) during render, i.e., immediately, not passed as a callback to be invoked when the user clicks the button. Note the difference with the following correct approach where we are passing function to onClick that will invoke complete

<button onClick={() => complete()}>Cancel</button>

© Laura Biester, Michael Linderman, and Christopher Andrews 2019-2024.