Project 5

Due: Aug 11, before midnight. No late submissions.

Important Reminder: As per the course Academic Honesty Statement, cheating of any kind will minimally result in your letter grade for the entire course being reduced by one level.

This document first describes the aims of this project followed by a brief overview. It then lists the requirements as explicitly as possible. It discusses alternatives for the design. Finally, it provides some hints as to how the project requirements can be met.

Aims

The aims of this project are as follows:

  • To expose you to using React.

  • To allow you to choose among several program designs or come up with one of your own.

  • To give you confidence in your ability to implement a program from scratch without any starter code.

Requirements

You must push a submit/prj5-sol directory to your github repository such that typing npm ci within that directory followed by tsc and npm start will start a web server on localhost at port 2346. This server should provide access to a single-page app which should allow displaying spreadsheets.

The app should render a spreadsheet with the same UI and UX as your previous project (the image below was captured with cell d1 focused):

The operation of the app is same as in your previous project which was illustrated in this video.

The major difference in the requirements from those in your previous project is that all the spreadsheet functionality using hooks-based functional React components implemented using TypeScript. You may continue to use the DOM API for displaying errors and for setting up the initial form used to provide the web service URL and spreadsheet name.

To facilitate possible automated grading, widgets are required to have attributes as follows:

  • The form for specifying the web services URL and spreadsheet name must have id attribute ss-form.

  • The widget for specifying the web services URL must have id attribute ws-url.

  • The widget for specifying the spreadsheet name must have id attribute ss-name.

  • The button for clearing the spreadsheet must have id attribute clear.

  • The widget used for changing a cell must have an id attribute equal to the cell ID, like a1, b2, etc.

  • Each cell widget must have a data-is-focused attribute which should be "true" iff the cell has focus.

Design Considerations

Here are some thoughts about the design of this project.

React Components

Most designs would identify the following three React components:

  • Cell: Represents an individual spreadsheet cell.

  • Spreadsheet: Represents an entire spreadsheet. It would be rendered as a table of Cell components.

  • Clear: Represents the clear widget.

Local Spreadsheet Representation

In the previous project, we stored the spreadsheet data (both expressions and values) as attributes within the DOM. Doing so for this project would not be in keeping with the spirit of the requirements that the spreadsheet be implemented using React. Possibilities include:

  1. A single object having a TS type like:

        type SpreadsheetData = {
          [cellId: string]: { expr: string, value: number }
        };
    

    An advantage of this representation is that all spreadsheet data is consolidated together, which may make development easier. A disadvantage is that all spreadsheet changes could require re-rendering the entire spreadsheet.

  2. Each Cell component would have value and expr props. This is similar to the distributed representation used in the previous project.

    An advantage of this representation is that it may be possible to implement a local change like focusing a cell entirely within a cell. A disadvantage would be that any changes in cell props would require re-rendering the entire spreadsheet. Another disadvantage is that the spreadsheet data would be scattered across all the cells representing the spreadsheet.

Locations for Web Service Calls

Web service calls could be made at:

  1. The cell level.

  2. The spreadsheet level.

or both.

(1) would require drilling the ws prop down all the way to the Cell component. Since most spreadsheet operations entail a global change to the spreadsheet, (2) appears preferable. Note that both alternatives will require upcalls from the Cell to the Spreadsheet level via callback props.

Cell Structure

In keeping with React's single source of truth design principal, it seems reasonable that each Cell have some kind of content state which reflects the current display of that cell.

  1. An unfocused cell would have its content state set to the value of the cell.

  2. A focused cell would have its content state set to the expression for that cell (which may have been partially edited by the user).

Rendering Variations

Considering the hierarchical and immutable nature of React components, as well as the fact that most spreadsheet operations can affect multiple cells, the simplest alternative is to re-render the entire spreadsheet after any change. Of course, React will attempt to minimize the amount of work done during this re-render, avoiding any unnecessary DOM updates and possibly even re-renders when the content of a subtree has not changed.

Since a typical spreadsheet operation will typically update only a small portion of the 100 cells in our \(10\times10\) spreadsheet, re-rendering all 100 cells seems particularly wasteful. Fortunately, there is an alternative.

  1. The Spreadsheet component passes in a registration callback to a descendant cell; passing in this registration callback will require prop drilling.

  2. Each cell calls the registration function after the first render (this can be done within an effect with empty dependencies). In this call, the cell will pass an argument identifying its cell-id as well as an updater function which allows updating selected parts of the cell's state. This updater function is recorded within some data structure in the spreadsheet component.

  3. When the spreadsheet component finishes its first render, it records the data structure within its state (or a ref).

  4. Subsequently, whenever the spreadsheet wishes to update the state of the cell, it simply calls the updater function.

This pattern allows the spreadsheet to update the state of a descendant cell without requiring a re-render of the complete spreadsheet.

Hints

The following points are worth noting:

  • It is easiest to start this project using your solution to Project 4 or the provided solution (the solution link will not be working at the time this project is assigned but should become functional soon after the expiry of the late submission deadline). You will need to reimplement the code which implements the functionality of the spreadsheet using hooks-based React functional components but can reuse the outer setup code almost without needing to make any changes.

  • React does not play well with content editable content; it is possible to overcome these problems, but for this project it may be a good idea to use an <input> element instead.

    The previous project used <td contentEditable="true"></td> to represent spreadsheet cells. Since this project requires the use of React, it may be easier to replace the use of contentEditable with <td><input .../></td>, where the <input> widget will hold the current display for a spreadsheet cell.

  • If you decide to reuse the DOM-based UI from Project 4 for the form allowing specifying the web service URL and spreadsheet name, then the React app may need to be reloaded whenever the form is reloaded. This could reload the React app into the same root element which will produce React warnings. These warning can be avoided by first creating a new root element into which the spreadsheet is loaded using code like the following:

        //static root of spreadsheet in HTML
        const ss = document.querySelector('#ss')!;
        ss.innerHTML = '';
        //create a new root element to avoid warnings
        const root = document.createElement('div');
        ss.append(root);
        //ws and ssName from form submission;
        //errors instance of Errors from Project 4
        //Spreadsheet a functional React component
        const spreadsheet =
          React.createElement(Spreadsheet, {ws, ssName, errors});
        ReactDom.createRoot(root).render(spreadsheet);
    
  • It is probably best to set up initial code for displaying an empty spreadsheet before you start adding functionality to it.

  • The initial spreadsheet load should be done using an effect which runs when the spreadsheet is first mounted into the DOM.

When submitting, please include a .gitignore which ensures that built artifacts are not checked in to git. If using a set up like Project 4, set up node_modules, dist, target and .parcel-cache to be ignored.