Released: September 13th, 2023
Due: September 20th, 2023 at 11:59 PM ET
To get checked off for this lab, meet with a TA at your lab section or TA hours
You're overwhelmed by the vast collection of donuts available. You’re lost, and unfortunately there is no way to traverse the universe of donuts. You’re unable to manage the mess of hyperdonuts and find yourself lost in hyperspace! Looks like you’ll need to come up with a means of orienting yourself to find the most scrumptious donut.
In the Nodes
unit, you will create a node file system - a standard way of managing and navigating between Hypertext nodes. At the end, you will have built a full stack application that will allow users to create, read, delete, and move hypermedia nodes.
Lab Objective: The objective for this lab is to explore the MERN stack from frontend to backend. You will go through the entire process of connecting to a MongoDB database, creating and inserting a node into the Mongo database, using Express middleware to make calls between the frontend and backend, and lastly visualizing the node's content and other relevant information as a React component in your web application.
Nodes
GitHub Classroom assignmentBackground Information
createNode
by making changes in the following files
NodeRouter.ts
NodeGateway.ts
NodeCollectionConnection.ts
NodeGateway.ts
text
and image
nodes.Visit our course Gradescope page and submit the "Hypothesis Annotations" assignment with your username that you used for you annotations this past week.
For Lab 1 and Assignment 1, you will be using the same repository. Accept the assignment here. Once you’ve accepted your GitHub repo, clone it locally, and you should be ready to go!
Cloning your repo locally:
git clone <link>
Add all changes in the working directory to the staging area:
git add -A
Create new commit from the files in the staging area:
git commit -m '<commit message>'
Update the remote branch with all local commits:
git push
Note: The demo may take up to 30 seconds to load due to the free backend we used fo 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 MyHypermedia application here.
These are some important concepts that you will come across during the lab. We understand that they can be confusing at first, so please ask if anything doesn't make sense once you use it!
This diagram shows the hierarchy of our full stack application. The interactions between each of the components of our system may seem complex at first, so refer to this diagram when implementing different parts of the assignment.
The frontend code uses the React framework, written in TypeScript and models the interactive part of our web application that the user will see. The React application is compiled into plain HTML, CSS and Javascript and is then hosted by a server at a specific endpoint (i.e. "http/my-web-app.com") - in this course we will use Vercel to host our frontend.
Our frontend, like most frontends, must fetch relevant data to show the user. For example, when a user searches for a video on YouTube, YouTube's frontend will need to fetch the appropriate search results to render onto the users browser. This is where the backend comes into play.
The backend part of our web application is responsible for listening to requests from the frontend and responds by providing the relevant processed data. Our backend will use Express.js to configure the endpoints at which the backend can service specific requests. For example, "http/my-web-app.com/search" might be configured to respond so that the frontend can process all search requests by issuing a HTTP POST
request to that endpoint. We will use Node.js with TypeScript to implement our backend logic.
Note: You will only implement the orange bordered sections in this lab. In Assignment 1, you will be creating your own Mongo database!
The Hypertext Transfer Protocol (HTTP) is designed to enable communication between clients and servers. An HTTP request is made by a client to a named host, which is located on a server. The aim of the request is to access a resource on the server.
In this assignment, you will use four different types of HTTP requests:
HTTP Request | Explanation | Example (let us say the URL/base endpoint is https://example.com) |
---|---|---|
GET | Used for retrieving data | https://example.com/node/get/:nodeid |
POST | Used for create methods | https://example.com/node/create |
PUT | Used for update or replace methods | https://example.com/node/move/:nodeId/:newParentId |
DELETE | Used for deletion of data. | https://example.com/node/:nodeid |
URL params: The :
indicates that the following value is a URL parameter. In other words, :nodeId
is a placeholder that indicates a nodeId
should be placed in its stead.
Note: The definitions and use-cases for each of these request types are flexible. There are industry standards and guidelines but at this stage, we don't need to know anything more than what is shown in the table above.
JavaScript is single-threaded, which means it only has one call stack. Using asynchronous functions, we can make time-intensive network requests without blocking the call stack.
What is "asynchronous"?
Simply put, it means that JavaScript can move on to the next line of code before the previous line finishes executing. Here is an example:
If you run this snippet of code in an editor, the console would produce the following message:
When we tell JavaScript to hold off for 3 seconds to print the second print
, JavaScript didn't wait. JavaScript added it into its "task list", marked it with execute this task 3 seconds later, and went ahead to execute the third task, which is printing third print
. third print
got printed immediately, and that's why second print
comes after the third print
. With the same reasoning, do you think the following snippet would achieve it's goal?
In fact, it doesn't do what we want. Instead, it printed out
[object Promise] { ... }
in the console. Why? Becuase JavaScript didn't wait, and printed out whatever apiResult
was before the fetch completed. You see, apiResult
is a Promise - a Promise is effectively a wrapper around the actual return type, denoting that JavaScript is expecting a object to be returned, and promises that the object will either be returned (resolved), or it will raise an error (rejected). It takes time to get the Promise resolved. In TypeScript, we denote a promise by Promise<T>
, where T
refers to some generic type (could be a string
, object
, INode
, etc.).
async
function & await
keywordasync
and await
allow you to write asynchronous code the same way you write synchonous code
async
functionFirst of all we have the async
keyword, which you put in front of a function declaration to turn it into an async function.
async
function vs regular functionRegular function:
async
Typescript function:
This is one of the traits of async functions — their return values are guaranteed to be converted to promises. In the second example, the return value would be of type Promise<string>
. So the async keyword is added to functions to tell them to return a promise rather than directly returning the value.
await
keywordThe advantage of an async
function only becomes apparent when you combine it with the await keyword. await
only works inside async
functions.
await
can be put in front of any async promise-based function to pause your code on that line until the promise fulfills, then return the resulting value.
Adding await
in front of a function, for example when we are retrieving from the database, is essentially telling JavaScript to wait until you have a response from the promise.
Now, you should know how to properly print the data that the GitHub API returns:
You can learn more about the JavaScript event loop here: What the heck is the event loop anyway?
In the src
folder on both the server
and client
you will find a types
folder. This contains all of the interfaces that our hypermedia system will use.
Note: We will be using a naming convention where all TypeScript interfaces are prefaced with an I
for interface. This helps code readability when you have both the interface and the implementation of that interface referenced in one file.
INode
A node is a generic representation of content in our hypertext corpus. In this assignment, we will only be implementing a generic node service which manages the location of nodes in a file tree, and supports basic content for text
and image
nodes. To be clear, we are making a notable design decision by choosing to organize nodes via a file tree. This isn’t the only option when building a hypertext system, but it is what you will be implementing in this assignment. At its core, any file tree is just a hierarchy of nodes. Let’s start with how we are defining “node” in the context of our Hypertext corpus:
The ?
in dateCreated?
means that it is an optional field in the INode
interface. This means that an INode
object can either have that field or not.
INodePath
The node path type is a simple interface which allows you to easily access an INode
's location. This is important, as you will frequently interact with node paths. Here is the definition for the INodePath
interface:
Example:
A root node with nodeId root
who has two children, child1
and child2
would have the following nodePath
:
{ path: ['root'], children: ['child1, child2'] }
and the child1
node will have the following nodePath
:
{ path: ['root', 'child1'], children: [] }
The children
array will only contain the nodeId
s of their immediate children - which excludes grandchildren or great grandchildren.
IServiceResponse
The IServiceResponse
is an interface we’ve created for this course to make it easier to understand if a function succeeds or fails. If a function fails, the ServiceResponse
will be a failureServiceResponse
that contain an error message. If it succeeds, the ServiceResponse will be a successfulServiceResponse
that contains a payload of type T
, which is a generic type that could be anything (eg. string, INode
, etc.) indicated in the angular brackets.
The IServiceResponse
interface enables us to easily determine if a function succeeded, and if it fails, to pass an error message to the calling function.
Here we see that if createNode()
executes successfully, it will return a ServiceResponse
with a payload of the created node (of type INode
).
Once you cd
into the server
folder, you will find the following file structure.
NodeRouter.ts
On the backend, you will be using the Express router found in NodeRouter.ts
to receive HTTP requests from the frontend, make corresponding changes to the database, and give the frontend updated data to show the user. Once receiving an HTTP request, the NodeRouter
will call on an appropriate method in BackendNodeGateway
.
Note: NodeRouter
listens to all HTTP requests to /node
, therefore NodeExpressRouter.post('/create', async...{})
actually corresponds to /node/create
. For a more detailed explanation, please look at server/src/app.ts
to see how NodeRouter
is configured.
BackendNodeGateway.ts
BackendNodeGateway
handles requests from NodeRouter
, and calls on methods in NodeCollectionConnection
to interact with the database. It contains the complex logic to check whether the request is valid, before modifying the database.
For example, before insertion, BackendNodeGateway.createNode()
will check whether the database already contains a node with the same nodeId
, as well as the the validity of node's file path. The NodeCollectionConnection.insertNode()
method, on the otherhand, simply receives the node object, and inserts it into the database.
NodeCollectionConnection.ts
NodeCollectionConnection
acts as an in-between communicator between the nodes
collection in the MongoDB database and NodeGateway
. NodeCollectionConnection
defines methods that interact directly with MongoDB. That said, it does not include any of the complex logic that NodeGateway
has.
For example, NodeCollectionConnection.deleteNode()
will only delete a single node. Whereas NodeGateway.deleteNode()
deletes all its children from the database as well.
Note: In the backend part of this lab you will implement /node/create
in NodeRouter.ts
, createNode
in BackendNodeGateway.ts
and insertNode
in nodeCollectionConnection.ts
. This is to give you a better understanding of the Express, Node.js and MongoDB stack end-to-end. In Assignment 1 however, to make the workload more managable, you will only be required to implement methods in NodeGateway.ts
as it handles the majority of the logic.
To permanently store our Hypermedia nodes such that they can be accessed using our web application no matter what computer they are on, we need to use a database service. In this class, we will be using MongoDB to store our data.
For this lab, you will be connecting to a database that we have created for you - a database that is shared with everyone in the course. In Assignment 1, you will create and deploy your own MongoDB database!
TODO:
cd
into your server
directory and create a new .env
file. Its filepath should be: server/.env
.env
file:So what did we just do with this .env
file? Well, to connect to a database, MongoDB requires a URI that contains the unique address of the database, a username and password. This way, only those with the correct login details and unique database address can access a database. DB_URI
in this .env
file is that URI - 'student' is the username, 'cs1951v' is the password and '@cs1951v-cluster.tjluq.mongodb.net/cs1951v-database.' is our database's unique address. We store this information in a .env
file so that this information does not lie in source code, code that is pushed online for potentially many to see. We use the dotenv
node package to access the URI stored in this .env
. Note, we have added .env
to our .gitignore
file so that private security details are not pushed online.
Before we start writing backend code, it's important check if your backend server runs:
TODO:
Make sure that you are in the server directory. Run the following npm
scripts to install the relevant dependencies and start the server:
npm install
npm run dev
To verify that your server is running, if you go to http://localhost:8000
in your browser where you should see MyHypermedia Backend Service
. If you don't see this message appear in your browser, please reach out to a TA!
In this lab, you will be implementing the functionality for creating a node, starting with the backend and then finally rendering it as a React component on the frontend. The image below is a high level overview of how we will create a node on the backend.
In this lab, we want to insert both a text and image node into our database describing (debatably) the most delicious donut in the world, the Boston Cream Donut.
INode
JSON ObjectBased on the INode
interface that we introduced above, this is how a node like this would look when written as an object:
Note: nodeId
is normally a randomly generated globally unique id (guid) on the client side; In this course, we use image.<guid>
to represent some such unique id on an image node.
TODO:
INode
object with "type": "text"
(in an empty txt file), the content of it could be anything you would like, for example, it could be a bio that explains the Boston Cream Donut. The JSON should be formatted in the same way as the Boston Cream Donut
INode shown above.
Boston Cream Donut
INode above a new guid such that:
"nodeId": "image.<new-guid>"
"filePath": {"path": ["image.<new-guid>"], ...}
Keep a hold of these JSON objects in a text file or anywhere that is most convenient for you - we will be needing them soon.
So why did we just create these random JSON objects? Well, servers and clients communicate with each other over HTTP requests and responses all the time. Sometimes, they need to send data in the form of objects and we oftentimes use the JSON (JavaScript Object Notation) format to represent this data.
In this lab, we will be sending our backend these 2 JSON objects attached to separate HTTP requests. Our backend server will then read this data and convert these JSON objects into a proper INode
object in our TypeScript environment.
Now, onto creating those HTTP requests to send to our backend!
Postman is a free tool which helps developers run and debug API requests - in other words, it simulates a client and send HTTP requests to our NodeRouter
. This is a quick and easy way to make HTTP requests when you don't have a client set up yet and want to interact with your backend server.
TODO:
My Workspaces
, create a new collection called cs1951v
and add a new request to this collection. Then copy the image
INode JSON object from Step 3 and ensure your Postman setup looks as follows…
nodeId
and filePath
fields reflect this change. Go back to TODO #2
in Step 3 if you have not done so yet.Your Postman should be set up such that the following is true:
POST
selected as the request typehttp://localhost:8000/node/create
Body
, raw
, and JSON
selected in the respective places.TODO:
Once everything is set up - click Send
. This will send the HTTP POST request to the backend server that you are running locally at localhost:8000
.
Oh no! You received a FailureServiceResponse
object,[NodeRouter] node/create not implemented
. Move on to Step 5 to make sure that our server accepts the POST request!
NodeRouter.ts
To implement the post method on the Express router for /node/create
navigate to server/src/nodes/NodeRouter.ts
. You can see that nearly all methods in the file take in two parameters, res
and req
:
req
is a Request
object that we receive from the client.res
is a Response
object that we send back to the client indicating the status of the request along with an optional JSON payload that contains any requested information.As we saw in Postman (Step 4), we are sending a POST request to http://localhost:8000/node/create. Therefore we need to set up our router to listen to a POST request at http://localhost:8000/node/create:
Note: NodeRouter
listens to all HTTP requests to /node
, therefore NodeExpressRouter.post('/create', async...{})
actually corresponds to /node/create
. For a more detailed explanation, please look at server/src/app.ts
to see how NodeRouter
is configured.
Now that we have the router listening to the proper route, let's read the JSON object that was attached to our Postman's POST request:
Using console.log()
to verify that node
has been received:
console.log(node)
after line 2cd server
, npm install
and npm run dev
Awesome! Our HTTP POST request sent via Postman was received and our NodeRouter has succesfully parsed the JSON INode object attached to the POST request.
Now let's check to see if node
is of type node. We use isINode()
to do this. Make sure to add isINode
from the import '../types' up at the top of the file.
Let's fill in the HTTP status code into our response.
Now, if the node is an INode
, we should call on NodeGateway
method to insert it into database.
Almost there - to make sure our router doesn't fail on unexpected errors, let's add a try/catch
to make sure (try/catch
is a common programming pattern that allows catching and handling errors).
For our NodeRouter
methods we want to use a try/catch
block so we can catch any uncaught errors and return that error message to the frontend. Note, for all caught errors, our backend should return a failureServiceResponse
.
Note: failureServiceResponse
is a method that we have created in IServiceResponse.ts
to return an IServiceResponse
object where success: false
. The same is true for successfulServiceResponse
except that success: true
.
Please have a look at server/src/types/IServiceResponse.ts
for the actual schema of IServiceResponse
objects.
BackendNodeGateway.ts
Now that we have written /create
for NodeRouter
and called BackendNodeGateway.createNode()
, let's move onto the next step and write BackendNodeGateway.createNode()
.
We start with an empty method:
First, we want to check if our argument is of type INode
. As a rule of thumb, each method is responsible for error checking its own inputs. Let's return a failureServiceResponse
with an appropriate failure message if node
is not a valid INode
.
Next, we want to check if a node with node.nodeId
already exists in the database. We don't want duplicate nodeId
's in the database. Let's use NodeCollectionConnection
's findNodeById()
method which has already been written for us to verify if nodeId
already exists in the database. We return a failureServiceResponse
if a duplicate already exists.
The nodes are organized in a tree structure such that root nodes contain child nodes that then contain other child nodes, etc. This means that when we are creating a node, we want to make sure that the parent that we are attaching this new node to exists. We can access the node's parentId by indexing node.filePath.path[nodePath.path.length - 2]
. Note that root nodes will not have a parentId
. Let's put this into code:
Now if our node's filePath
disagrees with its parent's filePath
, we want to send a failure response:
Now we need to add node.nodeId
to the parent's children
field.
And finally, once all checks have been completed and the parent node's filePath.children
field has been updated, we can insert this new node into the database using NodeCollectionConnection.insertNode()
. We then return the resulting response so NodeCollectionConnection
's response is passed onto NodeRouter
.
Great! We just finished BackendNodeGateway.createNode()
. Let's move onto NodeCollectionConnection.insertNode()
.
NodeCollectionConnection.ts
Finally, head over to NodeCollectionConnection.ts and find the insertNode() method. Last thing we need to do in the backend, is to interface with the database! Let's start by checking whether the input is a valid INode
.
After this, let's call the relevant MongoDB function, and return the inserted document on success.
Now that we have a working backend that accepts INode
objects from HTTP requests, we should make sure that our client is able to send those HTTP requests.
Once we have a way of creating nodes, we will be implementing two simple React components in this lab to introduce a few key concepts: props, rendering, and components as functions.
The blue elements are what you will be working on in this lab and assignment. You can search for a file in VSCode using Cmd+P
(Mac) or Ctrl+P
(Windows).
React is a frontend Javascript library for building isolated and state-aware UI components. This makes it ideal for building high performance modern web applications.
Some examples of webpages that you may have used that use React include:
If you're new to React or want a refresher, you might find this tutorial helpful.
A component is one of the core building blocks of React. In other words, we can say that every application you will develop in React will be made up of pieces called components. Components make the task of building UIs much easier. You can see a UI broken down into multiple individual pieces called components and work on them independently and merge them all in a parent component which will be your final UI.
The beauty of React is that when we change the state of a component, rather than re-rendering the entire UI's state, it only updates the component's state. This is what we mean by component isolation.
In this image we have broken down the React UI Components of the MyHypermedia Web App that you will be implementing.
In our stencil code, each React component is organized into a folder. Let us say we had a Donut
component. Then it would be a folder called Donut
which contains the following files:
Filename | Case Style | Description |
---|---|---|
Donut.tsx |
PascalCase | Handles the rendering of the component itself and the creation of the JSX object. |
donutUtils.ts/.tsx |
camelCase | Code that does not depend on useState or useEffect is factored into a utils file. |
Donut.scss |
PascalCase | SCSS file with everything relevant to the Donut component that is rendered. |
index.ts |
camelCase | File that exports all of the relevant functions & components to avoid lengthy and messy import calls. (eg. import { oreo } from '../../../../../../../donuts/chocolate/oreo' ) |
Before we start writing frontend code, it's important to check that your client runs as expected.
TODO:
Run the following npm
scripts to install the relevant dependencies and start the React services:
cd client
npm install
npm run dev
To verify that your React service is running, if you go to http://localhost:3000
in your browser then you should see the client web app that we have built MyHypermedia
. If the web app does not load, please shout "TAaaa!!" (in Slack or in CIT219)
createNode
in FrontendNodeGateway.ts
We want to easily send and retrieve data from our database using our frontend app. Users will interact with data through buttons, forms etc. and we need a way to retrieve data. For that we will use FrontendNodeGateway
to make HTTP requests.
We use Axios to make our HTTP requests, which offers an easier and more powerful way to make requests compared to the more basic Fetch API. Axios is a promise-based React library compatible with Node.js and your browser to make asynchronous JavaScript HTTP requests. All our requests are contained in requests.ts
. For createNode
we want to use a POST method since we are sending data from the client to the server!
Now we need to define the arguments that we are passing into the POST request:
url
:
When you are running it locally the baseEndpoint
variable http://localhost:8000
.
Since this NodeGateway
only sends HTTP requests to /node...
we use the servicePath
variable to define the permanent prefix attached to the route.
Thus, our URL should be:
baseEndpoint + servicePath + '/create'
body
:
The body of the POST HTTP request should be the INode
object that we are creating.
Finally, we arrive at the final method, which returns a IServiceResponse
Promise.
We can now access that from anywhere in our client
to make an API request to create a node!
You can look into the createNodeFromModal
method in createNodeUtils.ts
to see an example!
Before we do this step, we will introduce two very important hooks in React!
We will be using functional components with React hooks in this course as opposed to the traditional class components in React. We do this because functional components requires much less boilerplate code, and are now the industry standard.
First, let's talk about React hooks. You can identify a React hook by its names - they usually have use
as a prefix to their name. Essentially, React Hooks deal with manipulating the state, or the value, of the variable. With hooks, React will automatically rerender (update) the components that contain this variable.
useState
useState
is the most common hook, and it deals with creating reactive variables - whenever reactive variables get updated, React will automatically re-render the component with the updated value. Imagine we want to have a hitCounter
variable that represents the number of button clicks. We can declare it with the useState
hook:
The first word, const
, is the reserved keyword for declaring a new constant variable. However, we are actually declaring 2 things here: a hitCounter
variable of type number
, and a function setHitCounter
that is used to mutate the value of hitCounter
. Both of these are returned from the useState hook. On the right side of the equal sign, we are calling the useState
hook with the parameter 0
- that indicates that the hitCounter
will be initialized at the value of 0
.
So we've used useState
, is that it? Let's see how we can use these 2 variables to achieve our goal - building a hit counter!
Can you visualize what it would look like? If rendered in browser, there would probably be a Count: 0
along with a button that says Hit Me!
. Let's observe what is written inside the <button>
.
We see that there is an onClick
event handler written inside the <button>
div. Whatever is written inside the onClick
will be called when user clicks the button. In this case, we are calling setHitCounter
with the parameter hitCounter + 1
to increment hitCounter
everytime this button is pressed. hitCounter
's current value is 0
. With some math knowledge, we know that we are effectively putting 1
as an argument into the setHitCounter
; not surprisingly, React will now update hitCounter
's value to 1
, and the DOM will show Count: 1
. If we keep clicking the button, this process will be repeated, and we can see the hitCounter
value to steadily increase.
Note: DOM stands for Document Object Model, and it's a structured representation of the HTML elements that are present in a web page. It essentially represents the entire UI of your application.
To recap, useState
instantiates a variable with a default value, and React provides us with a mutator to change the variable's value. Every update to variable will cause a rerender of the component.
Note that the value of the hitCounter
will be restored to 0 when we refresh the page. That is the anticipated behavior. If we want to make the data persistent through page refresh, we can use a database, or utilize the browser's local storage.
Global State Management
You might notice that the stencil code sometimes uses useRecoilState
instead of useState
. This is because large applications with many state variables that need to be accessed by multiple components can quickly become cluttered and buggy.
That's where global state management comes in! The Recoil state management system uses the same basic principles as useState
hooks, but allows for easy access to the same state variable across components. Global state variables are defined in atom.tsx
, and to use them within a component, we can use:
useRecoilState
to access the value and mutatoruseRecoilValue
to only access the valueuseSetRecoilState
to only access the mutatoruseEffect
Now let's learn another simple hook - useEffect
. We can treat this hook as an if changed
statement. Let's see what the useEffect
hook looks like.
For readability, we formatted the useEffect
into one-line and commented on each part. A useEffect
hook takes in 2 parameters, the first one being a callback function, and the second parameter is a dependency array.
Callback Function
A callback function is a function that's passed as an argument to another function.
Dependency Array
A dependency array is essentially the if
part of the useEffect
hook. We can put variables of different types into the dependency array, and whenever any variable in the dependency array gets updated, the callback function will be executed. For example, in the code snippet above, whenever hitCounter
is updated, the callback function will print to the console. You can put as many variables as you want into the dependency array.
There are 2 special cases for the dependency array:
useEffect(() => { console.log("hello!) }, [])
, it means the callback function will only be called once on component creation. When we want to make an API call when the page / component loads, this mode comes in handy.useEffect(() => { console.log("change!")})
, the callback function will be executed whenever the component is re-rendered.With this knowledge, what does this snippet of code below do?
Whenever the button is clicked, the value of the hitCounter
will be changed, triggering a useEffect
call. The useEffect
will print a string to console.
Basic Rules of React Hooks
if
conditions, or nested functions.Now that we've gone over useState
and useEffect
hooks, let's go back to the codebase and implement the create
button to open the modal. We use a React UI package called Chakra to render our createNodeModal.
Navigate to MainView.tsx
, this is where we render the components and handle logic to load all of the components and the nodes from our database. We have divided the file into 6 sections, they are as follows:
useState
hooksuseEffect
hooksStep 1: Notice the following useState
hook in [1]:
We use a state variable called createNodeModalOpen
to keep track of state of the CreateNodeModal
.
If the createNodeModalOpen
variable changes, our MainView
component will know to re-render the components that depend on the createNodeModalOpen
, but how do we change the variable? For that we need to use the setCreateNodeModdalOpen
function from our useState
hook which updates the variable!
Step 2: Go to [6] JSX Component in the file. Since the createNode button is located in the Header
component we need the Header
component to be able to access and call the setCreateNodeModalOpen
function. How do we do this? We pass it in to the component as a prop
, one of the arguments of the component.
We'll go into more detail on components and props in Step 3!
We should update the onCreateNodeButtonClick
prop in the Header
component so that it looks as follows:
Now our Header
component takes in the method that updates our variable. It knows what to do when the create button is clicked! Try it out, and try creating a node with the CreateNodeModal!
Congratulations! You've successfully implemented the create node functionality, from the backend all the way to the frontend!
ImageContent.tsx
Now that we are able to create a node, what about viewing it? Sure, we have an INode
object with fields, but how useful is that?
This is what our Boston Cream Donut
currently looks like; not so delicious:
We need to take the INode
object and turn it into something useful. Start by navigating to ImageContent.tsx
.
Here, we see the basic structure of a React component: it's just a function!
Within this file, you will see that we use a global state management tool called recoil
in the form of useRecoilValue(). Here, we retrieve the currentNode, which is an INode
that contains a list of INode
properties that we can then access and use.
We will need currentNode.content
to render our image. You can do this by returning an <img />
tag with the src
set to our image url (which is our content
!).
It might surprise you to see an 'HTML tag' in a Javascript function. This is really a JSX tag rather than an HTML tag. JSX is an extension of Javascript that is similar to HTML, but also allows for JavaScript expressions. We use {...}
to denote JavaScript when we are within a JSX element.
For example:
Did you notice that <img>
tag can be written in 2 formats?
In JSX, which React uses, you can either use a traditional <img src='...'></img>
to render an image, or to use a self-closing tag that looks like <img src='...' />
. These are equivalent expressions and either format is accepted.
TextContent.tsx
Now we can implement our text content component in TextContent.tsx
. The content of text nodes is simpler to render: it's just a string
! You can render a JavaScript string
by wrapping it in curly braces and putting it in an outer tag (like a div
).
For example:
Once you render your text content, you should be see the content text when you click on a text node. Now, you'll get some practice styling React components using .scss
files!
Styling with SCSS (.scss
)
Throughout this class we will be using .scss
files instead of .css
files.
SCSS files allows us to write SASS, which is an extension of CSS (Cascading Style Sheets) that adds functionality like variables and nesting. Because of these additional features, SCSS files are often used instead of CSS files in industry.
Note: While there are other alternatives for styling your React components, we'll be sticking with SCSS for this course. You can use other CSS styling methods such as tailwind
and emotion
if you choose, but the TAs won't be able to offer support for these.
In the same directory, go to TextContent.scss
. Define a SCSS class for your text content – the syntax is like this (though the class can be named anything):
Now, hook up this SCSS class to your React component by adding a className
string to the HTML element you are rendering. You can do this like so:
Ok, you're now ready to start styling! Some good places to start are font-size
, font-weight
, color
, and padding
. You can add styles like so:
Change these values and add more styles (See https://www.w3schools.com/html/html_styles.asp for some more examples, or do a quick Google search!) until you are happy with how your text is rendered.
ImageContent.tsx
and TextContent.tsx
Now that you have implemented TextContent.tsx
and ImageContent.tsx
, we need our NodeContent.tsx
to render either a TextContent
or ImageContent
component depending on the type of node selected.
In client/src/components/NodeView/NodeContent/NodeContent.tsx
, implement the two TODO
s in this file.
Once done, your frontend should look like this!
Yay! Now you can render the content of text
and image
nodes from our Mongo database!