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
  • serializing forms
  • resetting forms
  • a generalized form-handler

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. In CoffeeRun, we'll see some sophisticated, abstract code for setting up some general-purpose code for handling form-submission. In this reading, we'll start with some concrete examples before we go to the higher abstraction.

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.

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

The jQuery submit method is just a shortcut for using on and the name of the event, which is what CoffeeRun does:

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

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.

$("#form3").on('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.

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").on('submit', function () { alert("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. Now we'll see how that plug-in works.)

Sometimes, jQuery's behavior is exactly what you want, but often, you'd like to know if you've done something wrong. So check the number of matched items, using the length of the object.

var $form = $("#from3");
if ($form.length === 0) {
    throw new Error("couldn't find form...");
}
$form.on('submit', function () { alert("submitted"); });

(In the code above, we've used a dollar sign in the name of the variable. That's a common but not universal convention for variables that contain jQuery results, since it helps you remember that you can use jQuery methods on the value of that varible. But it's also a little ugly. CoffeeRun chooses to use this convention; don't let it bother you.)

Generic Form Handling

In fact, you might even create a higher-level function that will search, check and then add the event handler. Like this:

function addFormSubmissionHandler(selector, fn) {
    var $form = $(selector);
    if ($form.length === 0) {
        throw new Error("couldn't find form...");
    }
    $form.on('submit', fn );
}

That's what CoffeeRun does. Actually, in CoffeeRun, we'll build a generic form-processing class (in the sense of OOP), and that class will always want to prevent the default behavior of submitting the form. So, CoffeeRun checks that the selector works properly in the constructor of the class:

class FormHandler {
    // instance variables
    $formElement = null;

    constructor (selector) {
        if (!selector) {
            throw new Error('No selector provided');
        }

        this.$formElement = $(selector);
        if (this.$formElement.length === 0) {
            throw new Error('Could not find element with selector: ' + selector);
        }
        if (this.$formElement.length > 1) {
            throw new Error('Found too many elements with selector: ' + selector);
        }

    }

So, all the checking happens when we create an instance of this class. The code checks that a selector has been supplied, and that it matches exactly one element. (We could even add code to make sure it matched a FORM element, but this is enough.)

What about adding the event handler? We'll do that in a method, rather than in the constructor. (That allows us to add multiple handlers, if we wanted to.)

Adding a Submission Handler

The CoffeeRun FormHandler class has a method to add a submission handler to the form. Here's the beginning of that method definition:

class FormHandler {
    // instance variables
    $formElement = null;

    constructor (selector) {
        ...
    }

    addSubmitHandler(callback) {
        console.log('Setting submit handler for form');
        this.$formElement.on('submit', function (event) {
            event.preventDefault();
            ...
            callback();
        });
    }
}

One important thing to notice about the code above is that the method has an argument (callback) that is passed in. The callback does the rest of the work of the form submission handler. So, you can think of the FormHandler class as two parts:

  1. find the form using a selector
    • if the selector didn't work, complain
  2. set up a submission handler for that form
    • the submission handler will do some routine stuff and then,
    • invoke a callback function arg to do the specific stuff for this form

The callback argument is named that because it is a callback function, just like the callback functions that are arguments to .map and .forEach and the functions that are click-handlers (the .click method) and so on.

Callbacks are a common way of modularizing code: one function does some generic stuff, and the custom stuff is done by a callback function that is passed as an argument to the generic function. We'll see this idea many times in this course.

Serializing Forms

In general, forms have several inputs and all of them get packaged up and submitted to the server. To do that, the form inputs have to each be converted into strings and those strings have to be concatenated together. That process is called serializing. The key with serializing is that it has to be reversible: all of it has to be done in a way that the server can reverse the process and get back the original set of name/value pairs.

jQuery has a method that will serialize a form for you. It's called, unsurprisingly, .serialize(). You can also get the inputs as an array of objects; that's called .serializeArray(). Each object in the array consists of a single name/value pair from the form, represented as a JS object (a dictionary-like set of name/value pairs).

For example, a form asking about pizza preferences:

<form id="pizza">
    <input name="kind">  <!-- e.g. pepperoni or veggie --> 
    <select name="size">
        <option>large (16 inch)</option>
        <option>medium (14 inch)</option>
        <option>personal (12 inch)</option>
    </select>
</form>

might serialize like this:

$("#pizza").serializeArray();
[{name: 'kind', value: 'veggie'},
 {name: 'size', value: 'personal (12 inch)'}]

If you're curious, check out jQuery .serializeArray to learn more.

Serializing into An Object

Serializing into an array is nice, but it's still a little inconvenient. If we want to find out what size the pizza needs to be, we have to search the array for the right name/value pair and get the corresponding value. It would be easier to have all the form data in a single JavaScript object (dictionary). Then, if the object is in a variable called order, finding the size is just order.size.

For example, we want to serialize the earlier pizza form into an object like this:

{kind: 'veggie',
 size: 'personal (12 inch)'}

In CoffeeRun, the generic code will arrange for all form submission handlers to serialize the form into an array, and then collect all the form inputs into a single object. Like this:

var data = {};
$(this).serializeArray().forEach(function (item) {
    data[item.name] = item.value;
    });

Let's carefully understand how this code works. First, I'll re-write it in a more verbose way:

var data = {};
var $form = $(this);
var items = $form.serializeArray();
items.forEach(function (item) {
    data[item.name] = item.value;
    });

The first line, data = {} just creates an empty object (dictionary).

The second line, $form = $(this) is not obvious. But it turns out that jQuery provides a nice feature for our event handlers. When an event occurs, before calling our event handler, jQuery binds the keyword this to the DOM element that the event happened to. Here, the event was a form submission, so this is bound to that form. This line of code wraps up the DOM element as a jQuery set, so all the jQuery methods are available. Note that we named the variable $form, but we could have named it form; there's nothing special about the dollar sign.

The third line, items = $form.serializeArray(), serializes the form as an array of name/value objects. We saw examples of this earlier. The array is stored in a variable called items. (I like to use plural words for arrays.)

Finally, we use the .forEach method on the array to iterate over all the elements in the array:

items.forEach(function (item) {
    data[item.name] = item.value;
    });

The anonymous callback function just takes the name/value pair out of the little object and puts it in data.

When this code is all done, all the name/value pairs are in the data object.

Form Submission Handler

The form submission handler then invokes the callback function with the form data object as its input. This is how the generic form submission handler allows the client to do whatever custom form processing it wants to do.

So the overall plan for the generic form submission handler is now:

  1. find the form using a selector (or complain)
  2. set up a submission handler for that form
    • prevent the default,
    • serialize the form into a single object
    • invoke the callback function
      • to do the specific stuff for this form
      • passing it the form data in a single object
    • clean up afterwards

Here's the code:

    addSubmitHandler(callback) {
        console.log('Setting submit handler for form');
        this.$formElement.on('submit', function (event) {
            event.preventDefault();

            var data = {};
            // In an event handler, 'this' is the DOM element
            $(this).serializeArray().forEach(function (item) {
                data[item.name] = item.value;
                console.log(item.name + ' is ' + item.value);
            });
            console.log(data);
            callback(data);
            this.reset();
            this.elements[0].focus();
        });
    }

Re-read the code above keeping the abstract plan in mind.

Final Steps

There are a few "cleanup" steps that the generic form processing does.

First, it resets the form, which clears all the inputs to their blank or initial values. It does the same thing as the reset button does on this form:

Try filling it out and then resetting.

To reset a form from JavaScript, there's a method in the DOM (not part of jQuery), so the code is just element.reset(). In the code above, we have the following, because this is bound to the DOM element for the form:

    this.reset();

That line of code clears out the form for the next user.

Just to make things a little easier for the user, after clearing the form, we can focus the first input of the form (the type of coffee order). "Focus" just means that that's where typed input will go. Typically, users will click on an element to give it focus and type into it. Alternatively, they may tab (press the "tab" key) to move from one input to the next.

The DOM element for a form has an array of inputs in the elements property. The first one, of course, is elements[0]. So we can focus the first one like this:

    this.elements[0].focus();

The focus() method is also part of the DOM (not jQuery). Now the form is ready for the next user.

The Client Code

We've talked a lot about the implementation of this generic FormHandler class. Let's look at it from the client's point of view. (Recall that OOP often can be thought of as providing some useful functionality to a client, in a generic, modular way. See OOP and APIs.)

The client is our main module. Conceptually, it wants to:

  • add a form-submission handler to the order form, that
  • will create an order in the truck

Because of all the intricate stuff that is hidden inside the FormHandler class, the client code turns out to be pretty simple:

var FORM_SELECTOR = '[data-coffee-order="form"]';

var myTruck = new Truck('ncc-1701', new dataStore());

var formHandler = new FormHandler(FORM_SELECTOR);
formHandler.addSubmitHandler( function (form_data) {
     myTruck.createOrder(form_data);
     });

The first line of code just sets up the CSS selector for the form. CoffeeRun used data attributes to select the form, but it could just as easily have used ids or other selectors.

The second line of code creates a truck, initializing it with an identifer and a data store.

The third line creates an instance of our generic FormHandler class, supplying the selector. Remember that the constructor will check that our selector works correctly and complain if it doesn't.

Finally, the last line adds an anonymous function to do the specific stuff we want done for the coffee order form, namely that it should use the form data (which our generic code has bundled up and passed to our callback in form_data) to create an order, using the truck's createOrder method.

While the code in our FormSubmission class is complex, the class reduces the complexity of our main code. Furthermore, adding another form with some custom handling would be much easier, saving us time and effort.

(The idea of putting a lot of work into a class, making other parts of the code easier, can be viewed in a more general way. If it takes me an hour to grade a student's programming assignment, but ten hours to write an auto-grader, that's probably not a good use of my time if I only have a few students. But if I teach 30 students, the auto-grader saves time overall. In general, using something many times allows us to amortize the cost (effort) of creating the thing.)

Values

Of course, sometimes you don't have to solve the big problem, like serializing a form in a generic way. If you just want to pull out a single value, rather than serialize the entire form, you can use the .value attribute or the jQuery .val() method:

var x = document.querySelector('#id_of_input').value;
var x = $("#id_of_input").val();

Of course, you can use different selectors, as needed. We'll see more about this later.

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
  • One way is to serialize an entire form, into an array of objects that are name/value pairs
  • Another is to pull out particular values using .value or .val()
  • We can define a class to process forms, with a method being invoked when the form is submitted
  • such a thing is an event handler for the submission event (analogous to a click event)
  • Coffeerun will call that a form submission handler