Working with Ampersand Forms

Let's be honest, forms are a pain!

Good forms don't just require markup, they require behavior. You want to give the user feedback on what they're doing often as they're doing it, but not be annoying.

Nothing's more frustrating as a user, than submitting a form with mostly correct data and being surprised by the failure, and it's even worse if they lose some data in the process.

Take a simple text input, how do we want it to behave? Well, it depends. If it's empty to start with and the user is first entering data, we don't just want to show them errors, they know it's incomplete, so we don't need to tell them they're "wrong", they already know!

But, if they "blur" from one field to go to the next, and the current input value is wrong, we should ideally tell them that before they go to submit it.

Also, if the field value has ever been valid, and they make it invalid, even if they're still focused on the field, we should probably tell them.

This is just for "simple" single-line text entry.

On the developer side, it's hard to do forms right. No matter what library we use, it seems to never quite do what we want it to.

On the other hand, for simple cases if we've got a model that describes exactly what data the API expects, it's annoying to have to write a bunch of form fields each time to just be able to request that data from the users.

So what do we do in ampersand for this?

In order to be flexible enough to support any type of form control you can think up, while keeping the simple stuff relatively simple to do, we handle this with several different tools working together. First, there's ampersand-form-view which manages the form as a whole. It follows the view conventions of ampersand so we can easily use it as a subview within a page.

The form view is made up of subviews, one for each value you're trying to get from the user.

So, the nice thing about that is now we've decoupled <input>s from values. So if you wanted a password from the user, you would have a single "password-view" that actually rendered two <input>s, for password and validation of that password, but ultimately just produced a single value.

So rather than thinking of a "field view" as a single form control, we think of it as a view that's responsible for a certain form value, whatever widget that might be.

So, how does this work? Well, in addition to following the contract for what a view is, it also follows the following rules.

Form input view conventions

Creating a form view

You end up a creating a form view that looks something like this.

var FormView = require('ampersand-form-view');
var InputView = require('ampersand-input-view');
var CheckboxView = require('ampersand-checkbox-view');
var ArrayInputView = require('ampersand-array-input-view');


module.exports = FormView.extend({
    fields: function () {
        return [
            new InputView({
                label: 'Name',
                name: 'name',
                value: this.model && this.model.name,
                placeholder: 'Name',
                parent: this
            }),
            new CheckboxView({
                label: 'Is Awesome?',
                name: 'awesome',
                value: this.model && this.model.isAwesome,
                parent: this
            }),
            new InputView({
                label: 'Coolness Factor',
                name: 'coolnessFactor',
                value: this.model && this.model.coolnessFactor,
                placeholder: '8',
                parent: this,
                type: 'number',
                tests: [
                    function (val) {
                        if (val < 0 || val > 11) return "Must be between 0 and 11";
                    },
                    function (val) {
                        if (!/^[0-9]+$/.test(val)) return "Must be a number.";
                    }
                ]
            }),
            new ArrayInputView({
                label: 'Favorite Colors',
                name: 'colors',
                value: this.model && this.model.colors,
                placeholder: 'blue',
                parent: this,
                numberRequired: 2,
                tests: [
                    function (val) {
                        if (['red', 'blue', 'green'].indexOf(val) === -1) {
                            return "Can only be red, blue, or green. Sorry."
                        }
                    }
                ]
            })
        ];
    }
});

Each of field views inside the form follows the rules above. But, as a whole, you've now got a form that knows how to create valid data with those fields.

Rather than creating a form that posts using traditional methods, you'll have a form that produces data that you can use to create and save, or edit an existing model, with the same form!

Then, in a page you might use the form as follows:

var PageView = require('./base');
var templates = require('../templates');
var PersonForm = require('../forms/person');


module.exports = PageView.extend({
    pageTitle: 'edit person',
    template: templates.pages.personEdit,
    render: function () {
        this.renderWithTemplate();
        this.form = new PersonForm({
            model: this.model,
            el: el,
            submitCallback: function (data) {
                // here you'll get a clean data object keyed
                // by field name. So for the sample form
                // data might look like this:
                // {
                //    name: "holly", 
                //    awesome: true, 
                //    coolnessFactor: 11,
                //    colors: ['red', 'green']
                // }
                console.log(data); 
            }
        });
    }
});

How to use it

The quickest way to build out a starting point for a form in your project is to point ampersand-cli at a model file to generate a form for editing it.

We'll eventually make more "official" input views types. But the idea is, if you want to write a color picker, or a date input view, or a username-checker-input that does server-side validation, or a password field with a strength indicator, you can write a view for that and as long as it follows the form view conventions in the list above and it will still work happily with the rest of the form.

Other examples

There's an example of a working form-view inside the app that gets generated when you follow the quick start guide. It shows how to create a single form view that gets used on separate pages for creating new models and editing existing ones with intelligent, completely customizable validation.

Getting the code itself