Model - View - Control Implementation
Conceptual Overview
The video provides a high level overview of Model View Control. This is the Transcript of the video.
Activity Overview
The video provides a general overview of the activity, but does not contain the detail needed to complete each process. Watch the video to obtain a general idea, but follow the written steps to complete the activity. This is the Transcript of the video.
Get Organized
We'll begin by creating several new files and storing these into existing folders.
- Create a new index.js file in the database folder.
- Create a new inventory-model.js file in the models folder.
- Create a new baseController.js file in the controllers folder.
Database Connection
With the database created, we need a means for our application to talk to it. That is what we'll do now.
-
Open the package.json file.
- Look at the list of dependencies.
- You'll find "pg" in the list. "pg" is a short-hand reference to the node-postgres package that Express can use to talk with a PostgreSQL database.
- Open the node-postgres Documentation in your browser.
- Bookmark this documentation, so you can refer back to it as needed.
- You can close the package.json file. I just wanted you to know that this package was listed and should be installed already.
- Find and open the .env file, which should be at the root of the project. Make sure that the name is ".env" and not ".envsample".
- Add the following code to the .env file:
This code helps us to identify when we are working and testing locally vs in a remote, production environment, which will be done later. Save and close the file.NODE_ENV='development'
- Open the index.js file in the database folder.
-
Add the code shown to the
index.js file:
const { Pool } = require("pg") require("dotenv").config() /* *************** * Connection Pool * SSL Object needed for local testing of app * But will cause problems in production environment * If - else will make determination which to use * *************** */ let pool if (process.env.NODE_ENV == "development") { pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl: { rejectUnauthorized: false, }, }) // Added for troubleshooting queries // during development module.exports = { async query(text, params) { try { const res = await pool.query(text, params) console.log("executed query", { text }) return res } catch (error) { console.error("error in query", { text }) throw error } }, } } else { pool = new Pool({ connectionString: process.env.DATABASE_URL, }) module.exports = pool }
-
An Explanation
- Line 1 - imports the "Pool" functionality from the "pg" package. A pool is a collection of connection objects (10 is the default number) that allow multiple site visitors to be interacting with the database at any given time. This keeps you from having to create a separate connection for each interaction.
- Line 2 - imports the "dotenv" package which allows the sensitive information about the database location and connection credentials to be stored in a separate location and still be accessed.
- Line 3-8 - a multi-line comment concerning the "ssl" code found in the connection pool function. This is critical code. Be sure to read and understand the comment..
-
Line 9 - creates a local
pool
variable to hold the functionality of the "Pool" connection. - Line 10 - an
if
test to see if the code exists in a developent environment, as declared in the .env file. In the production environment, no value will be found. - Line 11 - creates a new pool instance from the imported Pool class.
- Line 12 - indicates how the pool will connect to the database (use a connection string) and the value of the string is stored in a name - value pair, which is in the .env file locally, and in an "environment variable" on a remote server. These are equivelent concepts, but different implementations.
-
Lines 13 through 15 - describes how the Secure Socket Layer (ssl) is used in the connection to the database, but only in a remote connection, as exists in our development environment.
A Short Explanation
SSL is a means of excrypting the flow of information from one network location to another. In this case, when we attempt to communicate with the remote database server from our own computer the code indicates that the server should not reject our connection. However, when we work in a remote production server, the ssl lines must not exist. This is because our application server and the database server will be in the same system and their communication will be secure, which is what we will require.
- Line 16 - ends the pool function started on line 11.
- Line 17 - left blank.
- Lines 18-19 - comments related to the function to be exported in lines 21 through 31.
- Lines 20-31 - exports an asynchronous
query
function that accepts the text of the query and any parameters. When the query is run it will add the SQL to the console.log. If the query fails, it will console log the SQL text to the console as an error. This code is primarily for troubleshooting as you develop. As you test the application in your development mode, have the terminal open, and you will see the queries logged into the terminal as each is executed. - Line 32 - ends the
if
and opens theelse
structure. - Line 33 - creates a new "pool" instance from the "Pool" class.
- Line 34 - indicates the value of the connection string will be found in an environment variable. In the production environment, such a variable will not be stored in our .env file, but in the server's settings.
- Line 35 - ends the Pool object and instance creation.
- Line 36 - exports the pool object to be used whenever a database connection is needed. This is for the production environment, which means the queries will not be entered into the console.
- Line 37 - ends the
else
structure.
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save and close the file.
Add the Connection String to the .env file
In the database file that you just built, the code indicates that the connection pool will find the information it needs to talk with the database server in the .env file. Let's get that inserted, so it is there.
- In your browser, go to render.com and login using your GitHub credentials.
- Open the database server in the dashboard.
- Scroll down to the "Connections" area.
- Click the "Copy" button on the "External Database URL" line.
- Open the .env file in VSC.
-
On a blank line type,
DATABASE_URL=
, then paste the string copied from render.com. Make sure there is no space in the entire string. - Confirm that no warnings or errors appear in the .env file.
- Save and close the file.
When you get ready to run and test your application, the database
connection pool will look in the .env file for a variable of
DATABASE_URL
. When found, it will use the connection string to
attempt to talk to the database server. If successful, the pool will
then have the ability to run the query in the inventory-model file,
which you will build next, and return the data from the server.
The Inventory Model
With the database connection pool created we can now use it to begin interacting with the database. I hope you'll remember that in an M-V-C approach, the "Model" is where all data interactions are stored. Our inventory-model.js document is where we'll write all the functions to interact with the classification and inventory tables of the database, since they are integral to our inventory.
- Open the inventory-model.js file in the models folder.
-
Type the code shown in the image into the file:
const pool = require("../database/") /* *************************** * Get all classification data * ************************** */ async function getClassifications(){ return await pool.query("SELECT * FROM public.classification ORDER BY classification_name") } module.exports = {getClassifications}
-
An Explanation
- Line 1 - imports the database connection file (named index.js) from the database folder which is one level above the current file. Because the file is index.js, it is the default file, and will be located inside the database folder without being specified. The path could also be ../database/index.js. It would return the same result.
- Line 2 - left intentionally blank.
- Lines 3-5 - a multi-line comment introducing the function.
-
Line 6 - creates an "asynchronous" function,
named
getClassifications
. An asynchronous function returns apromise
, without blocking (stopping) the execution of the code. It allows the application to continue and will then deal with the results from thepromise
when delivered. -
Line 7 - will return (send back) the result of
the SQL query, which will be sent to the database server using a
pool connection, when the resultset (data) or an error, is sent
back by the database server.
Notice the two keywords: return and await.
Await
is part of theAsync - Await
promise structure introduced in ES6.Return
is an Express keyword, indicating that the data should be sent to the code location that called the function originally. - Line 8 - ends the function began on line 6.
- Line 9 - left intentionally blank.
- Line 10 - exports the function for use elsewhere.
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save and close the file.
The base controller
A controller is the location where the logic of the application resides. It is responsible for determining what action is to be carried out in order to fulfill requests submitted from remote clients. The "base" controller will be responsible only for requests to the application in general, and not to specific areas (such as inventory or accounts).
- Find and open the baseController.js file that should be in the controllers folder.
-
Add the code shown below to the file:
const utilities = require("../utilities/") const baseController = {} baseController.buildHome = async function(req, res){ const nav = await utilities.getNav() res.render("index", {title: "Home", nav}) } module.exports = baseController
-
An Explanation
- Line 1 - imports an index.js file (which does not yet exist) from a utilities folder (which does not yet exist) which is one level above the current location inside the controllers folder.
-
Line 2 - creates an empty object named
baseController
. - Line 3 - left intentionally blank.
-
Line 4 - creates an anonymous, asynchronous
function and assigns the function to
buildHome
which acts as a method of thebaseController
object. In short, this is similar in concept to creating a method within a class, wherebaseController
would be the class name andbuildHome
would be the method. Being asynchronous, it does not block (stop) the application from executing while it awaits the results of the function to be returned. The function itself accepts therequest
andresponse
objects as parameters. -
Line 5 - calls a
getNav()
function that will be found in the utilities > index.js file. The results, when returned (notice the "await" keyword), will be stored into thenav
variable. -
Line 6 - is the Express command to use EJS to
send the index view back to the client, using the
response
object. The index view will need the "title" name - value pair, and thenav
variable. Thenav
variable will contain the string of HTML code to render this dynamically generated navigation bar. - Line 7 - ends the function started on line 4.
- Line 8 - left intentionally blank.
-
Line 9 - exports the
baseController
object for use elsewhere.
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save and close the file.
Differences in Implementation
In the inventory-model.js file, you should have noticed that the function was created, and then exported within an unnamed object, e.g.:
module.exports = { getClassifications }
However, in the baseController.js file, a named object was created on line 2:
const baseController = {}
Then, the function was built as a named method of the object on line 4:
baseController.buildHome = async function(req, res) { ...
At the bottom of the controller the named object is exported:
module.exports = baseController
Both of these methods do the exact same thing. In fact, there are additional ways of doing the same thing. But the code found in the examples throughout the course are limited to these two. You may ask, "Why not use one way for everything?" The answer is that different organizations approach things differently, and we want you to see and use a variety. Just know, this approach is intentional.
Alter the "Index Route"
With the model and controller in place, it is time to alter the route to deliver the index.ejs view using the entire M-V-C approach.
- Find and open the server.js file, at the root of the project.
- Locate the "Require Statements" area, near the top of the file. Add a new require statement to bring the base controller into scope:
const baseController = require("./controllers/baseController")
- Save the file.
- Locate the "Index" route in the "Routes" area of the file.
-
Alter the route to match this image:
-
An Explanation
- Previously the index.ejs view was rendered directly in the route. While it worked, it did not follow the M-V-C methodology.
- The new code uses the imported "baseController" to "call" the "buildHome" method.
- This will execute the function in the controller, build the navigation bar and pass it and the title name-value pair to the index.ejs view, which will then be sent to the client.
- While we are close to testing, there is one more loose thread to tie up.
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save and close the file.
Build the Utilities file and functions
In the baseController file we wrote code to import a file from the utilities folder. That file will contain a function, which will use the inventory model to get the data from the database, then will build an HTML unordered list around the data. The finished HTML will be sent back as a string to be used in the navigation partial to render the actual navigation bar, based on the vehicle classification found in the database. Let's build the function, then test.
The "getNav" Function
- In VSC click at the very bottom of the explorer pane so that no folder or file is selected.
- Click the "New File..." icon.
-
Type "
utilities/index.js
", press the "Enter" key. - A new folder named utilities should appear with an index.js file within it.
- The new index.js file should be open in the work area. If not, open it.
- This file will hold functions that are "utility" in nature, meaning that we will reuse them over and over, but they don't directly belong to the M-V-C structure.
-
In the file, type the code shown below:
const invModel = require("../models/inventory-model") const Util = {} /* ************************ * Constructs the nav HTML unordered list ************************** */ Util.getNav = async function (req, res, next) { let data = await invModel.getClassifications() let list = "<ul>" list += '<li><a href="/" title="Home page">Home</a></li>' data.rows.forEach((row) => { list += "<li>" list += '<a href="/inv/type/' + row.classification_id + '" title="See our inventory of ' + row.classification_name + ' vehicles">' + row.classification_name + "</a>" list += "</li>" }) list += "</ul>" return list } module.exports = Util
-
An Explanation
- Line 1 - requires the inventory-model file, so it can be used to get data from the database.
-
Line 2 - creates an empty
Util
object. Just as you did earlier in the base controller. - Line 3 - left intentionally blank.
- Lines 4 through 6 - a multi-line comment introducing the function.
-
Line 7 - creates an asynchronous function, which
accepts the
request
,response
andnext
methods as parameters. The function is then stored in agetNav
variable of theUtil
object. - Line 8 - calls the
getClassifications()
function from the inventory-model file and stores the returned resultset into thedata
variable. -
Line 9 - creates a JavaScript variable named
list
and assigns a string to it. In this case, the string is the opening of an HTML unordered list. Note the use oflet
. This is because the value will be changed as the upcoming lines of the function are processed. -
Line 10 - the
list
variable has an addition string added to what already exists. Note the use of+=
, which is the JavaScript append operator. In this instance a new list item, containing a link to the index route, is added to the unordered list. -
Line 11 - uses a
forEach
loop to move through the rows of thedata
array one at a time. For each row, the row is assigned to arow
variable and is used in the function. -
Line 12 - appends an opening list item to the
string in the
list
variable. -
Line 13 - appends the code that is found on
lines 14 through 20 as a string to the
list
variable. -
Line 16 - a string that includes the beginning
of an HTML anchor. The
+
sign is the JavaScript concatenation operator, used to join two strings together. The value in the href attribute is part of a route that will be watched for in an Express router. -
Line 17 - the
classification_id
value found in the row from the array. It is being added into the link route. - Line 18 - the title attribute of the anchor element.
-
Line 19 - the
classification_name
value found in the row from the array. It is being added into the title attribute. - Line 20 - the last part of the string forming the opening HTML anchor tag.
-
Line 21 - the
classification_name
from the row being displayed between the opening and closing HTML anchor tags. This is the display name in the navigation bar. - Line 22 - the closing HTML anchor tag.
-
Line 23 - the closing list item tag being added
to the
list
variable. -
Line 24 - the ending of the
forEach
loop and enclosed anonymous function. -
Line 25 - the closing HTML unordered list tag
being appended to the
list
variable. -
Line 26 - sends the finished string, back to
where the
getNav
function was called. - Line 27 - ends the function which started on line 7.
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save the file.
If you look at the code (lines 7 to 24) carefully, it should be somewhat familiar. It simply keeps adding strings to a variable. When done, the entire string is nothing more than an HTML unordered list that contains some anchor elements. When the function is done building the string, the variable is sent to where the function was called.
Where does this leave us?
The flow of the M-V-C for our application runs something like this, so far:
- A client types or clicks a link containing the index route of our application.
- That triggers a
request
to our server.js page. - The route is found in the page, which calls the function in the baseController file.
-
The controller calls the
getNav
function and awaits the result to be returned. -
When the finished string is returned, it is stored in the
nav
variable. - The controller, then tells Express and EJS to send the index.ejs file back to the client, and send the nav string and title name-value pair along with it. These items will be used in the head.ejs and navigation.ejs partials.
- The server builds the finished home page and sends it to the browser.
Adjust the navigation.ejs Partial File
With all the code in place, there is one more minor change to be made, then we can test.
- Find and open the navigation.ejs file in the partials folder.
- Highlight and delete the existing unordered list that you typed as static code previously.
-
Type this code where the previous code was located:
<%- nav %>
-
When done, the navigation.ejs partial should look like this:
- Confirm that the code is typed correctly and the VSC does not report any warnings or errors.
- Save and close the file.
Time to Test
- Save all of your work.
- Open a new VSC terminal window.
- Type
pnpm run dev
, press "Enter". - The node server should start showing that it is running on localhost:5500
- In a browser, type
http://localhost:5500/
. - If things work as they should, you should be looking at your home page.
- Hover your mouse over any of the navigation items. You should now see a tooltip generated from the title attribute. You'll recall that the original, static, navigation items did not have this. But, your dynamic navigation items do!
- You should also be able to look in the terminal pane and see the query that was run when the page loaded. It is the same query that exists in the inventory-model, and is being written in the terminal as part of the code we placed in the database pool code, found in the database > index.js file.
- If things didn't work, check spelling, file and folder names and the path. Talk to your learning team, the TA or the professor. Be sure that you can get it to work.
- When done testing, be sure to stop the server with Control + C in the terminal.