Forms and Javascript
This chapter discusses handling user interaction via forms in Javascript. Forms are the basis for a lot of interaction in web applications as they provide the input elements that users can interact with: text input, buttons, checkboxes, drop-down selections.
The original intention of the HTML form was to gather user input so that it could be submitted via an HTTP request back to the server. This is still the default action in an HTML form, however, in a modern web application, we typically want to interrupt this default behaviour and handle the form data in the front-end. Handling form data may result in a request being sent back to a server but it could also just result in some action on the front end. We want to know how to write Javascript code to take control of forms in our web application.
Forms Recap
An HTML form consists of a <form>
element that contains one or more input
elements. For example:
<form id="sample-form">
<input type="text" name="username" placeholder="User Name">
<input type="password" name="password">
<input type="checkbox" name="agree" value="yes" checked>
<select name="colour">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select>
<input type="submit" value="Submit">
</form>
Each of the input elements is presented in a different way to the user and supports a different kind of interaction, from mouse clicks to text entry.
Once the user has carried out the interaction, the form fields contain data.
Each input element has a value
attribute that holds the value entered into
the field or selected by the user.
The default action when the form is submitted (usually by clicking the Submit
button) is to generate an HTTP request. Since this form has no method
attribute, this will be a GET
request and since it has no action
attribute,
the request will go to the same URL that this page was delivered from.
The GET
request would encode all
of the form data in the URL, e.g. https://example.com/page?usernam=Steve&password=chicken...
.
The server can then process this request as it sees fit and return a new page to
the browser.
Javascript Takes Over
In a single page web application, the interaction with the user is managed by Javascript code running in the browser. Such an application might use a form to interact with the user but then carry out a local operation in response to the form rather than sending off a request to a server. For example, imagine a form that can be used to set the background colour of the page (or more realistically, switch between dark and light stylesheets, but we'll simplify the example here).
<form id="colour-change-form">
<input name="colour" type="text">
<input type="submit" value="Change Colour">
</form>
We now want to attach a Javascript event handler to this form that will be called when the user clicks on the submit button.
let form = document.getElementById('colour-change-form');
form.onsubmit = (event) => {
...
}
If the user enters a colour name in the form and clicks "Change Colour" then the
default action would be to send a new GET
request. The first thing the
submit handler needs to do is prevent this default action by calling
event.preventDefault()
(where event
is the event object passed in
to the event handler).
The next step is to find the form and the colour value entered by the user.
The form element will be the target
attribute of the event since this event
originated by interaction with the form. To get the colour value, we
find the input element within the form and get it's value
property. In
this example I've used querySelector
to find the input but you could
also use getElementById
if it had an id.
form.onsubmit = (event) => {
event.preventDefault();
const form = event.target;
const colour = form.querySelector('[name="colour"]').value;
}
The final step is to carry out the desired action which in this case is to change the background colour of the page. I can do this by finding the base HTML element and setting it's style attribute:
const body = document.getElementsByTagName('html')[0];
body.style = 'background-color:' + colour;
Other Useful Events
The submit
event is triggered when the user clicks on the submit
button or otherwise submits the form (eg. by hitting the return key in
a text field). Other events are also useful in capturing user interaction
with a form.
Change
The change
event is triggered for a select input when a new selection
is made. For a text input, it is triggered when that input loses focus - when
the user clicks elsewhere in the page or tabs into another input field.
You can use the change event to run code when
an individual field in the form changes its value.
Keys
The keyup
or keydown
events are triggered as the user types into
a text field; either before or after the keystroke is registered. A common
practice is to bind a handler to the keyup
event to trigger auto-completion
of a form field. Here's a very simple auto-complete example for colours:
const colours = ['red', 'blue', 'green', 'pink', 'lightgreen', 'lightcyan', 'gray', 'purple'];
let form = document.getElementById('colour-change-form');
form.querySelector('[name="colour"]').onkeyup = (event) => {
const value = event.target.value;
const options = colours.filter(c => {
if (c.startsWith(value)) {
return c;
}
});
if (options.length === 1) {
event.target.value = options[0];
}
}
The handler gets the current value and then filters the list of colours to find those that start with this value. If the resulting list of options has just one member, then it sets the value of the field to that colour. This implementation is very simplistic - in particular if you delete one letter of the completed colour it will just add it again which is rather annoying! In a real implementation you would usually offer the list of options to the user as a drop-down list.
As a little extension to this example, I might want to have the colour change as soon as there is a successful completion of what the user is typing. So, as well as filling in the complete colour name, I would then trigger the submit handler for the form. You might think that you could do this with:
form.submit();
However, this will submit the form bypassing the submit handler and send a
new GET request. This is because the submit
method is meant for programmatic
submission of forms once you have validated the data and might even be called
from a submit handler. To get the desired outcome, we can just find the
submit button in the form and fire it's click
handler:
form.querySelector('[type="submit"]').click();
This will call the registered onsubmit handler for the form and have the desired effect.
Focus and Blur
The focus
event triggers when a user clicks on a field to enter some
data. The blur
event triggers when they leave. You can bind to these
events to keep track of where the user is in the form and to perform
validation (for blur
) of values once completed.
Submitting Form Data
Sometimes you do want to send a request in response to a form but you
want to be in control and handle the response in your application
rather than just letting the server send out a new page. In this case
you want to gather the input data from the form and use fetch
to
send a request to the backend server. An example might be one where
you want to carry out a transaction via an online commerce API. Our
form allows the user to select a product and when they click submit,
the order is placed.
<form id="order-form">
<select name="product">
<option name="x1000">Panoptic X1000</option>
<option name="pretank">Pre-tank Caltigulator</option>
<option name="frobulator">Cryostatic Frobulator</option>
</select>
<input type="number" name="quantity" value="1">
<input type="submit" value="Place Order">
</form>
The form asks for two values - a product and a quantity. The handler will first get these values from the form and then construct a POST request to the server with a JSON request body.
const orderForm = document.getElementById('order-form');
orderForm.onsubmit = (event) => {
event.preventDefault();
const form = event.target;
const data = {
product: form.querySelector('[name="product"]').value,
quantity: form.querySelector('[name="quantity"]').value
};
fetch('https://example.com/order',
{
method: 'POST',
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
// here we would update the page to confirm the purchase
console.log(data)
});
}
The fetch request body is a JSON version of the form data. The request body will look like this:
{"product":"Panoptic X1000","quantity":"1"}
In many cases, the remote API will want you to send data like this as JSON,
in other cases
it will accept other formats such as the older "multipart/form-data"
or
"application/x-www-form-urlencoded"
which is what is sent with a
regular form submission.
This example doesn't do much with the response from the request but this would typically be a confirmation of the order (or otherwise) and we would update the current page to reflect the result of the transaction. Perhaps adding to a list of products ordered for this user.
This example shows how form data can be gathered and used as part of a fetch
call to a server API. Your Javascript code is in control here and the request
submission doesn't result in page redraw as it normally would.