3.2 - Forms
🎯 Objectives
- Submit HTML form data to the server.
- Handle the form data sent to the server to create a new resource.
- Redirect the client instead of directly rendering a view after a POST request.
- Explain the benefits of using redirects to improve user experience.
🔨 Setup
- Navigate to the template repository for this exercise and follow these directions to fork it.
- Assuming Docker is started, in VS Code, hit
CMD/CTRL + SHIFT + P
, search + rundev container: open folder in container
, and select the downloaded folder. - In the terminal of VS Code, hit the
+
icon to open a new terminal instance. Runls
to make sure you’re in the root directory of the exercise and that you seepackage.json
. - Run
npm install
to install all the dependencies. - Run
npm run server
inside a JavaScript debug terminal to start the server.
🔍 Context
In the previous exercise, we learned how to render views using the Handlebars templating engine. We also learned how to pass data from the server to the views. In this exercise, we’ll learn how to handle user input with forms in Handlebars, send POST requests, and manage browser behavior with redirects.
🚦 Let’s Go
-
New Pokémon Form Route
- In your server code, add a route handler for
GET /pokemon/new
. - This route will render a new Handlebars view you’ll create next in the next step.
router.ts routes.GET["/"] = getHome;routes.GET["/pokemon"] = getAllPokemon;routes.GET["/pokemon/:id"] = getOnePokemon;routes.GET["/pokemon/new"] = getNewForm;routes.POST["/pokemon"] = createPokemon; - In your server code, add a route handler for
-
Create the View
-
Inside
/src/views
you’ll see the following HTML form structure:NewFormView.hbs {{> Header }}<h2>Create a New Pokemon</h2><form method="POST" action="/pokemon"><div><label for="name">Name:</label><input type="text" id="name" name="name" /></div><div><label for="type">Type:</label><input type="text" id="type" name="type" /></div><button>Create</button></form>{{> Footer }}In an HTML form’s
input
field,type
,id
, andname
serve distinct purposes:- Defines the type of data the input field expects.
- This helps the browser handle the input and provide appropriate user interaction.
- Examples:
type="text"
: Standard text input for characters.type="number"
: Input for numerical values.type="password"
: Hides the input characters, usually for passwords.
- Provides a unique identifier for the specific input element within the HTML document.
- Used for:
- Applying CSS styles specifically to that element.
- Accessing the element using JavaScript for further manipulation (e.g., validation).
- Identifies the data associated with the input field when the form is submitted.
- The server-side script receiving the form data will use this name to access the submitted value.
- In this exercise,
name
should be set to"name"
and"type"
for the respective Pokemon input fields.
-
-
Handle the GET request for the form (
GET /pokemon/new
)- Just like the
getHome
handler that renders theHomeView.hbs
, create a new route handler forGET /pokemon/new
that renders theNewFormView.hbs
. - Navigate to the new form page to see the form.
- Type in a Pokemon name and type, click create, and observe how the server is sending the request body back to the client. This is happening inside the
createPokemon
route handler.
name=Pikachu&type=Electric - Just like the
-
Handling the POST Request (
POST /pokemon
)-
In
createPokemon
we need to handle the form data sent by the client. We know that if the body was a JSON string, we could useJSON.parse
to convert it to a JavaScript object. But since it’s URL-encoded, we need to use a different method. -
The
URLSearchParams
class is a built-in JavaScript class that allows us to work with URL query parameters and form data. It’s a great way to parse URL-encoded strings.controller.ts const body = await parseBody(req);const newPokemon = Object.fromEntries(new URLSearchParams(body).entries());res.end();new URLSearchParams(body)
creates a newURLSearchParams
object from the URL-encoded string.entries()
returns an iterator that allows us to loop through the key-value pairs in the URL-encoded string.Object.fromEntries
is a method that takes an iterable of key-value pairs and returns a new object with those key-value pairs.
-
Now that we have the form data as a JavaScript object, we can use it to create a new Pokemon. Just like in previous exercises, use the
database
array and push the new Pokemon into it, using the array length + 1 as the new Pokemon’s ID.controller.ts const body = await parseBody(req);const newPokemon = Object.fromEntries(new URLSearchParams(body).entries());database.push({id: database.length + 1,name: newPokemon.name,type: newPokemon.type});res.end(); -
Finally, set the status code to
201
, set theContent-Type
header totext/html
, and render theListView.hbs
to show the updated list of Pokemon. -
Test the form by creating a new Pokemon and checking the list page to see if it was added.
-
-
How Refreshing!
- After creating a new Pokemon and rendering the
ListView.hbs
, try refreshing the page. What happens?
- Currently, after creating a Pokemon, users can refresh the page. This re-sends the POST request, creating duplicate Pokemon! Try refreshing a couple of times and see how the same Pokemon is added multiple times.
- After creating a new Pokemon and rendering the
-
Redirecting After a POST Request
-
After creating a new Pokemon, instead of rendering the
ListView.hbs
, we’ll redirect the client to the/pokemon
route.controller.ts const body = await parseBody(req);const newPokemon = Object.fromEntries(new URLSearchParams(body).entries());database.push({ id: database.length + 1, ...newPokemon });res.statusCode = 201;res.statusCode = 303;res.setHeader("Content-Type", "text/html");res.setHeader("Location", "/pokemon");res.end(); -
Test the form by creating a new Pokemon and checking the list page to see if it was added. Try refreshing the page and see if the Pokemon is duplicated.
-
-
Don’t leave JSON behind!
-
We can now handle URL-encoded form data, but what if we want to send JSON data to the server? We can’t use
URLSearchParams
for that. We need to useJSON.parse
to convert the JSON string to a JavaScript object. But, how do we know if the request body is JSON or URL-encoded? -
Using the
Content-Type
request header, we can check if the request body is JSON or URL-encoded. If theContent-Type
header isapplication/json
, we can useJSON.parse
to convert the request body to a JavaScript object.const contentType = req.headers["content-type"]; -
Using the
User-Agent
request header, we can check if the request is coming from a browser or a programmatic client like cURL. If theUser-Agent
header iscurl
, we can assume the request body is JSON, and we can send back a JSON response. Otherwise, we can assume the request came from a browser, and we can send back a redirect response.const userAgent = req.headers["user-agent"];
-
📥 Submission
Take a screenshot with half of the screen showing your list page and half showing the terminal making the POST request. If you want to get fancy and record a tiny video like the one above, you can do so using the screen recorder for Mac or Windows.
Submit the screenshot/video on Moodle.