View on GitHub

Enhance Workshop

This is the Enhance Workshop

Module Index

Module 6: HTML Forms

<!-- /app/pages/data.html-->
<nav-bar></nav-bar>

<h1>Data</h1>
<h2> Previous Data </h2>
<show-data></show-data>

<h2> Send New Data </h2>
<form action="/data" method="post">

  <fieldset>
    <legend>Personal Information:</legend>

    <label for="fname">First Name:</label><br>
    <input type="text" id="fname" name="fname"><br>

    <label for="lname">Last Name:</label><br>
    <input type="text" id="lname" name="lname"><br>

    <label for="email">Email:</label><br>
    <input type="email" id="email" name="email"><br>

    <label for="dob">Date of Birth:</label><br>
    <input type="date" id="dob" name="dob"><br>
  </fieldset>

  <button type="submit">Register</button>
</form>

Right now this will send data to the server. There is no one listening and we need to add a few parts to make it complete, but it is a start.

Validation:

Client-side vs server-side validation

Built-in vs. custom client-side validation

HTML built-in validation

Skipping validation

<form action="/data" method="post">
  <label for="fname">First Name:</label><br>
  <input type="text" id="fname" name="fname"><br>

  <button type="submit">Register</button>
  <button type="submit" formaction="/data?saveonly" formnovalidate >Save</button>
</form>

Custom Validation (Progressive Enhancement)

The built-in validation will accomplish a lot, but in a complex form, a few rules may need to be customized.

For example, what if we want to check if two password fields match before registering a new user account?

First, let’s create a new file, app/elements/registration-form.mjs, with the following contents.

// /app/elements/registration-form.mjs
export default function Registration({ html }) {
  return html`
<form id="registrationForm">
  <label for="password">Password:</label><br>
  <input type="password" id="password" name="password" required><br>

  <label for="cpassword">Confirm Password:</label><br>
  <input type="password" id="cpassword" name="cpassword" required><br>

  <input type="submit" value="Register">
</form>

<script type=module>
  const password = document.getElementById('password');
  const confirm_password = document.getElementById('cpassword');

  function validatePasswords(){
    if (password.value !== confirm_password.value) {
      confirm_password.setCustomValidity('Passwords do not match!');
    } else {
      confirm_password.setCustomValidity('')
    }
    // If you want to report the error as soon as it checks add the following
    // confirm_password.reportValidity() 
  }

  confirm_password.addEventListener('blur', validatePasswords);
  password.addEventListener('blur', validatePasswords);
</script>
`}

To try this out you can add a simple HTML page to test the form validation. Add the following to /app/pages/registration.html

<registration-form></registration-form>

Form Data Structure

POST / HTTP/2.0
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 34

email=bob@example.com&password=123&item=one&item=two
console.log(req.body)
// {email:"bob@example.com", password:"123", item: ["one", "two"]}
<form method="POST" action="/submit-form">
  <input type="checkbox" name="agree" checked value="yes">
  <input type="radio" name="gender" value="male">
  <input type="radio" name="gender" checked value="female">
  <input type="number" name="age">
  <input type="checkbox" checked name="newsletter">
  <input type="checkbox" name="attending">
  <input type="text" name="name">
  <input type="submit" value="Submit">
</form>
POST /submit-form HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 55

agree=yes&gender=female&age=25&name=Jane&newsletter=on

Server Normalization and Validation

Create a Nested Object

Expected Data Types from Strings

import formEncodingToSchema from '@begin/validator'

const formValues = {
    'anInteger': '3',
    'aFloat': '3.1',
    'aBooleanTrue': 'on'
    // 'aRadioFalse':'' // false boolean won't show up in form output
  }

  const Schema = {
    'id': 'ComplexSchema',
    'type': 'object',
    'properties': {
      'aFloat': { 'type': 'number' },
      'anInteger': { 'type': 'integer' },
      'aBooleanTrue': { 'type': 'boolean' },
      'aBooleanFalse': { 'type': 'boolean' },
    }
  }

  console.log(formEncodingToSchema(formValues, Schema))
  // { aBooleanTrue: true, aBooleanFalse: false, anInteger: 3, aFloat: 3.1 }

Validation

With a schema in place, we can validate the data in addition to setting types on the ambiguous string values.

import validator from '@begin/validator'

const Book = {
    id: 'Book',
    type: 'object',
    required: ['ID'],
    properties: {
        title: { type: 'string' },
        author: { type: 'string' },
        publication_date: { type: 'integer' },
        ID: { type: 'string' }
    }
}

export async function post(request) {
    let {valid, problems, data} = validator(request, Book)
    if (!result.valid) {
        return {
            json: { problems, book:data }
        }
    }
    // Data is valid!
}

JavaScript new FormData()

HTML forms are often all you need. They should be the baseline starting point for an app. But sometimes as a progressive enhancement, it is useful to submit the form with JavaScript.

<form id="myForm">
  <input type="text" name="username" placeholder="Username" required>
  <input type="password" name="password" placeholder="Password" required>
  <button type="submit">Submit</button>
</form>

<script>
  async function handleSubmit(event) {
    event.preventDefault()
    let formData = new FormData(this);
    try {
      let response = await fetch('https://example.com/api/endpoint', {
        method: 'POST',
        body: formData
      })
      let data = await response.json()
      console.log(data)
    } catch (error) {
      console.log('There was a problem: ' + error.message);
    }
  })
  document.getElementById('myForm').addEventListener('submit', handleSubmit)
</script>
<form>
  <input name=bar value=one/>
  <input name=bar value=two/>
  <input name=bar value=three/>
</form>
<script>
  const form = document.querySelector('form')
  const fromData = new FormData(form)
  console.log(Object.entries(formData))
  // {bar:"three"}
  console.log(formData.getAll('bar'))
  // ['one','two','three']
</script>

Multi-part Form Data and File Uploads