Lab 2: Anchors and Links

Released: September 30th, 4:00pm ET
Due: October 14th 11:59pm ET

Introduction

Now, we want to have more in-depth ways of interacting with our file system. One of the inherent features of all hypertext systems is the ability to create anchors and links. As with the Node objects you created in the last assignment, each part of the stack will have a way of interacting with these new objects. As a result, your completed assignment will contain the following for both Anchors and Links: Database Connections, Gateways, Routers, and Frontend Components.

In this lab we are laying out the foundations as we get ready to implement linking and anchoring. Now that you have all completed a basic file system and gone through the complexities of understanding the backend structure for our nodes, this unit will be focused on frontend, and will involve far more design decisions! As always, if you have any questions about anything please reach out to a TA!

Objective: Introduce you to the new types and some techniques for frontend that you will be working with for this assignment.

Important note on Assignment 2: Links & Anchors will be released on Monday at 9am, rather than this Thursday at 6pm. This allow TAs to have more time to make sure the codebase is fully functional and has no issues! We apologize for this inconvenience and will be extending accordingly!

Checklist

Reflecting on Assignment #1

You successfully prepped your donut/doughnut! Now it’s time for looking over the recipe to see where you’re at. Please complete the short and easy feedback form for Assignment #1! We want to get a good sense of what has gone well and what has not, so we can improve for the coming assignments.

Cloning the GitHub Repo

Unlike Unit 1, we will not be using the same repository for lab and the assignment. This is so that you all have the same starting point for the assignment as some of you may implement this lab differently!

You can clone the repository here, eveything else in terms of setup should be the same as Unit 1 once you have it stored locally!

Always remember to run yarn install in both the frontend and backend portions of the code.

New Types - ILink & IAnchor

We highly recommend that you look over the files in server/src/types as all the following types are defined there, doing so will make your life much easier when you are looking for helper functions in the future:

We also provide and define useful helper methods related to these types in each of the files. Again, we highly recommend that you familiarize yourself with each of these types before coding.

Anchors

In the previous assignment, you implemented the file system for nodes, but didn’t actually implement anything regarding selecting content on a node. In order for anchors to work properly, you need to be able to make selections on the content of a node (whether that node is a text document, an image, or a video). Conceptually, you can think about implementing anchors via two distinct components:

  1. a generic component that manages all types of anchors and stores the associated node and other metadata about the anchor (IAnchor), and
  2. specific implementations of anchors that define the location of that anchor for the given node type (Extent).

We have defined a schema for anchors as the IAnchor interface:

export interface IAnchor { anchorId: string nodeId: string // Defines the extent of the anchor in the document, // e.g. start / end characters in a text node. // If extent is null, the anchor points to the node as a whole. extent: Extent | null }

The new Extent interfaces:

// Extent is the combination of 2 interfaces export type Extent = ITextExtent | IImageExtent // Defines the extent of an anchor on a text node export interface ITextExtent { type: 'text' text: string startCharacter: number endCharacter: number } // Defines the extent of an anchor on an image node export interface IImageExtent { type: 'image' // the following 4 numbers will be explained later on left: number top: number width: number height: number }

Once we have a way of creating anchors on our nodes, we need an intuitive way to navigate between them. This is where links come in! Whenever we place an anchor on the content of a node, we want to have the ability to link this anchor to another location. For example, if there is a statistic or quote within the content of a specific node, we want to have the ability to create an anchor on this section, and then link it to an anchor on the node from where this information comes.

The new ILink interface:

export interface ILink { linkId: string explainer: string title: string dateCreated?: Date anchor1Id: string anchor2Id: string }

Backend

Environment Variables

You should know how to do this by now! Use the same .env file that you did for your own MongoDb connection in Unit 1 and add it into your server folder.

DB_URI = <YOUR OWN URI> PORT=5000 TSC_COMPILE_ON_ERROR=true ESLINT_NO_DEV_ERRORS=true

MongoDB Queries

In Unit 1 you got your hands dirty with NodeGateway.ts methods. In Unit 2, you will familiarize yourself with AnchorCollectionConnection.ts and LinkCollectionConnection.ts. These collection connection classes use the mongodb node package to interact with the MongoDB database we created in Assignment 1.

But first, what exactly is MongoDB?

MongoDB is a cross-platform (runs on multiple operating systems), document-oriented database management system (DBMS). MongoDB is also a non-relational, NoSQL database. (SQL is a query language for relational databases, not a database architecture itself, so the NoSQL name is confusing, arguably the category should have been called non-relational.)

It is important to know at a high-level how this type of database operates, as the structure of a MongoDB database is inherently different from relational databases that use SQL. Traditional relational databases that use SQL to perform operations store data with predefined relationships. This is why these types of databases consist of a set of tables with columns and rows - to organize data points using defined relationships for easy access.

Don’t skip this paragraph!

A non-relational, NoSQL database such as Mongo is different. Non-relational databases do not use the tabular schema of rows and columns found in most traditional database systems. Instead, data is stored as JSON-like objects with indexes that allow for constant lookup for certain fields. As a result, these types of databases do not use SQL to perform operations and tend to be more flexible by allowing data to be stored in myriad ways.

MongoDB uses documents that are in JSON-like format, known as BSON, which is the binary encoding of JSON. Node.js and MongoDB work very well together, in part because Mongo uses a JavaScript engine built into the database as JavaScript is good at handling JSON objects.

With Mongo being a non-relational database, it has its own way of storing data. Here are some of the constructs that make up the database structure:

Task 1: First, navigate to server/src/app.ts. This file is what runs when you yarn start. As you see, we configure a MongoClient in this file as so:

// access MongoDB URI from .env file const uri = process.env.DB_URI // instantiate new MongoClient with uri and our recommended settings const mongoClient = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true, }) // connect mongoClient to a MongoDB server mongoClient.connect()

A few lines below this, we pass this mongoClient into each of our routers:

// instantiate new NodeRouter and pass mongoClient const myNodeRouter = new NodeRouter(mongoClient) // (ignore this: it attaches a new router to our express application) app.use('/node', myNodeRouter.getExpressRouter()) // the same is done for the anchor and link routers // [more code for anchor and link routers follow...]

This mongoClient is then passed down from the router > gateway > collectionConnection via their respective constructors. For example for links:

// In LinkRouter.ts: constructor(mongoClient: MongoClient) { this.linkGateway = new LinkGateway(mongoClient) //... } // In LinkGateway.ts: constructor(mongoClient: MongoClient, collectionName?: string) { this.linkCollectionConnection = new LinkCollectionConnection( mongoClient, // If collectionName is undefined, collectionName will be // 'links' by default. We use this for e2e testing when we // create a separate 'e2e-testing-links' collection. You can // think of the '??' operator as a safer alternative to '||' collectionName ?? 'links' ) } // In LinkCollectionConnection.ts constructor(mongoClient: MongoClient, collectionName?: string) { // store mongoClient in this.client as an instance variable this.client = mongoClient // store collectionName in this.collectionName as an instance variable this.collectionName = collectionName ?? 'links' } // We can now use the MongoClient from App.ts in LinkCollectionConnection

Now that we have the MongoClient, let’s try to write LinkCollectionConnection.deleteLinks().

Task 2: Implement LinkCollectionConnection.deleteLinks()

First, we need to get the links collection:

/** * Deletes links when given a list of linkIds. * @param {string[]} linkIds * @return successfulServiceResponse<{}> on success * failureServiceResponse on failure */ async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> { // note: earlier, in the constructor of linkCollectionConnection, // we assign this.collectionName to 'links' and mongoClient to this.client const collection = await this.client.db().collection(this.collectionName) }

Next, we need to form a MongoDB query.

/** * Deletes links when given a list of linkIds. * @param {string[]} linkIds * @return successfulServiceResponse<{}> on success * failureServiceResponse on failure */ async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> { const collection = await this.client.db().collection(this.collectionName) // this query requests all documents where the field 'linkId' matches // some element in 'linkIds' const myQuery = { linkId: { $in: linkIds } } }

Learning MongoDB queries
Over the duration of the semester, you will become familiar with MongoDB queries and will hopefully be able to form basic ones on your own. Here are some basic ones to get you started, but feel free to consult Google at anytime!

Some Basic MongoDB Query Functions
The stencil code already calls getCollection and stores the response in a variable. Below are some of the functions that you can call on the collection variable to interact with the MongoDB database. Feel free to reference the previous assignment’s backend NodeCollectionConnection.ts file to help with your implementation.
collection.findOne(query) - Docs
collection.find(query) - Docs
collection.deleteOne(query) - Docs
collection.deleteMany(query) - Docs
collection.insertMany(query) - Docs
collection.insertOne(query) - Docs

Next, we need to call a MongoDB API call with our newly formed query. Notice that we call await on this method because we are sending a request the MongoDB server that we connected to using mongoClient.connect(). Because they are hosted on a remote server, we must then await a response from the MongoDB server before continuing our program.

/** * Deletes links when given a list of linkIds. * @param {string[]} linkIds * @return successfulServiceResponse<ILink> on success * failureServiceResponse on failure */ async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> { const collection = await this.client.db().collection(this.collectionName) const myQuery = { linkId: { $in: linkIds } } // Here we use the 'deleteMany' function as we want to delete multiple // documents that meet our query const deleteResponse = await collection.deleteMany(myQuery) }

Now that we have stored the deleteResponse, we have to verify whether or not the deletion was successful and return an IServiceResponse accordingly.

/** * Deletes links when given a list of linkIds. * @param {string[]} linkIds * @return successfulServiceResponse<ILink> on success * failureServiceResponse on failure */ async deleteLinks(linkIds: string[]): Promise<IServiceResponse<{}>> { const collection = await this.client.db().collection(this.collectionName) const myQuery = { linkId: { $in: linkIds } } const deleteResponse = await collection.deleteMany(myQuery) // we use result.ok to error check deleteMany if (deleteResponse.result.ok) { return successfulServiceResponse({}) } return failureServiceResponse('Failed to delete links') }
Error checking MongoDB responses

Each MongoDB method call will return a differently shaped response. This means, that the method for error checking a deleteMany reponse may be different to the method for error checking a findOneAndUpdate method. For example, this is how we error check findOneAndUpdate in NodeCollectionConnection.updateNode():

async updateNode( nodeId: string, updatedProperties: Object ): Promise<IServiceResponse<INode>> { const collection = await this.client.db().collection(this.collectionName) const updateResponse = collection.findOneAndUpdate( { nodeId: nodeId }, { $set: updatedProperties }) if (updateResponse.ok && updateResponse.lastErrorObject.n) { return successfulServiceResponse(updateResponse.value) } return failureServiceResponse( 'Failed to update node, lastErrorObject: ' + updateResponse.lastErrorObject.toString() ) }

We highly recommend looking at NodeCollectionConnection.ts for tips on how to form MongoDB queries. E.g. how to query a nested field, how to update documents, etc.

Frontend

Environment Variables

Please add the following to your frontend’s .env file (client/src/.env).

TSC_COMPILE_ON_ERROR=true ESLINT_NO_DEV_ERRORS=true

Improving performance in React

In this section, we will introduce a two more built-in React hooks that help writing React code to reduce unnecessary rerendering. They essentially memoize your functions (useCallback) or your values (useMemo).

There are also a few “Code Review” info blocks that give software engineering tips and best practices, sort of like what you might receive from a code review at a company.

useCallback

You may remember that a callback is simply a function passed as an argument. Whenever you pass a function as an argument, it’s best to wrap it in a useCallback to make sure it only updates when needed.

// Example without useCallback const MyParentComponent = () => { const [selectedNode, setSelectedNode] = useState<INode>(...); const handleNodeClick = (node: INode) => { setSelectedNode(node) } return ( <MyChildComponent onNodeClick={handleNodeClick} /> ) }
// Example with useCallback const MyParentComponent = () => { const [selectedNode, setSelectedNode] = useState<INode>(...); const handleNodeClick = useCallback((node: INode) => { setSelectedNode(node) }, [setSelectedNode]) return ( <MyChildComponent onNodeClick={handleNodeClick} /> ) }

In the first example, a new reference to handleNodeClick is created every time MyParentComponent rerenders. Since we pass handleNodeClick as a prop to MyChildComponent, we will also make MyChildComponent rerender, which is inefficient. That’s where useCallback comes in!

The first argument to useCallback is the function you want to wrap. The second argument is the dependency array, which is important and required. In the example above, the dependency array tells React to only change handleNodeClick if setSelectedNode changes.

Code Review: The example above also illustrates a common naming pattern in React. Our child component has a prop with a name like onNounVerb that will be called when some event happens in the child component (like a button click). We then write a function with a name like handleNounVerb that handles the event, which we pass into the child component.

useMemo

If we want to memoize a value instead of a callback function, we can use useMemo. It has a similar interface, including the dependency array:

const selectedNodeTitle: string = useMemo(() => { return selectedNode.title; }, [selectedNode])

In this example, selectedNode is a state variable, and we only want selectedNodeTitle to change when selectedNode changes. Often times, we will use useMemo to store values that we derive from state variables.

Code Review: As a rule of thumb, you should store as little data as possible in state variables, and derive your other data from the state. For instance, in the above example, we wouldn’t want to store selectedNodeTitle as its own state variable, since we’d then have to remember to update it.

Dependency arrays are very important! We have enabled a linter rule called exhaustive-deps that will make sure that every value that’s used in the function is included in the dependency array, preventing a wide array of bugs.

For example, the following code would error:

const selectedNodeTitle: string = useMemo(() => { return selectedNode.title; }, []) // eslint error, since selectedNodeTitle will not update // if selectedNode changes

To what extent is an Extent

When we make a link from a document, we don’t necessarily always want to link from the entire document. For example if we are linking from a PVDonut’s menu, we may want to link from just one particular donut - rather than the entire menu, for that we need to create extents.

In this lab you will be implementing selecting an extent on both text and image nodes. In order to create links, you need to know how to create the anchor that you are going to link from!

useRef React Hook

useRef returns a mutable ref object whose .current property is initialized to the passed argument (initialValue in the example below). The returned object will persist for the full lifetime of the component.

const refDonut = useRef(initialValue);

Note that the initial value we pass into useRef is often null.

Here’s a common use case for use ref, which you be using in later steps of this lab:

// this is a functional component called ChocolateDonut function ChocolateDonut() { const refDonut = useRef(null); const handleMakeChocolateClick = () => { // `current` points to the mounted div with className="donut" refDonut.current.style.backgroundColor = "chocolate" }; return ( <> <div className="donut" ref={refDonut}/> <Button icon={<fa.FaDonut/>} onClick={onMakeChocolateDonut}> Focus the input </Button> </> ); }

Essentially, useRef is like a “box” that can hold a mutable value in its .current property.

You might be familiar with refs primarily as a way to access the DOM. If you pass a ref object to React with <div ref={myRef} />, React will set its .current property to the corresponding DOM node whenever that node changes.

Extents in Images: IImageExtent

An extent is basically the terminology that we use to say we want to specify this part of a node. For an image node, that could be a selection of a particular person. For example we can see in the image below we have a selection over the two table tennis tables. These are anchors, but within anchors the extent is what handles where exactly on the image the anchor should be!

Given a list of anchors, you will be generating these visual indicators of where exactly the anchor is. Note that if extent is null, then we are linking from the entire image.

// Defines the extent of an anchor on an image node export interface IImageExtent { type: 'image' left: number top: number width: number height: number }

Since anchor.extent can be both an image or text type, we want to check that it is the correct type before we access the properties (eg. top, width) which will exist on IImageExtent but not ITextExtent. We can do that with the following condition:
if (anchor.extent?.type == 'image')

Here is an example of what we might want to add into the anchorElementList array. Feel free to edit this!

<div
  key={'image.' + anchor.anchorId}
  className="image-anchor"
  style={{
    width: anchor.extent.width,
    height: anchor.extent.height,
    top: anchor.extent.top,
    left: anchor.extent.left,
  }}
/>

Notice that we have a key property, this is because when we render the same property over again in our HTML DOM, it needs to have a unique key.

Task 3: Load the existing randomly generated anchors onto your image using the extent!

Note: If selecting a region on an image does not work, then change the divider in the onMove method. This is annoyance of having screens with different resolutions!

Extents in Text: ITextExtent

First let us look at what an ITextExtent object looks like.

// Defines the extent of an anchor on a text node export interface ITextExtent { type: 'text' startCharacter: number endCharacter: number text?: string // optional property to store text on extent }

Imagine we have the following piece of text from PVDonuts. As you can see there are already existing links that are underlined and blue. In our implementation, we would store this as an anchor with the extent defined as ITextExtent. This has a startCharacter and an endCharacter relative to paragraph. We want our anchors to have access to that information so that when we load our text we can visually show where our anchors are.

We’re not your typical donut shop. We’re a bit over the top.
We officially hit the scene when we opened our doors in 2016, but we’ve been experimenting since 2014. We’re most popular for introducing brioche style donuts to the New England area, and they’re what makes us unique. We also offer old fashioned, filled brioche, cake, crullers, fritters, and more. All of our signature styles are made, rolled, cut, dipped and decorated by hand each day.

Task 4: Go to TextContent.tsx, and read the to-dos for more information on each step. You can refer to ImageContent.tsx if you are in doubt or ask a TA!
Manage the extent for when we want to create a new anchor - which would be when we click Start Link. To check your implementation is correct click Start Link and check that the alert that pops up is the expected Extent (Note: It should not be undefined)

Here are the TODOs:

  1. Add a use ref so that we can keep track of the text content
  2. Add an onPointerUp method which updates the selected extent
  3. Set the selected extent to null when this component loads
  4. Update the textContent-wrapper HTML Element so that it works with useRef and onPointerUp

[Optional] Tricky Optional Task 4.1:
Display the anchors in the text node. You do not need to worry about our anchors being clickable yet (that will come in the assignment itself)

  1. Write a method where we display the existing anchors.

Extents on other document types

Task 5: Discuss these questions with a partner, try and write out an interface for each of these 2 of the following:

Styling

Just a note that there are other ways to be stylish when writing HTML 😎 - feel free to use any you’d like!

SCSS

This is what you have used so far - but there are many other ways to style - feel free to switch it up, or continue using scss. We just want to introduce you to some other ways of styling your web app!

If you decide to use an SCSS alternative, you are responsible for converting all .sass files to the file format of your choice.

Styling with Bootstrap

Sometimes, we want to be able to style our elements without creating an entire new .scss file. For many common styles like padding, margin, and flex-box, you can just add a Bootstrap utility class.

We’ve added Bootstrap to our assignment repos, so the classes are ready to use! For padding and margin, there are defined levels (i.e. 1, 2, 3, 4, 5) that correspond to consistent values (i.e. 4px, 8px, 16px, 24px, 32px).
Having consistently scaling spacing values is generally good practice in design!

Padding

// Padding level 3 on all sides <div className="p-3">Hello!</div> // Padding level 2 on the left <div className="pl-2">Hello!</div> // Padding level 4 on the right <div className="pr-4">Hello!</div> // Padding level 1 on the left and right <div className="px-1">Hello!</div> // Padding level 1 on the top and bottom <div className="py-1">Hello!</div>

Margin

// Margin level 3 on all sides <div className="m-3">Hello!</div> // Margin level 2 on the left <div className="ml-2">Hello!</div> // Margin level 4 on the right <div className="mr-4">Hello!</div> // Margin level 1 on the left and right <div className="mx-1">Hello!</div> // Margin level 1 on the top and bottom <div className="my-1">Hello!</div>

Flexbox

Flexbox allows you to create responsive containers that nicely lay out your elements. You can create flexboxs that are horizontal (flex-row, the default) or vertical (flex-column).

For a great explanation and resource on flexbox, see this guide:
https://css-tricks.com/snippets/css/a-guide-to-flexbox/

Bootstrap flexbox classnames are pretty straightforward; here’s some common ones:

d-flex (this means display: flex)
flex-row
flex-column
align-items-center
justify-content-center

Checkoff