Form example

How it works

Client-side validation for the form components. One config per form: which fields, which rule, when to check.

Form validates the fields of a c--form-a and shows the result in the markup: the wrapper of the control gets a --error or --valid modifier and the message goes into the c--form-error-a span of its group. The rules come from the @andresclua/validate package, one function per kind of control: isString, isNumber, isSelect, isRadio, isCheckbox, isEmail and isFile.

The class in src/js/handler/form/Form.js wraps those functions: it attaches the listeners each field asks for, runs every field on submit, writes the state into the DOM and removes its listeners on destroy(). Nothing is sent anywhere; what happens after a valid submit is yours, in onComplete.

The markup

A form with a js-- hook and one c--form-group-a per field. The error span is optional: if a group has none, the class creates it when the first message is needed.

<form class="c--form-a js--form-validate-blur">
  <!-- One group per field: label, control wrapper and the error message -->
  <div class="c--form-group-a">
    <label class="c--label-a" for="job_title">Job Title</label>
    <div class="c--form-input-a">
      <input class="c--form-input-a__item" type="text" id="job_title" />
    </div>
    <span class="c--form-error-a"></span>
  </div>
  <button class="c--btn-a" type="submit">Submit</button>
</form>

The handler

Handler.js maps each hook to a config, and the configs live in src/js/handler/form/variations/index.js. A config is a function that receives the form element and returns its fields, so every selector in it is scoped to that form. This is the shortest one:

export const configFormBlur = (payload) => {
    var { Manager, element} = payload;
    var gsap = Manager.libraries.GSAP.gsap;
    return {
        element,
        fields: [
            {
                element: element.querySelector("#job_title"),
                validationFunction: "isString",
                config: {
                    required: true,
                    minLength: 2,
                    customMessage: {
                        required: "Job Title can't be empty.",
                        minLength: "Job Title must be at least 2 characters long.",
                    },
                },
                on: 'blur',
            },
        ],
        onComplete: () => {
            const spinner = element.querySelector(".c--loader-a");
            if (!spinner) return;

            const tl = gsap.timeline();

            tl.set(spinner, { display: "block" }) // in case it was hidden
            .fromTo(
                spinner,
                { opacity: 0 },
                { opacity: 1, duration: 0.3, ease: "power2.out" } // quick fade-in
            )
            .to(
                spinner,
                { opacity: 1, duration: 0.8 } // leave it there a bit (feels natural)
            )
            .to(
                spinner,
                {
                opacity: 0,
                duration: 0.4,
                ease: "power2.inOut",
                onComplete: () => {
                    spinner.style.display = "none";
                },
                }
            );
        }
    };
};

What the framework does for you

  • Lazy loading. A form is only instanced when it gets within 30px of the viewport (Boostify), and the class is imported once and cached by the Manager.
  • Page transitions. The listeners are removed before swup replaces the content, and the forms on the new page are instanced again.
  • Enter key. Every form listens to its own submit event, so pressing Enter in a field validates everything, with or without a submit button in the config.

Validate on blur

One field, checked as soon as you leave it.

Blur on: "blur"isStringonComplete

Type something short in the field and click outside: the wrapper turns red and the message appears under it, "Job Title must be at least 2 characters long." Leave it empty and you get "Job Title can't be empty." instead. Come back, fix it and leave again: the message goes, the wrapper turns green.

Submit, with the button or with Enter, validates the field again and, when it passes, runs onComplete: here a GSAP timeline that fades the c--loader-a spinner in and out at the bottom right corner of the screen. GSAP comes from the Manager, like every other library.

export const configFormBlur = (payload) => {
    var { Manager, element} = payload;
    var gsap = Manager.libraries.GSAP.gsap;
    return {
        element,
        fields: [
            {
                element: element.querySelector("#job_title"),
                validationFunction: "isString",
                config: {
                    required: true,
                    minLength: 2,
                    customMessage: {
                        required: "Job Title can't be empty.",
                        minLength: "Job Title must be at least 2 characters long.",
                    },
                },
                on: 'blur',
            },
        ],
        onComplete: () => {
            const spinner = element.querySelector(".c--loader-a");
            if (!spinner) return;

            const tl = gsap.timeline();

            tl.set(spinner, { display: "block" }) // in case it was hidden
            .fromTo(
                spinner,
                { opacity: 0 },
                { opacity: 1, duration: 0.3, ease: "power2.out" } // quick fade-in
            )
            .to(
                spinner,
                { opacity: 1, duration: 0.8 } // leave it there a bit (feels natural)
            )
            .to(
                spinner,
                {
                opacity: 0,
                duration: 0.4,
                ease: "power2.inOut",
                onComplete: () => {
                    spinner.style.display = "none";
                },
                }
            );
        }
    };
};
Live example
on: "blur"isStringonComplete

Validate on submit

Nothing happens while you type: the check runs when you click the button.

Submit on: nullsubmitButtonSelectorisString

The field has on: null, so no listener is attached to it. The config names the button instead, with submitButtonSelector, and the class validates every field when that button is clicked. Click it with the field empty: "Name can't be empty." With fewer than five characters: "Name must be at least 5 characters long." The message stays until the next click, however much you type in between; once the name passes, the wrapper turns green and the message goes.

The selector is resolved inside the form, so two forms on the same page can use the same class for their buttons.

export const configFormClick = ({element}) => ({
    element,
    submitButtonSelector: ".js--form-validate-click-submit",
    fields: [
        {
            element: element.querySelector("#name"),
            validationFunction: "isString",
            config: {
                required: true,
                minLength: 5,
                customMessage: {
                    required: "Name can't be empty.",
                    minLength: "Name must be at least 5 characters long.",
                },
            },
            on: null,
        },
    ],
    onSubmit: () => {
        // custom logic (click simple)
    },
});
Live example
on: nullsubmitButtonSelectorisString

Select, radio and checkbox

Five fields of four kinds, all checked on submit.

Submit isStringisNumberisSelectisRadioisCheckbox

Every field here has on: null: click Submit with the form empty and all five messages appear at once. Username is required. Age is required and must be a number: type letters and you get "The value must be a valid number !". Country uses isSelect, which fails while the placeholder option is still selected, with the default message of the package. Gender uses isRadio on the three radios of the group: "Select a Gender" until one is checked. Fruits uses isCheckbox with minRequired: 2: "At least two options must be selected." until two are.

Inputs and selects get the --error and --valid modifiers on their wrapper. Radio and checkbox groups only get the message, in the c--form-error-a span of their group. The radios and checkboxes are passed as a NodeList with elements, queried inside the form.

export const configFormAdvanced = ({element}) => ({
    element,
    submitButtonSelector: ".js--form-validate-advanced-submit",
    fields: [
        {
            element: element.querySelector("#username"),
            validationFunction: "isString",
            config: {
                required: true,
                customMessage: {
                    required: "username can't be empty.",
                },
            },
            on: null,
        },
        {
            element: element.querySelector("#age"),
            validationFunction: "isNumber",
            config: {
                required: true,
                customMessage: {
                    required: "Age can't be empty.",
                    invalid: "The value must be a valid number !",
                },
            },
            on: null,
        },
        {
            element: element.querySelector("#country"),
            validationFunction: "isSelect",
            config: {
                required: true,
            },
            on: null,
        },
        {
            elements: element.querySelectorAll("#radioForm .c--form-radio-a__item"),
            validationFunction: "isRadio",
            config: {
                required: true,
                customMessage: { 
                    required: "Select a Gender",
                },
            },
            on: null,
        },
        {
            elements: element.querySelectorAll("#checkbox .c--form-checkbox-a__item"),
            validationFunction: "isCheckbox",
            config: {
                minRequired: 2,
                customMessage: { 
                    minRequired: "At least two options must be selected." 
                },
            },
            on: null,
        },
       
    ],
    onSubmit: () => {
        // custom logic (advanced)



    },
});
Live example
isStringisNumberisSelectisRadioisCheckbox
Gender
Fruits

Options reference

What a config can set, at form level and per field.

Reference

Form

OptionValuesWhat it does
elementthe form elementGiven to the config by the handler. Required
fieldsarray of fieldsThe fields to validate, see below
submitButtonSelectorselector string, default nullThe button that triggers the validation of every field, looked up inside the form. The submit event of the form always does it too
onSubmitfunctionRuns on submit, before the fields are validated
onCompletefunctionRuns on submit when every field is valid
onErrorfunctionRuns on submit when any field fails, with the list of invalid fields and their messages

Field

OptionValuesWhat it does
elementan input or selectThe control to validate. Its value is passed to the rule
elementsa NodeListFor isRadio and isCheckbox: the radios or checkboxes of the group
validationFunction"isString", "isNumber", "isSelect", "isRadio", "isCheckbox", "isEmail", "isFile", or a functionThe rule. A string picks one of the package validators; a function is used as is
onevent name, default "blur", or nullWhen the field is checked on its own. null attaches no listener: the field is only checked on submit
configobjectThe options of the rule, see below

Rule config

OptionRulesWhat it does
requiredisString, isNumber, isSelectFails on an empty value. isRadio always needs one option checked, whatever the config says
minLengthisStringMinimum number of characters
minRequiredisCheckboxHow many boxes must be checked, default 1
customMessageallYour messages, one per check: required, minLength, minRequired and, for isNumber, invalid when the value is not a number. A check without a message uses the default of the package

The package has more checks than this page uses (maxLength, pattern, min, max, integer, a customValidation function); they go in the same config object and are documented in its readme.