UNPKG

valy

Version:

Intuitive frontend form validation

1,096 lines (852 loc) 30.6 kB
(function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define("Valy", [], factory); else if(typeof exports === 'object') exports["Valy"] = factory(); else root["Valy"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ function(module, exports, __webpack_require__) { 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); __webpack_require__(1); var _valy = __webpack_require__(2); var _valy2 = _interopRequireDefault(_valy); var _valy3 = __webpack_require__(3); var _valy4 = _interopRequireDefault(_valy3); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Constants */ var INPUT_TYPE_EVENT_INPUT = ['text', 'password', 'color', 'date', 'datetime', 'datetime', 'email', 'month', 'number', 'range', 'search', 'tel', 'time', 'url', 'week']; /** * Plugion class * Provides API to the user. */ var Valy = function () { /** * Plugin class constructor * @param {element} formElement * @param {Object} settings Config object */ function Valy(formElement) { var settings = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Valy); // Prevent initialisation if there is no element present. if (!formElement) { return; } this.formElement = formElement; this.settings = settings; this.init(); } /** * Class initialize method */ _createClass(Valy, [{ key: 'init', value: function init() { // Stop the browser default validation this.formElement.setAttribute('novalidate', true); this.bind(); } /** * Class event bind method */ }, { key: 'bind', value: function bind() { var _this = this; this.formElement.addEventListener('submit', function (event) { return _this.handleSubmit(event); }); _valy2.default.collectFormElements(this.formElement).forEach(function (element) { element.addEventListener(INPUT_TYPE_EVENT_INPUT.includes(element.type.toLowerCase()) ? 'input' : 'change', function (event) { return _this.handleElementChange(event); }); }); } /** * Handles submit event - validates form and prevents form submission if there are errors. */ }, { key: 'handleSubmit', value: function handleSubmit(event) { if (!Valy.validateForm(this.formElement).valid) { event.preventDefault(); } } /** * Handles element change - live validate elements. */ }, { key: 'handleElementChange', value: function handleElementChange(event) { Valy.validateElement(event.target); // Validate radio siblings, or they may have error set from previous validations. if (event.target.type.toLowerCase() === 'radio') { _valy2.default.getRadioSiblings(event.target).forEach(function (element) { if (element !== event.target) Valy.validateElement(element); }); } } /** * Validates form * @param {Element} formElement * @return {Object} Form valid state and form errors */ }], [{ key: 'validateForm', value: function validateForm(formElement) { // Collect form elements var errors = _valy2.default.collectFormElements(formElement) // Validate each element .map(function (element) { return { element: element, validation: Valy.validateElement(element) }; }) // Filter only errors .filter(function (item) { return item.validation.errors.length; }); var valid = !errors.length; if (valid) { Valy.setFormValid(formElement); } else { Valy.setFormInvalid(formElement, errors); } return { valid: valid, errors: errors }; } /** * Validates element * @param {Element} element * @return {Object} Element valid state and errors */ }, { key: 'validateElement', value: function validateElement(element) { var validationType = _valy2.default.determineValidationType(element); var validationRules = _valy2.default.getValidationRules(element); var validation = { valid: true, errors: [] }; switch (validationType) { case 'file': validation = _valy2.default.validateFile(element, validationRules); break; case 'checkbox': validation = _valy2.default.validateCheckbox(element, validationRules); break; case 'radio': validation = _valy2.default.validateRadio(element, validationRules); break; case 'select': validation = _valy2.default.validateSelect(element, validationRules); break; case 'field': validation = _valy2.default.validateField(element, validationRules); break; default: console.error('ValyJS: Can\'t validate "' + (element.name || element.type) + '" element!'); } if (validation.valid) { Valy.setElementValid(element); } else { Valy.setElementInvalid(element, validation.errors); } return validation; } /** * Sets element as valid * @param {Element} element */ }, { key: 'setElementValid', value: function setElementValid(element) { _valy4.default.setElementValid(element); } /** * Sets element as invalid * @param {Element} element * @param {Array} errors Array of errors */ }, { key: 'setElementInvalid', value: function setElementInvalid(element, errors) { _valy4.default.setElementInvalid(element, errors); } /** * Sets element as valid * @param {Element} element */ }, { key: 'setFormValid', value: function setFormValid(element) { _valy4.default.setFormValid(element); } /** * Sets element as invalid * @param {Element} element * @param {Array} errors Array of elements ad errors */ }, { key: 'setFormInvalid', value: function setFormInvalid(element, errors) { _valy4.default.setFormInvalid(element, errors); } }]); return Valy; }(); exports.default = Valy; module.exports = exports['default']; /***/ }, /* 1 */ /***/ function(module, exports) { 'use strict'; // Polyfills // Adds support for Element.closest() - Android 4.4, Edge, Safari 8, iOS Safari 8.4 if (!('closest' in Element.prototype)) { Element.prototype.closest = function (selector) { var all = Array.from(document.querySelectorAll(selector)); var current = this; while (current && !all.includes(current)) { current = current.parentNode; } return current; }; } // Adds support for Element.remove() - IE 11, Android 4.3, Safari 6, iOS Safari 6.1 if (!('remove' in Element.prototype)) { Element.prototype.remove = function () { if (this.parentNode) { this.parentNode.removeChild(this); } }; } /***/ }, /* 2 */ /***/ function(module, exports) { 'use strict'; /** * Constants */ Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var INPUT_TYPE_FIELD = ['text', 'password', 'color', 'date', 'datetime', 'datetime', 'email', 'month', 'number', 'range', 'search', 'tel', 'time', 'url', 'week', 'file']; var EXCLUDED_ELEMENT_NAMES = ['button', 'keygen', 'output']; var EXCLUDED_INPUT_TYPES = ['submit', 'reset', 'button', 'datetime-local', 'hidden']; var EMAIL_REGEX = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; var rulesAttribute = 'data-valy-rules'; /** * Core class * Handles core methods. */ var Core = function () { function Core() { _classCallCheck(this, Core); } _createClass(Core, null, [{ key: 'getRegExpFromString', /** * Converts RegExp like string to RegExp object * @param {String} input * @return {RegExp} */ value: function getRegExpFromString(input) { return new RegExp(input.match(/^\/(.+)\/(\w*)$/)[1], input.match(/^\/(.+)\/(\w*)$/)[2]); } /** * Gets radio element siblings * @param {Element} element * @return {Array} Array of elements */ }, { key: 'getRadioSiblings', value: function getRadioSiblings(element) { return Array.from((element.form || document).querySelectorAll('[name="' + element.name + '"]')); } /** * Determines the validation type of an element * @param {Element} element * @return {String or null} Validation type or null */ }, { key: 'determineValidationType', value: function determineValidationType(element) { var elementName = element.nodeName.toLowerCase(); var elementType = element.type.toLowerCase(); switch (true) { case elementName === 'textarea': case elementName === 'input' && INPUT_TYPE_FIELD.includes(elementType): return 'field'; case elementName === 'input' && elementType === 'checkbox': return 'checkbox'; case elementName === 'input' && elementType === 'radio': return 'radio'; case elementName === 'select': return 'select'; default: return null; } } /** * Collects form elements * @param {Element} formElement * @return {Array} Array of elements */ }, { key: 'collectFormElements', value: function collectFormElements(formElement) { return Array.from(formElement.elements).filter(function (element) { var elementName = element.nodeName.toLowerCase(); var elementType = element.type.toLowerCase(); return !(EXCLUDED_ELEMENT_NAMES.includes(elementName) || elementName === 'input' && EXCLUDED_INPUT_TYPES.includes(elementType)); }); } /** * Gets custom validation rules * @param {Element} element * @return {Array} Array of validation rules */ }, { key: 'getCustomValidationRules', value: function getCustomValidationRules(element) { var escapedRegexes = []; return element.getAttribute(rulesAttribute) // Exclude the escaped slashes from the string .replace(/\\\//g, '@@escapedRegexSlash@@') // Exclude the regexes from the string .replace(/\/.*?\/\w*/g, function (match) { escapedRegexes.push(match); return '@@escapedRegex' + (escapedRegexes.length - 1) + '@@'; }) // Split the rules into array .split(/;\s?/) // Remove the empty rules .filter(function (rule) { return !!rule; }).map(function (rule) { var key = rule.split('(')[0]; var options = rule // Remove the key .replace(key, '') // Remove the brackets .replace(/\(|\)/g, '') // Split the options into array .split(/,\s?/) // Remove the empty options .filter(function (option) { return option !== ''; }) // Add back the regexes or convert to number .map(function (option) { if (!isNaN(option)) { return Number(option); } else if (option.match(/@@escapedRegex(\d+)@@/)) { return Core.getRegExpFromString(option.replace(/@@escapedRegex(\d+)@@/, function (match, index) { return escapedRegexes[index - 0].replace(/@@escapedRegexSlash@@/g, '\\/'); })); } return option; }); return { key: key, options: options }; }); } /** * Gets validation rules * @param {Element} element * @return {Array} Array of validation rules */ }, { key: 'getValidationRules', value: function getValidationRules(element) { var rules = []; if (element.required) { rules.push({ key: 'required', options: [] }); } if (element.pattern) { rules.push({ key: 'pattern', options: new RegExp(element.pattern) }); } if (element.type && element.type === 'email') { rules.push({ key: 'email' }); } if (element.getAttribute(rulesAttribute)) { rules.push.apply(rules, _toConsumableArray(Core.getCustomValidationRules(element))); } return rules; } /** * Validates file * @param {Element} element * @param {Array} validationRules * @return {Object} Element valid state and errors */ }, { key: 'validateFile', value: function validateFile(element, validationRules) { var valid = true; var errors = []; validationRules.forEach(function (rule) { switch (rule.key) { case 'required': if (!element.value) { valid = false; errors.push('required'); } break; } }); return { valid: valid, errors: errors }; } /** * Validates checkbox * @param {Element} element * @param {Array} validationRules * @return {Object} Element valid state and errors */ }, { key: 'validateCheckbox', value: function validateCheckbox(element, validationRules) { var valid = true; var errors = []; validationRules.forEach(function (rule) { switch (rule.key) { case 'required': if (!element.checked) { valid = false; errors.push('required'); } break; case 'unchecked': if (element.checked) { valid = false; errors.push('unchecked'); } break; } }); return { valid: valid, errors: errors }; } /** * Validates radio * @param {Element} element * @param {Array} validationRules * @return {Object} Element valid state and errors */ }, { key: 'validateRadio', value: function validateRadio(element, validationRules) { var valid = true; var errors = []; var siblings = Core.getRadioSiblings(element); validationRules.forEach(function (rule) { switch (rule.key) { case 'required': var hasCheckedSibling = false; siblings.forEach(function (element) { if (element.checked) { hasCheckedSibling = true; } }); if (!hasCheckedSibling) { valid = false; errors.push('required'); } break; case 'selected': if (!element.checked) { valid = false; errors.push('selected'); } break; case 'unselected': if (element.checked) { valid = false; errors.push('unselected'); } break; } }); return { valid: valid, errors: errors }; } /** * Validates select * @param {Element} element * @param {Array} validationRules * @return {Object} Element valid state and errors */ }, { key: 'validateSelect', value: function validateSelect(element, validationRules) { var valid = true; var errors = []; validationRules.forEach(function (rule) { switch (rule.key) { case 'required': if (element.value === '') { valid = false; errors.push('required'); } break; case 'exact': if (element.value !== rule.options[0]) { valid = false; errors.push('exact'); } break; case 'selectedCount': var selectedCount = Array.from(element.options).filter(function (option) { return option.selected; }).length; if (selectedCount < rule.options[0] || selectedCount > rule.options[1]) { valid = false; errors.push('selectedCount'); } break; } }); return { valid: valid, errors: errors }; } /** * Validates field * @param {Element} element * @param {Array} validationRules * @return {Object} Element valid state and errors */ }, { key: 'validateField', value: function validateField(element, validationRules) { var valid = true; var value = element.value; var errors = []; validationRules.forEach(function (rule) { switch (rule.key) { case 'required': if (value === '') { valid = false; errors.push('required'); } break; case 'pattern': if (value !== '' && !(rule.options[0] || rule.options).test(value)) { valid = false; errors.push('pattern'); } break; case 'email': if (value !== '' && !value.match(EMAIL_REGEX)) { valid = false; errors.push('email'); } break; case 'presence': if (value.length < (rule.options.length ? rule.options[0] : 1) || value.length > rule.options[1]) { valid = false; errors.push('presence'); } break; case 'exact': if (value !== rule.options[0]) { valid = false; errors.push('exact'); } break; case 'number': if (isNaN(value) || Number(value) < rule.options[0] || Number(value) > rule.options[1]) { valid = false; errors.push('number'); } break; case 'matchField': if (document.querySelector(rule.options[0]) && value !== document.querySelector(rule.options[0]).value) { valid = false; errors.push('matchField'); } break; } }); return { valid: valid, errors: errors }; } }]); return Core; }(); exports.default = Core; module.exports = exports['default']; /***/ }, /* 3 */ /***/ function(module, exports) { 'use strict'; /** * Constants */ Object.defineProperty(exports, "__esModule", { value: true }); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var classHolderAttribute = 'data-valy-class-holder'; var messageContainerAttribute = 'data-valy-message-container'; var errorByTypeAttribute = 'data-valy-error-'; var elementInvalidClass = 'valy-invalid'; var elementValidClass = 'valy-valid'; var errorMessageClass = 'valy-error-message'; var formElementInvalidClass = 'valy-form-invalid'; var formElementValidClass = 'valy-form-valid'; /** * UI Class * Handles DOM modifications */ var UI = function () { function UI() { _classCallCheck(this, UI); } _createClass(UI, null, [{ key: 'getHTMLClassHolder', /** * Gets error/valid class holder of element * @param {Element} element * @return {Element} Class holder */ value: function getHTMLClassHolder(element) { return element.closest(element.getAttribute(classHolderAttribute)) || element; } /** * Gets message container for element messages * @param {Element} element * @return {Element} Message container */ }, { key: 'getMessageContainer', value: function getMessageContainer(element) { var messageContainerSelector = element.getAttribute(messageContainerAttribute); var messageContainer = null; var current = element; if (!messageContainerSelector) { return null; } while (current && !messageContainer) { messageContainer = current.querySelector(messageContainerSelector); current = current.parentNode; } return messageContainer; } /** * Gets error message for specific element by it's type * @param {Element} element * @param {String} errorType * @return {String} Error message */ }, { key: 'getErrorMessageByType', value: function getErrorMessageByType(element, errorType) { return element.getAttribute(errorByTypeAttribute + errorType); } /** * Sets element as valid * @param {Element} element */ }, { key: 'setElementValid', value: function setElementValid(element) { var classHolder = UI.getHTMLClassHolder(element); // Make element valid element.setCustomValidity(''); // Add valid class to the class holder classHolder.classList.add(elementValidClass); // Remove invalid class from the class holder classHolder.classList.remove(elementInvalidClass); UI.clearElementErrorMessages(element); } /** * Sets element as invalid * @param {Element} element * @param {Array} errors Array of errors */ }, { key: 'setElementInvalid', value: function setElementInvalid(element, errors) { var classHolder = UI.getHTMLClassHolder(element); // Make element invalid element.setCustomValidity(errors.join(', ')); // Add invalid class to the class holder classHolder.classList.add(elementInvalidClass); // Remove valid class from the class holder classHolder.classList.remove(elementValidClass); UI.setElementErrorMessages(element, errors); } /** * Sets form as valid * @param {Element} element */ }, { key: 'setFormValid', value: function setFormValid(element) { // Add valid class to the element element.classList.add(formElementValidClass); // Remove invalid class from the element element.classList.remove(formElementInvalidClass); UI.clearElementErrorMessages(element); } /** * Sets form as invalid * @param {Element} element * @param {Array} formErrors Array of elements and errors */ }, { key: 'setFormInvalid', value: function setFormInvalid(element, formErrors) { // Add error class to the element element.classList.add(formElementInvalidClass); // Remove valid class from the element element.classList.remove(formElementValidClass); UI.setFormErrorMessages(element, formErrors); } /** * Clears element error messages * @param {Element} element */ }, { key: 'clearElementErrorMessages', value: function clearElementErrorMessages(element) { var messageContainerElement = UI.getMessageContainer(element); // Clear messages if (messageContainerElement) { messageContainerElement.querySelectorAll('.' + errorMessageClass).forEach(function (element) { return element.remove(); }); } } /** * Sets element error messages * @param {Element} element * @param {Array} errors Array of errors */ }, { key: 'setElementErrorMessages', value: function setElementErrorMessages(element, errors) { var messageContainerElement = UI.getMessageContainer(element); // Set messages if (messageContainerElement) { (function () { var messagesFragment = document.createDocumentFragment(); // Remove duplicates [].concat(_toConsumableArray(new Set(errors))).forEach(function (error) { var errorText = UI.getErrorMessageByType(element, error); if (errorText) { var errorElement = document.createElement('span'); errorElement.textContent = errorText; errorElement.classList.add(errorMessageClass); messagesFragment.appendChild(errorElement); } }); UI.clearElementErrorMessages(element); messageContainerElement.appendChild(messagesFragment); })(); } } /** * Sets form element error messages * @param {Element} formElement * @param {Array} formErrors Array of elements and errors */ }, { key: 'setFormErrorMessages', value: function setFormErrorMessages(formElement, formErrors) { var messageContainerElement = UI.getMessageContainer(formElement); // Set messages if (messageContainerElement) { (function () { var messagesFragment = document.createDocumentFragment(); formErrors.forEach(function (formError) { // Remove duplicates [].concat(_toConsumableArray(new Set(formError.validation.errors))).forEach(function (error) { var errorText = UI.getErrorMessageByType(formError.element, error); if (errorText) { var errorElement = document.createElement('span'); errorElement.textContent = errorText; errorElement.classList.add(errorMessageClass); messagesFragment.appendChild(errorElement); } }); }); UI.clearElementErrorMessages(formElement); messageContainerElement.appendChild(messagesFragment); })(); } } }]); return UI; }(); exports.default = UI; module.exports = exports['default']; /***/ } /******/ ]) }); ; //# sourceMappingURL=valy.js.map