Lesson Overview

So far in all our MVC programs we've gotten our inputs using URL parameters. However, most of the time you'll want to gather your inputs from a user with HTML forms. This allows for more advanced functionality: gathering multiple inputs, getting different kinds of inputs such as a selection from a list, performing client-side validation, etc.

In this lesson we'll learn how to process HTML forms on the server side, allowing you to create even more dynamic content.

Pre-Requisites

Before doing this lesson, make sure you've gone through the lessons on Model View Controller with Express.js and EJS Templates.

If you need to review HTML forms, you can read through Form Basics, Form Input Elemetns, and Form Validation.

Express MVC with a Form

Let's try an example with a form. In a previous lesson you wrote a program that displayed the Chinese Zodiac animal for a specific year. Let's rewrite this as an MVC program using Express.

Add a new project. I'm calling mine express_zodiac. Add the /views and /controllers directory. We don't need a model for this project.

Add these two views in your /views directory:

Our form is going to ask for a user's name and the year. So we'll need to handle a request to load the index page.

the input form, name entered as Arti, year entered as 2009
The form on the index page

When the user submits the form, we want to see the following output:

output shows Welcome Arti header, then paragraph Arti, you were born in 2009, which is year of the Ox.
The output of the program

Once the user submits the form with the two inputs, we'll need that request to be handled as a POST request. This request will have a few steps:

  1. Get the request body as url-encoded data.
  2. Retrieve the animal for the user-entered year.
  3. Render the result.ejs with the user's name and the zodiac year.

Each of these will be its own middleware function.

For this project, we should also separate our code into two controllers. One homeController, which manages the rendering of the EJS templates, and one animalController, which uses the third-party chinese-year package to get the zodiac animal for a specific year. This allows us to re-use the animalController in other programs!

Add the two controllers to the /controllers directory: homeController.js and animalController.js or call them whatever makes sense to you (although I do thing the name "animalController" is kind of funny :D )

Animal Controller

The animalController will have one exposed function that does pretty much what we had in our previous zodiac program: get the year, make sure if the year is empty that we assign the current year, then use the year in the getAnimal() endpoint of the chinese-year package.

To make the animal available to the next middleware function, we can add it as a parameter in the request body's query string: that's a common way of passing data from one middleware function to another, and it's totally fine: we'll just add a new item to the request body for the animal we found.

Also remember that in this case, it's from a POST request, so the query string is accessed via req.body, not req.query.

"use strict";
// /controllers/animalController.js
const chineseYear = require("chinese-year");

exports.getAnimal = (req, res, next) => {
    // make sure the year input has an actual year in it
    if (!req.body.year) {
        req.body.year = new Date().getFullYear();
    }
    // get the animal for the year
    const animal = chineseYear.getAnimal(req.body.year);
    // add the animal to the query string
    req.body.animal = animal;
    // next middleware
    next();
}

Home Controller

Our homeController handles the rendering of the index page and also the results page when the form has been submitted. So this controller needs two functions exposed: one to render the index page and one to render the results page.

The index page function is simple, since it has no placeholders:

exports.renderIndex = (req, res) => {
  res.render("index");
};

In fact, this doesn't even need to be an .ejs file, but we'll tackle that issue in an upcoming lesson.

Our function that renders the results page needs to pass three locals to results.ejs:

exports.renderResult = (req, res) => {
  res.render("result", { 
      username: req.body.username,
      year: req.body.year, 
      animal: req.body.animal
  });
}

Now we can put it all together in the app.js file. Add the app.js to your project root if you haven't already done so.

app.js

Add the code to app.js to:

"use strict";

  const express = require("express"),
      app = express(),
      animalController = require("./controllers/animalController"),
      homeController = require("./controllers/homeController");
  
app.set("port", process.env.PORT || 3000);
app.set("view engine", "ejs");

// rest of code will go here

app.listen(app.get("port"), () => {
  console.log(`Server running on http://localhost:${app.get("port")}`);
});

Recall that we need a function to handle requests to the index page. Let's use /zodiac/index, but feel free to be a bit more creative (e.g. with or without .html)

// route get requests to index page
app.get("/zodiac/index", homeController.renderIndex);

As we discussed, we need three middleware functions to handle the POST request for form submission:

First, populate req.body with the query string in the request body:

app.use(
  express.urlencoded({
    extended: false
  })
);

Second, get the zodiac animal:

app.use("/zodiac/getyear", animalController.getAnimal);

Finally, we can render the result page:

app.post("/zodiac/getyear", homeController.renderResult);

If you haven't done so already, initialize your app. Then make sure you install all three modules/packages via npm install:

npm install chinese-year express ejs -s

Now you can run your program. Load the index page in your browser, e.g.
localhost:3000/zodiac/index

Fill in your name and birth year, then click the submit button.

You should see the output in the browser!

Exercises

  1. Write an MVC Express application that displays information about a circle with a specific radius.

    Use this index.ejs file as a base and put it in your /views directory.

    form with value 2 entered for radius
    The form asks for a radius

    When the user clicks the submit button, the page should reload with the circle information in the first <section> element.

    form with value 3 entered for radius
    The form asks for a radius

    Add the circle.js object/class as the model for your application (e.g. it should go in /models). Your circle should have a radius property, two accessor properties circumference and area, and a toString() function property. If the radius is not provided when someone creates a new circle object, use a default value of 0.

    Add a single controller with three exposed functions:

    • renderIndex() renders the index page.
    • getCircle() uses the inputted radius from the query string and uses it to contruct a new Circle object. If radius is empty, construct a default circle.
    • renderResult() passes the circle to the index.ejs page and renders it.

    Modify your index.ejs page: Inside the empty first <section> element, add the code to do the following if a circle object exists as a local:

    • render a level-2 heading with the content "Circle Information".
    • below the heading, display the circle (it should automatically use the toString() if you coded it correctly) in a paragraph element.
    • below the circle object, display a paragraph element containing "Circumference: " followed by the circle circumference
    • below the circle object, display a paragraph element containing "Area: " followed by the circle area

    TIP: when you render the index page, pass in a null circle object.

    output shows circle with radius 2, circumference 18.84, area 28.27 except with way more decimal places
    The output with circle information