Lab 2: Anchors and Links
Released: September 28th
Due: October 4th at 11:59 PM ET
To get checked off for this lab, meet with a TA at your lab section or TA hours
⚠️ Do not clone the Assignment 2 stencil code until you have submitted Assignment 1 for the last time to Gradescope. We will be checking to make sure you do not submit Assignment 1 after cloning Assignment 2. ⚠️
In the first assignment, we created a basic hypertext system. Now we want to have more in-depth ways of interacting with our nodes. One of the features common to all hypertext systems is the ability to create two objects we will be adding to our system: 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. 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 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 more focused on frontend work, and will involve many 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 development that you will be working with for this assignment.
linkCollectionConnection.deleteLinks()
extents
!You successfully prepped your donut! Now it's time to look over the recipe to see where you're at. Please complete this quick feedback form for Assignment 1. We want to get a good sense of what has gone well and what hasn't, so we can improve on future assignments.
Note: The demo may take up to 30 seconds to load due to the free backend we used for hosting. Additionally, you might notice some strange behavior if there are multiple students interacting with the demo at the same time.
You can find a demo of the unit2 MyHypermedia application here.
You can clone the repository here.
Note: The same repo will be used for Lab 2 and Assignment 2!
ILink
& IAnchor
We highly recommend that you look over the files in server/src/types
as all of the following types are defined there:
IServiceResponse
INode
INodeProperties
INodePath
RecursiveNodeTree
IAnchor
Extent
ILink
We also provide and define useful helper methods related to these types in each of the files. Looking over these now will make your life much easier when you're looking for helper functions in the future.
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:
IAnchor
), andExtent
).We have defined a schema for anchors as the IAnchor
interface:
and theExtent
interfaces:
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:
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.
If you change the port, make sure to also change the endpoint in the client's endpoint.ts
file.
In Unit 1, you got your hands dirty with BackendNodeGateway.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.
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.
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 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 (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 npm run dev
. As you see, we configure a MongoClient
in this file as so:
A few lines below this, we pass this mongoClient
into each of our routers:
This mongoClient
is then passed down from the router
> gateway
> collectionConnection
via their respective constructors. For example for links:
Now that we have the MongoClient
, let's try to write LinkCollectionConnection.deleteLinks()
.
Task 2: We will now implement LinkCollectionConnection.deleteLinks()
First, we need to get the links
collection:
Next, we need to form a MongoDB query.
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 MongoDB Documentation 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 to 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.
Now that we have stored the deleteResponse
, we have to verify whether or not the deletion was successful and return an IServiceResponse
accordingly.
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()
:
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.
In this section, we will introduce two more built-in React hooks that help 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.
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:
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:
useRef
React HookuseRef
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 useRef
, which you be using in later steps of this lab:
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.
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!
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.
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')
Task 3: We will now load the existing randomly generated anchors onto your image using the extent
!
Go to ImageContent.tsx
, and add the div below to the list we created called anchorElementList
.
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!
Here is an example of what we might want to add into the anchorElementList
array. Feel free to edit this!
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.
ITextExtent
First let us look at what an ITextExtent
object looks like.
Imagine we have the following piece of text from PVDonuts.
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.
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.
Task 4:
Go to TextContent.tsx
, and manage the extent for when we want to create a new anchor - which would be when we click Start Link
.
Here are the TODOs:
onPointerUp
method which updates the selected extentuseRef
and onPointerUp
Task 5: Discuss these questions with a partner first, and try to write out an interface for one of the following:
Discuss your findings with the TA!
Just a note that there are other ways to be stylish
when writing HTML 😎 - feel free to use any you'd like!
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 .scss
files to the file format of your choice.
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.
If you want to use Bootstrap on any of the assignments, all you need to do is install it on your client app! Just search online for how to install Bootstrap on Next.js for a tutorial. 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!
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:
linkCollectionConnection.deleteLinks()