Skip to content

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

  1. Navigate to the template repository for this exercise and follow these directions to fork it.
  2. Assuming Docker is started, in VS Code, hit CMD/CTRL + SHIFT + P, search + run dev container: open folder in container, and select the downloaded folder.
  3. In the terminal of VS Code, hit the + icon to open a new terminal instance. Run ls to make sure you’re in the root directory of the exercise and that you see package.json.
  4. Run npm install to install all the dependencies.
  5. 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

  1. 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;
  2. 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, and name 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.
  3. Handle the GET request for the form (GET /pokemon/new)

    • Just like the getHome handler that renders the HomeView.hbs, create a new route handler for GET /pokemon/new that renders the NewFormView.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
  4. 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 use JSON.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 new URLSearchParams 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 the Content-Type header to text/html, and render the ListView.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.

  5. How Refreshing!

    • After creating a new Pokemon and rendering the ListView.hbs, try refreshing the page. What happens?

    Resubmission

    • 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.
  6. 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.

  7. 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 use JSON.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 the Content-Type header is application/json, we can use JSON.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 the User-Agent header is curl, 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.


Comic