Welcome!
Everything is fine.

EJS View Engine, GET vs POST

Today, we'll learn about:

  • EJS
  • static files
  • GET versus POST

Plan

  1. Announcements
  2. The Big Picture
  3. Recap EJS
  4. Recap GET v POST
  5. Quiz Questions
  6. Exercises

Announcements

  1. oops I forgot to send out the second quiz about GET vs POST; sorry
  2. Grading Status.
    • People did really well on Wordle. Bravo!
    • I'll catch up on the others this weekend
  3. Twenty Questions is optional, but let me know if you want to complete it anyhow

Big Picture

Let's look again at the big picture.

What is a Database?

Very generally, a database stores data, but that definition is so generally as to be useless. So, let's be more specific:

A database

  • stores data
  • the data is in machine-friendly form
  • the representation is easy and efficient to search. For example, hashtables (dictionaries) or b-trees.
  • the database may have additional indexes to speed up certain kinds of searches.
  • the representation is easy and efficient to insert, update and delete

For the most part, we trust that the DBMS — Database Management System does all these things correctly and efficiently. So, we will treat this as an "opaque box" or abstraction barrier, and we won't worry about how the DBMS is implemented. (That would be a different course.)

Where is the Database? What does it Look Like?

It's in the cloud, meaning servers at MongoDB data centers.

It doesn't really look like anything. Well, a seething mass of bits.

But we work with data all the time without knowing what it looks like:

  • Email
  • Google Docs
  • Facebook, Yelp, Instagram, TikTok

All of these are essentially interfaces to databases. We don't know what the database looks like, but we can see its effects.

What's the Difference between a Database and a Website?

Interesting question! They are similar in that they

  • are sources of information
  • we look stuff up in them
  • we can store/post/upload stuff into them

But lots of websites are static or at least not updated by the user:

  • Online newspapers (NY Times, etc)
  • Government offices
  • College website
  • our personal websites (Assignment 2)
  • games and such (Assignments 3, 4, 5)
  • etc.

Theses are all "Web 1.0" while "Web 2.0" allows user-contributed data. This data is put into a database and rendered to (other) users when they search the database.

In fact, that's what this course is about: building web interfaces to databases

We haven't gotten there yet, but we're getting closer.

  • H7 Lookup allows the user to search a MongoDB database, namely WMDB
  • H8 CRUD allows the user to insert, update and delete from a copy of the WMDB.
  • H9 Ajax implements Ajax interactions to a copy of the WMDB

Database Representation

Aren't databases always like, rows and columns? Why can't I read MongoDB like that?

  • If you Google for databases, you'll probably find this. Those are relational databases.
  • MongoDB is a non-relational database, which means that it's not organized around rows and columns.
  • Instead, each collection is a set of documents
  • Each document is roughly equivalent to a row, and each property is like a column, but a document can have lists and subdocuments and such, which is more flexible than the rigid row/column format.

So, you can think about MongoDB collections as rows, but I don't know that it's helpful.

The best way to think about the collection is the results we get from using Mongosh and a "find-all":

>>> use wmdb;
>>> db.movies.find().pretty()

The result is a list of documents.

Express Apps

Express Apps might do anything; we will use them as a user-friendly web-browser interface to the MongoDB database.

  • the web browser is the first tier
  • MongoDB is the third tier
  • Express is the middle tier: the intermediary between the user and the database

Express Handlers

Last time, we imagined handlers like this:

// This is our simple Hello World example
app.get('/', (req, res) => {
    return res.send('Hello World!');
});

Now, with EJS, our handlers will be like this:

var visitCounter = 0;

// This is our simple Hello World example, this time, rendering a template
app.get('/', (req, res) => {
    let now = new Date();
    visitCounter ++;
    return res.render('index.ejs', 
                      {visits: visitCounter, 
                       time: now.toLocaleTimeString()});
});

That render hides a bunch of HTML in the .ejs file, combining it with the dynamic data that is its second argument.

EJS Substitutions

We can substitute values into the EJS file using a special tag-like syntax:

    <p>This visit is at <%= time %>.</p>

    <p>There have been <%= visits %> visits since the app started.</p>

After the rendering, before the HTML goes to the browser, the EJS has turned into normal HTML:

    <p>This visit is at 4:18pm.</p>

    <p>There have been 17 visits since the app started.</p>

Templating in General

You've almost certainly used F-strings in Python. That's templating:

name='Hermione'
grade='Outstanding'
msg=f'Student {name} got a {grade}'
print(msg)

There are also templates in JavaScript, which many of you know about already:

let name='Hermione'
let grade='Outstanding'
let msg=`Student ${name} got a ${grade} on their OWL`
console.log(msg)

These are great, but they don't have conditionals or loops or other cool things that a templating language like EJS has.

Compared to Browser-Side Dynamic Values

This is similar in effect to stuff we did earlier in the course:

    <p>This visit is at <span id="the_time"></span>.</p>

    <p>There have been <span id="the_visits"></span> visits since the app started.</p>

with JS like this:

$("#the_time").text("4:18pm");
$("#the_visits").text("17");

except that this happens in the browser, rather than on the server.

There are some advantages to doing the rendering server-side:

  • client might not support JS, such as a web scraper or screen reader
  • web crawlers don't always support JS, so better for SEO
  • more efficient for under-powered clients

On the other hand, there can be advantages to doing it browser-side:

  • dynamic update of the page
  • less data being sent

EJS Conditionals

Essentially, we are switching languages (from HTML to JS) within the EJS file, by using the special <% ... %> tag. The example from the reading:

    <% if( userId === null ) { %>

        <%# login form, because user not logged in %>
        <form action="/login" method="post">
            <label>username <input type="text" name="username"></label>
            <label>password <input type="password" name="password"></label>
            <input type="submit" value="login">
        </form>

    <% } else { %>

        <%# logout button %>
        <form action="/logout" method="post">
            <button>logout <%= userId %></button>
        </form>

    <% } %>

EJS Lists

We can also iterate over a list, either using a loop or .forEach:

    <h1>List of <%= listDescription %></h1>

    <ul>
        <% list.forEach( (elt) => {%>

        <li><%= elt %></li>

        <% }) %>
    </ul>

The render just provides a list:

const ourHeroes = ['Harry', 'Ron', 'Hermione', 'Fred', 'George'];

app.get('/heroes', (req, res) => {
    return res.render('list.ejs', 
                       {listDescription: 'Some Heroes of the Harry Potter Saga',
                        items: ourHeroes});
});

Note that there's nothing special about the word list. It could just as easily be a different word:

    <h1>List of <%= listDescription %></h1>

    <ul>
        <% items.forEach( (item) => {%>

        <li><%= item %></li>

        <% }) %>
    </ul>

The render just provides the items. That's the dynamic data. (Well, here it's not dynamic, but it could be.)

const ourHeroes = ['Harry', 'Ron', 'Hermione', 'Fred', 'George'];

app.get('/heroes', (req, res) => {
    return res.render('list.ejs', 
                       {listDescription: 'Some Heroes of the Harry Potter Saga',
                        list: ourHeroes});
});

Partials

You can save some EJS in a file and "load" that file into the main template:

    <%- include("partials/navbar.ejs") %>

And the partials/navbar.ejs file then contains just the stuff we want to insert:

<nav>
    <ul>
        <li><a href="/home">Home</a></li>
        <li><a href="/about">About Us</a></li>
        <li><a href="/contact">Contact information</a></li>
    </ul>
</nav>

App Structure

We'll set up our apps like this:

app/
    connection.js -> ../connection.js
    cs304.js -> ../cs304.js
    node_modules -> ../node_modules
    public/
        styles.css
    server.js
    views/
        index.ejs
        partials/
            navbar.ejs

You can copy that, recursively with symlinks, from ~cs304node/apps/starter/ or other examples. I'll usually guide you to one.

GET vs POST

  • most obvious in <form method="GET/POST"> but other places as well
  • different in format and usage

Let's demo the differences:

form demos

GET vs POST Facts

Let's review these facts:

  • GET requests can be cached
  • GET requests remain in the browser history
  • GET requests can be bookmarked
  • GET requests should never be used when dealing with sensitive data
  • GET requests have length restrictions
  • GET requests are only used to request data (not modify)

and in contrast:

  • POST requests are never cached
  • POST requests do not remain in the browser history
  • POST requests cannot be bookmarked
  • POST requests have no restrictions on data length

Quiz Questions

How did people feel about EJS?

  • 0
  • 0
  • 5
  • 10
  • 0
About GET vs POST? * 0 * 0 * 8 * 20 * 2 Wow! That's surprising. In the past, students have struggled a lot more with GET vs POST.

Let's look at your quiz questions on EJS

and [quiz questions B](../../quizzes/quiz11b.html)

Examples

For these examples, we're going to first copy the EJS example app from the course account, and then you can run and modify your copy.

cd ~/cs304/apps
cp -rd ~cs304node/apps/ejs ejs
cd ejs
ls -l

Notice the files there. In fact, let's list the subfolders, too

ls 
ls public
ls views
ls views/partials

Yikes, that's a lot of files!

Running an App

To run an app, we use Node.js:

node server.js

SSH Tunnel

You'll probably need to set up an SSH tunnel. Adapt the following and run it on your laptop, not on the server:

ssh -L 8080:localhost:PORT cs304guest@cs.wellesley.edu

Note that you need to replace the PORT with the four-digit port number that your app runs on.

Run the App

From now on in this course, our server-side main file will (almost) always be called server.js. That's not required, but it's convenient to know where to start.

Thanks to the SSH tunnel, you can access your app on localhost:

localhost:8080

What Routes are Available?

When running an app, I often want to know what routes are available. Here's a nice incantation:

grep "^app" server.js

That uses regular expressions to find all lines that begin with the word app. Very helpful!

grep — globally search for regular expression and print is super important by itself and as a milestone in the history and philosophy of Unix.

Learning more:

Running the App

Exit by typing ^C (control-C: Hold down the control key and press c.)

Now, let's run the EJS example:

node server.js

Click on the URL. Try some of these:

Exercise 0: Explore

Do the following:

  • Dig into the code for each, starting in server.js to see what endpoint handles it, then
  • Look at the EJS file that is rendered.
  • Figure out where the CSS is coming from

Exercise 1: Displaying a Dictionary

We're going to work up to providing a pre-filled form. First, let's just display some data. Feel free to modify the data

var formData = {
    customer: "Hermione",
    phone: "3249",
    email: "hgranger@hogwarts.ac.uk",
    size: "small",
    crust: "thin",
    due: "20:00",
    instructions: "deliver by owl"
 };

To do:

  1. Create an EJS file to render this data into
  2. Add a route to render the file with the data above
  3. Test it

Exercise 2: Pre-filling a form

Now, we're going to create a form and use the data above to pre-fill it

To do:

  1. Modify your EJS file to create a form like our pizza form
  2. Don't use a <select> menu for the size; just use a text input.
  3. Use EJS to insert the dynamic data as the value for each form input. see below
  4. Render the form
  5. Test that it works.

You can specify the value of an input using the value attribute:

<input type="text" name="college" value="Wellesley">

which looks like:

Exercise 3: Send the Data to your Express App

Express will parse the query string and put the data in a dictionary called query. So you can get the data from req.query.

For now, just print the submitted data:

app.get('/order', (req, res) => {
    const queryData = req.query;
    console.log(queryData);
});
  1. Add that route to your app.
  2. Modify the form to submit the data to the /order route
  3. Test this.
  4. Modify the route to console.log the email, size and instructions
  5. Modify to re-render the form with the updated info

Extra Credit: Pre-select from a SELECT Menu

By default, the first option on a <select> menu is selected, but you can select other options by adding a selected attribute to the menu, like this:

<select name="grad_year">
<option>choose one</option>
<option>2020</option>
<option>2021</option>
<option>2022</option>
<option selected>2023</option>
<option>2024</option>
<option>2025</option>
<option>2026</option>
</select>

Which looks like:

For extra credit bragging rights, convert the pizza size back to a <select> menu and pre-select the correct size.

Discussion

Here's my solution:

var formData = {
    customer: "Hermione",
    phone: "3249",
    addr: "hgranger@hogwarts.ac.uk",
    size: "small",
    due: "20:00",
    instructions: "deliver by owl"
 };

app.get('/form1', (req, res) => {
    return res.render('pizza.ejs', {data: formData});
})

var orderCounter = 0;

app.get('/order', (req, res) => {
    let data = req.query;
    orderCounter ++;
    console.log(data.addr, data.size, data.instructions);
    return res.render('pizza.ejs',
                      {counter: orderCounter,
                       data: data});
})

and form:

<h2>Order Counter: <%= counter %></h2>

    <form class="example"
      id="pizza-form"
      method="GET"
      action="/order">
  <p><label>Customer name: <input name="customer" value="<%= data.customer %>"></label></p>
  <p><label>Telephone: <input type=tel name="phone" value="<%= data.phone %>"></label></p>
  <p><label>E-mail address: <input type=email name="addr" value="<%= data.addr %>"></label></p>
  <p><label>Size:
      <select name="size">
        <option value="">choose size</option>
        <option value="small"  <%= data.size=="small" ? "selected" : "" %> >small (10-inch)</option>
        <option value="medium" <%= data.size=="medium" ? "selected" : "" %> >medium (12-inch)</option>
        <option value="large"  <%= data.size=="large" ? "selected" : "" %> >large (14-inch)</option>
      </select>
    </label></p>
  <p><label>Preferred delivery time:
      <input name="due" type=time min="11:00" max="21:00" step="900" value="<%= data.due %>">
  </label></p>
  <p><label>Delivery instructions:
      <textarea rows="3" cols="30" name="instructions"><%= data.instructions %></textarea>
  </label></p>
  <p><button type="submit">Submit order</button></p>
</form>
  • What are your questions?
  • What mistake did we make above?

We used GET for a pizza order. We should use POST instead.