Skip to content

Instantly share code, notes, and snippets.

@james-jlo-long
Last active October 2, 2017 15:30
Show Gist options
  • Save james-jlo-long/deeea69801585777be1f82a9f55e7827 to your computer and use it in GitHub Desktop.
Save james-jlo-long/deeea69801585777be1f82a9f55e7827 to your computer and use it in GitHub Desktop.
A form validator that relies on HTML5 Form Validation
/**
* Validates the form. The validation relies on HTML5 Form Validation.
*
* @see https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/Form_validation
* @alias validate
* @param {Element} form
* Form to validate.
* @param {Object} settings
* Settings for the form.
* @param {String} settings.highlightClass
* Class to be added to the form elements to show their validation state.
* @param {Object} settings.messages
* Validation messages. The key should be the input's name and the value
* should be an object of constraint validation API properties to the
* message. An example is shown below.
*
* @example <caption>Validating a form</caption>
* validate(document.querySelector("form"), {
* messages: {
* email: {
* valueMissing: "The e-mail address is essential"
* }
* }
* });
*/
var validate = (function () {
"use strict";
const INPUT_SELECTOR = (
"input:not([type=\"submit\"]):not([type=\"hidden\"]),"
+ "select,"
+ "textarea"
);
const CHECKBOX_SELECTOR = "[type=\"checkbox\"]";
const DEFAULT_SETTINGS = {
highlightClass: "validate-field",
messages: {}
};
/**
* Creates an array of elements that match the given selector. The selector
* can be limited so it only searches within a given context element.#
*
* @private
* @param {String} selector
* CSS selector representing the elements to find.
* @param {Element} [context = document]
* Optional context for the CSS selector. If ommitted, document is
* assumed.
* @return {Array.<Element>}
* Array of any/all matching elements.
*/
function domArray(selector, context) {
return Array.from((context || document).querySelectorAll(selector));
}
/**
* Adds the given highlight class to the element but only if the element
* matches the INPUT_SELECTOR CSS selector (and doesn't already have the
* class).
*
* @private
* @param {Element} element
* Element to which the class should be added.
* @param {String} highlightClass
* Class to add to the element.
*/
function addHighlightClass(element, highlightClass) {
if (element.matches(INPUT_SELECTOR)) {
element.classList.add(highlightClass);
}
};
/**
* Helper function for getting all inputs that match the given input's name.
*
* @private
* @see domArray
* @param {Element} input
* Input element whose name should be used to find all related
* elements.
* @param {Element} form
* Form in which to find the elements.
* @return {Array.<Element>}
* All elements that have the given element's name.
*/
function getMatchingName(input, form) {
return domArray(`[name="${input.name}"]`, form);
}
/**
* Returns whether or not the input is required.
*
* @private
* @param {Element} input
* Input whose required state should be returned.
* @return {Boolean}
* true if the input is required, false otherwise.
*/
function isRequired(input) {
return input.required;
}
/**
* Returns whether or not the input is checked.
*
* @private
* @param {Element} input
* Input whose checked state should be returned.
* @return {Boolean}
* true if the input is checked, false otherwise.
*/
function isChecked(input) {
return input.checked;
}
/**
* Sets the validation message based on the given input's validation state
* and available messages. If no matching message is found, the default
* validation message is returned.
*
* @private
* @param {Element} input
* Input which should possibly get a custom validation message set.
* @param {Object} allMessages
* All validation messages.
*/
function setValidationMessage(input, allMessages) {
var validity = input.validity;
var messages = allMessages[input.name];
var isMsgSet = false;
if (messages) {
Object.keys(messages).some(function (test) {
if (validity[test]) {
input.setCustomValidity(messages[test]);
isMsgSet = true;
}
return isMsgSet;
});
if (!isMsgSet) {
input.setCustomValidity("");
}
}
}
return function (form, settings) {
var config = Object.assign({}, DEFAULT_SETTINGS, settings || {});
var allMessages = config.messages;
var highlightClass = config.highlightClass;
// Create an opt-out.
if (!form.novalidate && !form.dataset.novalidate) {
// Flag the form so we don't re-bind existing functionality.
form.dataset.novalidate = true;
form.addEventListener("focusout", function (e) {
addHighlightClass(e.target, highlightClass);
});
form.addEventListener("invalid", function (e) {
domArray(INPUT_SELECTOR, form).forEach(function (input) {
addHighlightClass(input, highlightClass);
if (input.matches(CHECKBOX_SELECTOR)) {
getMatchingName(input, form).forEach(function (check) {
check.required = input.required;
});
}
if (allMessages) {
setValidationMessage(input, allMessages);
}
});
}, true);
form.addEventListener("change", function (e) {
var input = e.target;
var others;
if (input.matches("[type=\"checkbox\"]")) {
others = getMatchingName(input, form);
if (others.filter(isRequired).length) {
if (input.checked) {
others.forEach(function (other) {
other.required = (other === input);
});
} else if (!others.filter(isChecked).length) {
others.forEach(function (other) {
other.required = true;
});
}
}
}
});
if (allMessages) {
form.addEventListener("input", function (e) {
setValidationMessage(e.target, allMessages);
});
}
}
};
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment