Forms and JavaScript

This reading is about how we can use JavaScript to process the data that the user enters into a form. We'll look at the following topics:

  • submit handlers
  • preventing defaults

Form Submission

Let's talk about forms. HTML forms were invented so that a page could collect information from the user and allow it to be packaged up and submitted to a server for some kind of processing. Think about forms on Amazon.com or Ebay.com or any other kind of web application. Think about the customer feedback questionnaires we are constantly being asked to fill out. Even Facebook posts. All of those are forms being submitted to servers.

We can write JavaScript code that gets triggered when a form is submitted.

Let's start with the following form

Go ahead and fill it out and submit it if you like.

Note that the URL changes when you submit this form, with your form data (key/value pairs) appearing in the URL. Here, there's only one input, name="ans", but in general we could have many inputs. We saw quite a few inputs in our pizza form; submit that form and look at the URL.

Submit Handlers

The first thing we want to do is add a JS function that will be invoked when the form is submitted. Form submission is a kind of event, so this is a kind of event handler.

What should our function do? For now, let's just alert the user that they submitted the form. A little bit of jQuery will suffice.

There are two equivalent ways of attaching a function to the form-submission event:

$("#form2").submit(function () { alert("form submitted!"); });

and

$("#form2").on('submit', function () { alert("form submitted!"); });

The former is slightly more succinct. Later, we will learn a small advantage of the second form, namely event delegation.

Without jQuery, we could attach the event handler like this:

document.querySelector("#form2")
    .addEventListener('click', 
        function () {alert("form submitted!")});

Note that the native API's querySelector will return null if the selector doesn't match anything (say there is a typo in the selector string), and so you'll get an error saying something like Cannot read properties of null (reading 'addEventListener). This is not an intuitive error message, but at least it pinpoints roughly where the error is. Anytime you get a cannot read properties of null error, that often means that something unexpectedly turned out to be null, so look earlier in the computation.

Preventing Defaults

Usually, we want to send the data to a server when a form's submit button is clicked (or the user presses enter in a text field), but in this case we don't. We want to prevent the default behavior, so we'll change our event handler to get the event object and use the preventDefault() method. We saw this back in OtterGram when we wanted to prevent the default behavior of clicking on a hyperlink. This is exactly the same idea.

Here's the JS/JQ code. Notice that we added an argument to the event handler function, to gain access to the event object that the browser will supply.

$("#form3").submit(function (evt) {
    evt.preventDefault();
    alert("form submitted!");
});

Go ahead and submit this form. Note that the URL doesn't change with this event handler. Of course, that's because we've prevented the default behavior, so we are still on the same page.

The preventDefault method is part of the native API, so that line doesn't depend on jQuery at all.

jQuery and its pitfall

What's wrong with the following combination of HTML and CSS and JavaScript?

<form id="form3"> ...</form>
#from3 { border: 1px solid green; }
$("#from3").submit(function (evt) {
    evt.preventDefault();
    alert("form submitted!");
});

Right; spelling. In CSS, you won't get an error message; it's just a rule that doesn't happen to apply to anything. Similarly, in jQuery, it'll look up everything that matches that selector, and add the given function as a submit handler. Alas, nothing matches that selector, but jQuery doesn't give you an error message. It treats it as an empty set: valid but useless.

Recall that this is why I implemented the bounds plug-in to jQuery. So, using my plug-in, we can do:

$("#from3").one().submit(function (evt) {
    evt.preventDefault();
    alert("form submitted!");
});

Now, the code would produce an error message, and we can debug it.

Note that there's a similar phenomenon in the native API and for the very same reason. Suppose we use querySelectorAll to, say, add an event handler to a bunch of li elements:

  1. apple
  2. banana
  3. cherry
document.querySelectorAll('#list1 li')
    .forEach((e,i) => 
        e.addEventListener(
            'click',
            () => alert("click on element "+i)));

Notice this adds a different event handler to each LI. (Later, we may talk about a more efficient way to do this, called event delegation.) Go ahead and try it; I executed that code in this page.

However, suppose there is a typo in the selector, maybe something like this:

document.querySelectorAll('#list1 li')
    .forEach((e,i) => 
        e.addEventListener(
            'click',
            () => alert("click on element "+i)));

There's no error message at all! Why? Because the querySelectorAll returns an empty list, and our code swiftly but uselessly does a forEach over that empty list.

Values

If we are going to process the form on the client (in the browser), rather than on the server, we need a way to get out the values of the inputs.

jQuery provides a useful .val() method that either reads the current value or sets it. We will usually be interested in reading the value.

Let's start with a more interesting form, returning to our pizza form:

Pizza Form

Pizza Form

Recall that this form had the following inputs:

  • name="customer"
  • name="phone"
  • name="addr"
  • name="size"
  • name="due"
  • name="instructions"

Since name is an attribute of the input, we can use the attribute selector syntax (square brackets around the attribute=value expression) to select those inputs. For example, [name="customer"] will select the input that holds the customer's name. Then, we can use the .val method to get the user's input.

Try it! Fill out the form and copy/paste the following expression into the JS console:

[ $('[name="customer"]').val(),
$('[name="phone"]').val(),
$('[name="addr"]').val(),
$('[name="size"]').val(),
$('[name="due"]').val(),
$('[name="instructions"]').val() ];

Exercise: find a JS expression that will return a dictionary of all the form data.

Values of Radio Buttons

The above works very well, even on things like select. but it doesn't work for radio buttons because all the radio buttons in a group share the same name. However, the selector language includes a "pseudo-class" called :checked that selects the checked radio button in a group. Here are some radio buttons:

You can try the following expression in the JS console. It should return the value attribute of the radio button you have selected.

$("[name=station]:checked").val();

Summary

  • Forms collect data from the user as a set of name/value pairs
  • JavaScript can access that data, pulling it into variables that we can use in ordinary programming
  • You can get the value using jQuery: $("[name=customer]").val()
  • For radio buttons you can use the :checked pseudo-class: $("[name=station]:checked").val()
  • We can attach a function as an event handler for form submission using $(selector).submit(fun); or $(selector).on('submit', fun);
  • The event handler can use event.preventDefault() to prevent the form being submitted to the server.

Additional Info on Event Delegation

Here's repetition of the list from above, this time with id=list2:

  1. apple
  2. banana
  3. cherry

Earlier, we added an event handler to each LI element, so three in all. That's not very many, but if there were hundreds or thousands of elements (for example, handling "likes" for a post with many comments), that becomes pretty inefficient. There's a better way, based on the fact that browsers use event bubbling. In a nutshell, event bubbling means that clicking on an element also counts as clicking on its parent, its grandparent, its great-grandparent, all the way up to the top of the document tree. So, clicking on an LI above means there's also a click event on the parent OL.

So far, so good. Now, it gets interesting. There's only one OL, so if we add an event handler to the OL, we can have one event handler that "handles" clicks on all the LI descendants, no matter how many. This is called event delegation.

Ah, but suppose you care which LI was clicked on (which you almost certainly will). That element is available in the event object, as a property called target. So consider the following code, using the native API:

document.querySelector("#list2").addEventListener(
     'click',
     (event) => { 
         const li = event.target;
         alert("You clicked on "+li.textContent);
         });

The li variable is bound to the particular LI that was clicked on. We then used the textContent property of a document element to get the text inside the LI.

Event delegation is important enough that jQuery has a convenient shorthand, adding an extra argument to say what kind of descendant we care about. (The native code above doesn't do that, so it's not quite as precise at the jQuery code. We can add that precision if desired.)

$("#list2").on(
    "click",   // the event
    "LI",      // this is the descendant selector
    (event) => {
        const li = event.target;
        alert("jQuery says you clicked on "+li.textContent);
        });