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
submitevent, 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.
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";
},
}
);
}
};
}; on: "blur"isStringonComplete Validate on submit
Nothing happens while you type: the check runs when you click the button.
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)
},
}); on: nullsubmitButtonSelectorisString Select, radio and checkbox
Five fields of four kinds, all checked on 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)
},
}); isStringisNumberisSelectisRadioisCheckbox Options reference
What a config can set, at form level and per field.
Form
| Option | Values | What it does |
|---|---|---|
element | the form element | Given to the config by the handler. Required |
fields | array of fields | The fields to validate, see below |
submitButtonSelector | selector string, default null | The button that triggers the validation of every field, looked up inside the form. The submit event of the form always does it too |
onSubmit | function | Runs on submit, before the fields are validated |
onComplete | function | Runs on submit when every field is valid |
onError | function | Runs on submit when any field fails, with the list of invalid fields and their messages |
Field
| Option | Values | What it does |
|---|---|---|
element | an input or select | The control to validate. Its value is passed to the rule |
elements | a NodeList | For isRadio and isCheckbox: the radios or checkboxes of the group |
validationFunction | "isString", "isNumber", "isSelect", "isRadio", "isCheckbox", "isEmail", "isFile", or a function | The rule. A string picks one of the package validators; a function is used as is |
on | event name, default "blur", or null | When the field is checked on its own. null attaches no listener: the field is only checked on submit |
config | object | The options of the rule, see below |
Rule config
| Option | Rules | What it does |
|---|---|---|
required | isString, isNumber, isSelect | Fails on an empty value. isRadio always needs one option checked, whatever the config says |
minLength | isString | Minimum number of characters |
minRequired | isCheckbox | How many boxes must be checked, default 1 |
customMessage | all | Your 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.