HTML Form and Lists

Learning Objectives

Every application I work on just seems to be a load of forms and lists.
Bernard Mordan, Junior developer 2015

Its time to start having a conversation with our application. At the moment we have implemented data flowing in one direction; from the datastore to the browser. The program tells us what it knows. Now we need to create that journey in the opposite direction; from the user to the datastore. To do that we need a way to interact with our program.

We are going to do this in 3 steps:

  1. First of all we need to collect the data we need to create a new restaurant from our user.
  2. Secondly we are going to sent that data to our server
  3. Finally if we create the resource ok we need to indicate to the user that our operation has worked and they have been successful; if it didn't work we need to communicate that to our user and advise about what they can do next.

To create a new restaurant we need a name, an image URL, menus those menus need items with prices. It's quite complicated and we'll need to think about collecting quite a lot of data before we are ready to start creating a new resource.

User input

HTML provides a comprehensive set of elements we can use for data collection.

Input TypeData TypeExample
textString
numberNumber
Select (Dropdown)Emun
radio1 from a list
checkboxMany from a list
timeDateTime
emailValid Email
telValid Telephone Number
urlValid URL
searchString
passwordString (masked)
rangeNumber
colorHEX code
filefile path

Paragraph text.

Input elements are enclosed within a <form> element. The form element captures the data from all the different input fields it contains and will enable us to intercept and process our inputs before sending them to the server. From the list above can you identify input elements that you might need?

Create the parent form element

<form id="create-restaurant" onsubmit="processForm(this);return false;">
    <div>
        <label for="restaurant-name">Name of restaurant</label>
        <input id="restaurant-name" name="name" required/>
    </div>
    <div>
        <label for="restaurant-image">Upload a hero image</label>
        <input id="restaurant-image" name="image" type="file" accept="image/*" required/>
    </div>
    <div>
        <button>Create</button>
    </div>
</form>

The javascript in the onsubmit handler is calling a function that is defined below. By returning false, we prevent the submit triggered by a <button> element within a <form> element from reloading the page. This is a useful override as we may not wish to reload the page and lose information the user has given us. We can interact with our API server in the background without disturbing the user experience.

The <label> element is a semantic tag and is therefore useful for screen readers and similar technologies. In order to indicate programmatically that a label belongs to a particular input, we can either nest the <input> within the <label>, or match the <label> for attribute to the id of the <input> (as above).

The this passed to processForm is the form element itself. Once we have extracted the data from the form element with new FormData(form) we can send that data off to our endpoint.

function processForm(form) {
    fetch('http://localhost:3000/restaurants', {
        method: 'POST',
        body: new FormData(form)
    })
    .then(console.info)
    .catch(console.error)
}

Update the server

We are dealing with 'Multipart' form data. The form for the restaurant has multiple parts, the text field with the name of the restaurant, the image file meta data, and the binary content of the image file itself. This is best processed on the server using a middleware called multer. You will need to update your server code to handle the images that it receives and configure where and how the files are stored on disc. The URL to that points to the image file needs to be added to the new restaurant record before its stored in your database. Below is an example of how to set up the multer middleware to make processing your image uploads easier.

const express = require('express')
const app = express()
const path = require('path')
const multer = require('multer')
const storage = multer.diskStorage({
    destination: (req, fileData, next) => {
        // this is where your uploaded image file will be saved
        next(null, path.join(__dirname, 'public', 'uploads'))
    },
    filename: (req, fileData, next) => {
        // name the file however you like I'm using a timestamp
        next(null, new Date().getTime() + path.extname(fileData.originalname))
    }
})
const images = multer({ storage })

app.post('/restaurants', images.single('image'), (req, res) => {
    console.log(req.body) // here you can access the text field name of the restaurant req.body.name 
    console.log(req.file) // this object is the meta data you need to store/process
    // the uploaded file will be in your `public/uploads` folder (go look!)
    res.sendStatus(201) // 201 is the response code for successfully creating a resource
})

Now you will have a URL for an image that you can display in your UI with your restaurant in req.file.path and you can access the name of the restaurant in req.body.name.

Assignment