Managing Web Forms with JavaScript

Introduction

In the world of web development, form handling is a crucial skill. Forms are the way that our users interact with an application, and they are the primary way that users can provide data to our applications. It can also be a frustrating experience for both users and developers to govern forms in a way that is both user-friendly and secure. In this lesson we will examine approaches to handling forms with JavaScript, including the limitations of JavaScript driven validation.

Form Element

Although we have already covered the HTML Form Element in a previous course, it is worth refreshing ourselves on the attributes and behaviour of a form element. The form element is a container for a group of form controls, such as input elements, select elements, and textarea elements. The form element is used to collect user input and usually this is sent to a server for processing.

Let’s take a look at a simple search form element:

html
	<form name="searchbar" action="/search" method="get">
	  <label for="example-search">Search</label>
	  <input type="text" id="example-search" name="search" required minlength="3" />
	  <button type="submit">Search</button>
	</form>

In this example we have three attributes on the form element:

  • name - This is a unique name for the form. This is used to reference the form in JavaScript.
  • action - This is the URL that the form data will be sent to when the form is submitted.
  • method - This is the HTTP method that will be used to send the form data to the server. The two most common methods are get and post.

The input element has two attributes that are used to validate the user input:

  • required - This attribute indicates that the input is required and the form cannot be submitted without a value.
  • minlength - This attribute indicates the minimum number of characters that the input must contain.

It also has a unique name, id, and type attribute. The name attribute is used to reference the input in JavaScript. The id attribute is used to associate the label element with the input. The type attribute indicates the type of input that the element represents - in this case a search bar.

HTMLFormElement

Now, let’s examine the same form element represented in JavaScript:

javascript
	// We can use the name attribute to reference the form element
	const form = document.forms.searchbar;

A named form element is automatically added to the document.forms collection. While this is not the only way to reference a form element, it is quite convenient. We could use querySelector or getElementById to reference the form element.

The form variable is an instance of the HTMLFormElement interface. This interface has a number of properties and methods that we can use to interact with the form element. Let’s take a look at some of the more useful properties and methods:

  • form.action - This property contains the URL that the form data will be sent to when the form is submitted. In our case this is /search.
  • form.method - This property contains the HTTP method that will be used to send the form data to the server. In our case this is get.
  • form.elements - This property contains a collection of all the form controls in the form. In our case this is the input, label and button elements.
  • form.elements.search - This property contains a reference to the input element with the name attribute of search.
  • form.validate() - This method will validate the form and return a boolean value indicating whether the form is valid or not.
  • form.submit() - This method will submit the form to the server.
  • form.reset() - This method will reset the form to its initial state.

You may notice that there is a direct relationship between form attributes and form properties. This is not a coincidence! By changing these values in JavaScript we are changing the attribute values in HTML simultaneously.

Form Events

Form elements issue a number of events that we can listen for in JavaScript. These events form the basis of form validation and data handling. Let’s take a look at some of the more useful events:

  • submit - This event is fired when the form is submitted. This event is fired when the user clicks the submit button, or when the form.submit() method is called.
  • reset - This event is fired when the form is reset. This event is fired when the user clicks the reset button, or when the form.reset() method is called.
  • input - This event is fired when the value of an input element changes. This event is fired when the user types into an input element, or when the input.value property is changed in JavaScript.

There are many more events that are not listed above, however these can be used to handle the majority of form interactions.

We can listen for a form event in the same way as any other DOM event:

javascript
	document.forms.searchbar.addEventListener('submit', (event) => {
	  // Prevent the default behaviour of the form
	  event.preventDefault();
	
	  // Reference the form element
	  const form = event.target;
	
	  // Reference the input element
	  const input = form.elements.search;
	
	  // Alert the user of the search term
	  alert(`You searched for ${input.value}`);
	});

The most important part of this example is that the event.target object contains all of the data we need to continue. It is common when working with forms in JavaScript for the first time to take a more verbose approach like so:

javascript
	const form = document.querySelector('form[name="searchbar"]');
	const input = form.querySelector('input[name="search"]');
	
	form.addEventListener('submit', (event) => {
	  event.preventDefault();
	  alert(`You searched for ${input.value}`);
	});

This works, but it means that the developer is responsible for keeping track of names, classes, ids and other unique identifiers. The other disadvantage of this approach is that it will break if the form is created dynamically. For example, if the form is only created when a user clicks a button - it may not be available on page load. By sticking to the event.target approach we can be sure that the element will be available at the right time.

Submit Event

The submit event is the most important event when working with forms. This event is fired when the user clicks the submit button, or when the form.submit() method is called. This event is fired before the form is submitted to the server, and it is the last chance we have to validate the form data before it is sent. From a user perspective, this means that the user is happy with their input and finished with the form.

By default, the submit event will attempt to send the input data to the URL specified in the action attribute of the form. Traditionally this would be an endpoint ready to accept the data, store or process it and return a status page to show that it worked. However, in recent years it has become more common to intercept an event using JavaScript and handle the data in the browser. For new developers this “traditional” behaviour can be confusing, as a form will appear to refresh the page if an action is not specified and the user clicks the submit button.

Let’s look at the most basic example of a form submit event:

javascript
	document.forms.searchbar.addEventListener('submit', (event) => {
	  // Prevent the default behaviour of the form
	  event.preventDefault();
	});

This code only prevents the page from changing, and does nothing else. This is a common starting point for a form submit event handler as it allows us to continue working with the form data below the preventDefault call.

Reset Event

The reset event is fired when the user clicks the reset button, or when the form.reset() method is called. This event is fired before the form is reset to its initial state. Most forms will not require a specific reset handler as the default behaviour is usually sufficient. In very complex forms it may be necessary to perform API actions as part of the reset process, for example.

javascript
	document.forms.searchbar.addEventListener('reset', (event) => {
	  alert('The form will be reset');
	});

Input Event

The input event is fired when the value of an input element changes. This event is fired when the user types into a text input element, toggles a checkbox, or when the input.value property is changed in JavaScript. This event is fired before the value of the input element is changed. This event may be useful for validating the input as the user types, or for updating the UI as the user types. For example, a sign-up form for an online RPG requires unique usernames. We could use the input event to check if the username is available as the user types:

html
	<form name="signup">
	  <label for="username">Username</label>
	  <input type="text" id="username" name="username" required minlength="3" />
	  <button type="submit">Sign Up</button>
	</form>
javascript
	// Example function to randomly declare some usernames as taken
	function usernameIsTaken(username) {
	  // Generate a random number
	  const randomNumber = Math.random();
	  // Round the number to 0 or 1
	  const randomBoolean = Math.round(randomNumber);
	  // Return true or false
	  return Boolean(randomBoolean)
	}
	
	document.forms.signup.addEventListener('input', (event) => {
	  const username = event.target;
	
	  // Simulate a request to the server to check if the username is taken
	  const taken = usernameIsTaken(username.value);
	
	  if (taken) {
	    // Set a custom error message to inform the user
	    username.setCustomValidity(`User ${username.value} is already taken, please try another name.`);
	  } else {
	    // Clear the custom error message
	    username.setCustomValidity('');
	  }
	});
	
	document.forms.signup.addEventListener('submit', (event) => {
	  // Prevent the form from refreshing
	  event.preventDefault();
	  // Reset the form
	  event.target.reset();
	})

In this example we are using the input event to check if the username is available as the user types. If the username is taken, we set a custom error message on the input element. This will prevent the form from being submitted until the error message is cleared. If the username is not taken, we clear the custom error message.

Form Validation

Before discussing the role of JavaScript in form validation, we should review the topic of client-side validation first. As front-end developers, we are responsible for validating the user input before it is sent to the server, for example if the server expects a 10 digit phone number, we should make some effort to validate that the user’s input value contains 10 digits. In reality, we can do nothing to prevent a user from overriding our validation and sending whatever they choose to the server, which is why client-side validation cannot be considered a security practice. Instead, this is something that we do to improve the experience of our users. The alternative, providing no validation, means that the user will constantly face server error messages that they may not understand why trying to complete a form.

Consider the example of a credit card form on a checkout page. A user is highly motivated to finish their purchase and wants this process to be as easy as possible. If they incorrectly enter the expiry date for their card, this will cause a server error. The application can either attempt to warn the user before they submit the form, or wait until the request has caused an error and show that to the user. Generally speaking, as developers we attempt to reduce errors before they happen - and in this case doing so will have a significant positive impact on user experience.

HTML vs JavaScript Validation

In a previous course we have covered the use of validation attributes in HTML forms. These attributes are powerful, well supported and localised into every major language on earth. They do come with some limitations, however, and only in these situations do we require JavaScript to handle validation. In general, we should approach validation tasks with an HTML-first attitude and use JavaScript to complement, rather than replace this functionality.

This can be a difficult concept to grasp, so let’s look at some example side by side. In this example, we will attempt to validate a text input - making sure that it contains at least 3 characters:

HTML only

This example shows that these validation requirements can be met using HTML attributes and no additional JavaScript.

html
	<form name="example">
	  <input type="text" id="example" name="example" required minlength="3" />
	  <button type="submit">Submit</button>
	</form>

JavaScript only

This example shows that these validation requirements can be met using JavaScript and without using HTML attributes.

html
	<form name="example">
	  <input type="text" id="example" name="example" />
	  <button type="submit">Submit</button>
	</form>
javascript
	document.forms.example.addEventListener('submit', (event) => {
	  // Prevent the form from refreshing
	  event.preventDefault();
	
	  // Reference the input element
	  const input = event.target.elements.example;
	
	  // Check if the input value is at least 3 characters long
	  if (input.value.length < 3) {
	    // Set a custom error message
	    input.setCustomValidity('Please enter at least 3 characters');
	  } else {
	    // Clear the custom error message
	    input.setCustomValidity('');
	  }
	
	  // Submit the form
	  event.target.submit();
	});

HTML and JavaScript

This example shows that these validation requirements can be met using both HTML attributes and JavaScript, without recreating the validation process in JavaScript.

html
	<form name="example">
	  <input type="text" id="example" name="example" required minlength="3" />
	  <button type="submit">Submit</button>
	</form>
javascript
	document.forms.example.addEventListener('submit', (event) => {
	  // Prevent the form from refreshing
	  event.preventDefault();
	
	  const form = event.target;
	  const input = form.elements.example;
	
	  if (input.validity.valid) {
	    // Do something if the value is valid
	    console.log("Input is valid")
	  } else {
	    // Otherwise do something else
	    alert("Input is invalid")
	  }
	});

Custom Validation

In this simple example there is no real requirement to provide custom or additional validation in JavaScript - however this can be useful. For example, we may want a user to enter their password twice on signup to ensure that they have not made a mistake. This is not a task that can be easily solved with HTML attributes, so we can use JavaScript to provide this functionality.

html
	<form name="signup">
	  <label for="password">Password</label>
	  <input type="password" id="password" name="password" required minlength="8" />
	  <label for="password-confirm">Confirm Password</label>
	  <input type="password" id="confirm" name="confirm" required minlength="8" />
	  <button type="submit">Sign Up</button>
	</form>
javascript
	document.forms.signup.addEventListener('submit', (event) => {
	  // Prevent the form from refreshing
	  event.preventDefault();
	
	  const form = event.target;
	  const password = form.elements.password;
	  const passwordConfirm = form.elements.confirm;
	
	  if (password.value !== passwordConfirm.value) {
	    // Set a custom error message
	    passwordConfirm.setCustomValidity('Passwords do not match');
	  } else {
	    // Clear the custom error message
	    passwordConfirm.setCustomValidity('');
	    form.reset();
	  }
	});

Validation Libraries

When building on frameworks such as React, Vue or Angular, it is common to use a validation library to handle form validation. These libraries are usually more powerful than the native HTML validation attributes, and provide a more consistent developer experience. As a professional developer it is expected that validation will be done using a standard approach, such as one of the major libraries. This is because custom approaches to JavaScript validation are often inconsistent, and can be difficult for other developers to understand.

We will not be covering any of these libraries in this course, instead we will focus on the underlying fundamentals of form validation.

Form Data

When we create a web form using HTML, we are also setting up a data structure that can be used to send this data to a server. Other examples of data structures you may be more familiar with would be arrays or objects. A web form stores the input data from the user in a format known as FormData. This is a special type of object that is used to store key-value pairs, where the key is the name of the input element and the value is the value of the input element. This is a very convenient format for sending data to a server, as it is easy to convert to JSON.

Let’s take a look at a simple example of working with formData:

html
	<form name="example">
	  <label for="firstName">First Name</label>
	  <input type="text" id="firstName" name="firstName" required minlength="3" value="John" />
	  <button type="submit">Submit</button>
	</form>
javascript
	// Access the form element
	const form = document.forms.example;
	// Use it to create a new FormData object
	const formData = new FormData(form);
	// Access the value of the firstName input
	const firstName = formData.get('firstName');
	// Log the value to the console
	console.log(firstName);

In this example we can access the value of the input by querying the formData object using the get method. This method takes the name of the input as an argument and returns the value of the input.

Multiple Inputs

In some cases, our forms may require something more complex than uniquely named inputs. For example, if we are writing a blog article and want an input field to add tags to an article. Ideally, we would not place a limit on how many tags can be created, but this could get tricky quickly:

html
	<form name="example">
	  <label for="firstName">First Name</label>
	  <input type="text" id="firstName" name="firstName" required minlength="3" value="John" />
	  <label for="tags">Tags</label>
	  <input type="text" id="tags1" name="tags1" />
	  <input type="text" id="tags2" name="tags2" />
	  <input type="text" id="tags3" name="tags3" />
	  <button type="submit">Submit</button>
	</form>
javascript
	const form = document.forms.example;
	const formData = new FormData(form);
	const tags1 = formData.get('tags1');
	const tags2 = formData.get('tags2');
	const tags3 = formData.get('tags3');
	const tags = [tags1, tags2, tags3];

Although this will work, it is not ideal and will make it harder to add or remove tag fields in future. It limits the user to adding only three tags. Let’s take a look at a better implementation:

html
	<form name="example">
	  <label for="firstName">First Name</label>
	  <input type="text" id="firstName" name="firstName" required minlength="3" value="John" />
	  <label for="tags">Tags</label>
	  <input type="text" id="tags" name="tags" />
	  <button type="button" id="add-tag">Add Tag</button>
	  <button type="submit">Submit</button>
	</form>
javascript
	const form = document.forms.example;
	const formData = new FormData(form);
	const tags = formData.getAll('tags');
	
	document.querySelector('#add-tag').addEventListener('click', (event) => {
	  const input = document.createElement('input');
	  input.type = 'text';
	  input.name = 'tags';
	  form.insertBefore(input, event.target);
	});

The way that this example works, is that each time you click the button - the insertBefore() method adds a new identical tag input to the form. Even though these inputs all share the same name attribute, we can gather all of their values into an array using the getAll method. This is a much more flexible approach, and allows us to add as many tags as we like.

FormData to JSON

Although the internet originally ran on FormData type requests, the world has moved on to JSON as the preferred format for sending data to a server. This is because JSON is a more flexible format, and can be used to send more complex data structures. Thankfully, it is quite straightforward to convert data from FormData to JSON.

javascript
	const form = document.forms.example;
	const formData = new FormData(form);
	const json = Object.fromEntries(formData.entries());
	console.log(json);

Let’s break down the third line in this example further:

javascript
	// Creates a normal JS object from a key/value array
	Object.fromEntries()
	
	// Returns an array of key/value pairs
	formData.entries()
	
	// Example output:
	// [["firstName", "John"], ["tags", "JS"], ["tags", "Forms"], ["tags", "FormData"]]

This is a multi-step data conversion process:

  1. Get the form element from the DOM
  2. Create a new FormData object from the form element
  3. Convert the FormData object to an array of key/value pairs
  4. Convert the array of key/value pairs to a normal JavaScript object

Practical Example

Let’s pull all of this together into a generic form submit listener that is capable of turning any form into a JavaScript object:

html
	<form name="contact">
	  <label for="firstName">First Name</label>
	  <input type="text" id="firstName" name="firstName" required minlength="3" value="John" />
	
	  <label for="lastName">Last Name</label>
	  <input type="text" id="lastName" name="lastName" required minlength="3" value="Smith" />
	
	  <label for="email">Email</label>
	  <input type="email" id="email" name="email" required value="john@smith.com" />
	
	  <label for="message">Message</label>
	  <textarea name="message" id="message" cols="30" rows="10">Hello World</textarea>
	
	  <button type="submit">Submit</button>
	</form>
javascript
	document.forms.contact.addEventListener('submit', (event) => {
	  // Prevent the form from refreshing
	  event.preventDefault();
	
	  // Get the form element
	  const form = event.target;
	
	  // Create a new FormData object
	  const formData = new FormData(form);
	
	  // Convert the FormData object to a JSON object
	  const json = Object.fromEntries(formData.entries());
	
	  // Log the JSON object to the console
	  alert(JSON.stringify(json, null, 2));
	});

Summary

In this lesson we have covered the basics of working with forms in JavaScript. We have learned how to reference form elements, how to listen for form events, how to validate form data and how to convert form data to JSON. We have also learned about the limitations of JavaScript validation and the importance of using HTML validation attributes where possible.

The key take aways are that form validation doesn’t have to be complicated, nor does working with forms in JavaScript. If you can think of your form as a data container, not just a visible element on the page - you can use JavaScript to do some very powerful things with it.

Lesson Task

Brief

Create a digital menu, where users can select items that they would like to eat. Use radio, select and checkboxes to control the user input. When the form is submitted, display the total price of the order to the user.

Process

  1. Create a new HTML file called menu.html
  2. Create a new JavaScript file called menu.js
  3. Link the JavaScript file to the HTML file
  4. Create a form element with a name attribute of menu
  5. Add input elements to the form to represent the menu items
  6. Add a submit button to the form
  7. Add an event listener to the form to listen for the submit event
  8. Prevent the default behaviour of the form
  9. Calculate the total price of the order
  10. Display the total price of the order to the user

HTML Example

In the example below, notice how the value contains the price of the item instead of the name. This is the data that can be used to calculate the total order value.

Bear in mind the difference between radio buttons and checkboxes. Radio buttons are used when the user can only select one option, whereas checkboxes are used when the user can select multiple options.

html
	<form name="menu">
	  <fieldset>
	    <legend>Starters</legend>
	    <label for="starter1">Croquettes</label>
	    <input type="radio" id="starter1" name="starter" value="5" />
	    <label for="starter2">Miso Soup</label>
	    <input type="radio" id="starter2" name="starter" value="6" />
	    <label for="starter3">Terrine</label>
	    <input type="radio" id="starter3" name="starter" value="7" />
	  </fieldset>
	  <fieldset>
	    <legend>Sides</legend>
	    <label for="side1">Fries</label>
	    <input type="checkbox" id="side1" name="side" value="3" />
	    <label for="side2">Salad</label>
	    <input type="checkbox" id="side2" name="side" value="4" />
	    <label for="side3">Bread</label>
	    <input type="checkbox" id="side3" name="side" value="2" />
	  </fieldset>
	  <button type="submit">Place Order</button>
	</form>

Advanced Process

  1. Add a Vegan/Vegetarian/None diet select option
  2. Change the contents of the form based on the user’s diet
  3. Use data attributes to style each item based on which diet it is suitable for.

Solution

You can find an interactive solution here.