pristinejs
Advanced tools
Comparing version
@@ -16,5 +16,23 @@ (function (global, factory) { | ||
min: "Minimum value for this field is ${1}", | ||
max: "Maximum value for this field is ${1}" | ||
max: "Maximum value for this field is ${1}", | ||
pattern: "Input must match the pattern ${1}" | ||
}; | ||
function findAncestor(el, cls) { | ||
while ((el = el.parentElement) && !el.classList.contains(cls)) {} | ||
return el; | ||
} | ||
function tmpl(o) { | ||
var _arguments = arguments; | ||
return this.replace(/\${([^{}]*)}/g, function (a, b) { | ||
return _arguments[b]; | ||
}); | ||
} | ||
function groupedElemCount(input) { | ||
return input.pristine.self.form.querySelectorAll('input[name="' + input.getAttribute('name') + '"]:checked').length; | ||
} | ||
var defaultConfig = { | ||
@@ -31,3 +49,3 @@ classTo: 'form-group', | ||
var SELECTOR = "input:not([type^=hidden]):not([type^=submit]), select, textarea"; | ||
var ALLOWED_ATTRIBUTES = ["required", "min", "max", 'minlength', 'maxlength']; | ||
var ALLOWED_ATTRIBUTES = ["required", "min", "max", 'minlength', 'maxlength', 'pattern']; | ||
@@ -47,47 +65,39 @@ var validators = {}; | ||
_('required', { fn: function fn(val) { | ||
return val !== ''; | ||
return this.type === 'radio' || this.type === 'checkbox' ? groupedElemCount(this) : val !== undefined && val !== ''; | ||
}, priority: 99, halt: true }); | ||
_('email', { fn: function fn(val) { | ||
return (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) | ||
); | ||
}, priority: 1 }); | ||
return !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val); | ||
} }); | ||
_('number', { fn: function fn(val) { | ||
return parseFloat(val); | ||
}, priority: 1 }); | ||
return !val || !isNaN(parseFloat(val)); | ||
}, priority: 2 }); | ||
_('integer', { fn: function fn(val) { | ||
return val && /^\d+$/.test(val); | ||
} }); | ||
_('minlength', { fn: function fn(val, length) { | ||
return console.log(val, length) || val && val.length >= parseInt(length); | ||
}, priority: 1 }); | ||
return !val || val.length >= parseInt(length); | ||
} }); | ||
_('maxlength', { fn: function fn(val, length) { | ||
return val && val.length <= parseInt(length); | ||
}, priority: 1 }); | ||
return !val || val.length <= parseInt(length); | ||
} }); | ||
_('min', { fn: function fn(val, limit) { | ||
return parseFloat(val) >= parseFloat(limit); | ||
}, priority: 1 }); | ||
return !val || (this.type === 'checkbox' ? groupedElemCount(this) >= parseInt(limit) : parseFloat(val) >= parseFloat(limit)); | ||
} }); | ||
_('max', { fn: function fn(val, limit) { | ||
return parseFloat(val) <= parseFloat(limit); | ||
}, priority: 1 }); | ||
return !val || (this.type === 'checkbox' ? groupedElemCount(this) <= parseInt(limit) : parseFloat(val) <= parseFloat(limit)); | ||
} }); | ||
_('pattern', { fn: function fn(val, pattern) { | ||
var m = pattern.match(new RegExp('^/(.*?)/([gimy]*)$'));return !val || new RegExp(m[1], m[2]).test(val); | ||
} }); | ||
function findAncestor(el, cls) { | ||
while ((el = el.parentElement) && !el.classList.contains(cls)) {} | ||
return el; | ||
} | ||
function Pristine(form, config, live) { | ||
function tmpl(o) { | ||
var _arguments = arguments; | ||
return this.replace(/\${([^{}]*)}/g, function (a, b) { | ||
return _arguments[b]; | ||
}); | ||
} | ||
function Pristine(form, config, online) { | ||
var self = this; | ||
init(form, config, online); | ||
init(form, config, live); | ||
function init(form, config, online) { | ||
function init(form, config, live) { | ||
self.form = form; | ||
self.config = config || defaultConfig; | ||
self.online = !(online === false); | ||
self.live = !(live === false); | ||
self.fields = Array.from(form.querySelectorAll(SELECTOR)).map(function (input) { | ||
@@ -97,20 +107,20 @@ | ||
var params = {}; | ||
var messages = {}; | ||
ALLOWED_ATTRIBUTES.forEach(function (item) { | ||
var val = input.getAttribute(item); | ||
if (val !== null) { | ||
_addValidatorToField(fns, params, item, val); | ||
} | ||
}); | ||
[].forEach.call(input.attributes, function (attr) { | ||
if (/^data-pristine-/.test(attr.name)) { | ||
var name = attr.name.substr(14); | ||
if (name.endsWith('-message')) { | ||
messages[name.slice(0, name.length - 8)] = attr.value; | ||
return; | ||
} | ||
if (name === 'type') name = attr.value; | ||
_addValidatorToField(fns, params, name, attr.value); | ||
} else if (~ALLOWED_ATTRIBUTES.indexOf(attr.name)) { | ||
_addValidatorToField(fns, params, attr.name, attr.value); | ||
} else if (attr.name === 'type') { | ||
_addValidatorToField(fns, params, attr.value); | ||
} | ||
}); | ||
_addValidatorToField(fns, params, input.getAttribute('type')); | ||
fns.sort(function (a, b) { | ||
@@ -120,8 +130,7 @@ return b.priority - a.priority; | ||
self.online && input.addEventListener(!~['radio', 'checkbox'].indexOf(input.getAttribute('type')) ? 'input' : 'change', function (e) { | ||
self.live && input.addEventListener(!~['radio', 'checkbox'].indexOf(input.getAttribute('type')) ? 'input' : 'change', function (e) { | ||
self.validate(e.target); | ||
}.bind(self)); | ||
input.pristine = { input: input, validators: fns, params: params }; | ||
return input.pristine; | ||
return input.pristine = { input: input, validators: fns, params: params, messages: messages, self: self }; | ||
}.bind(self)); | ||
@@ -151,3 +160,3 @@ } | ||
valid = false; | ||
!silent && _showError(field, field.messages); | ||
!silent && _showError(field); | ||
} | ||
@@ -158,8 +167,8 @@ } | ||
self.getErrorMessages = function (input) { | ||
return input.length ? input[0].pristine.messages : input.pristine.messages; | ||
self.getErrors = function (input) { | ||
return input.length ? input[0].pristine.errors : input.pristine.errors; | ||
}; | ||
function _validateField(field) { | ||
var messages = []; | ||
var errors = []; | ||
var valid = true; | ||
@@ -172,3 +181,4 @@ for (var i in field.validators) { | ||
valid = false; | ||
messages.push(tmpl.apply(validator.msg, params)); | ||
var error = field.messages[validator.name] || validator.msg; | ||
errors.push(tmpl.apply(error, params)); | ||
if (validator.halt === true) { | ||
@@ -179,3 +189,3 @@ break; | ||
} | ||
field.messages = messages; | ||
field.errors = errors; | ||
return valid; | ||
@@ -188,3 +198,2 @@ } | ||
} else if (elemOrName instanceof HTMLElement) { | ||
//TODO check if pristine field | ||
elemOrName.pristine.validators.push({ fn: fn, msg: msg, priority: priority, halt: halt }); | ||
@@ -197,24 +206,61 @@ elemOrName.pristine.validators.sort(function (a, b) { | ||
function _showError(field, messages) { | ||
var ret = _removeError(field); | ||
var errorClassElement = ret[0], | ||
errorTextParent = ret[1]; | ||
errorClassElement && errorClassElement.classList.add(self.config.errorClass); | ||
function _getErrorElements(field) { | ||
if (field.errorElements) { | ||
return field.errorElements; | ||
} | ||
var errorClassElement = findAncestor(field.input, self.config.classTo); | ||
var errorTextParent = null, | ||
errorTextElement = null; | ||
if (self.config.classTo === self.config.errorTextParent) { | ||
errorTextParent = errorClassElement; | ||
} else { | ||
errorTextParent = errorClassElement.querySelector(self.errorTextParent); | ||
} | ||
if (errorTextParent) { | ||
errorTextElement = errorTextParent.querySelector('.' + PRISTINE_ERROR); | ||
if (!errorTextElement) { | ||
errorTextElement = document.createElement(self.config.errorTextTag); | ||
errorTextElement.className = PRISTINE_ERROR + ' ' + self.config.errorTextClass; | ||
errorTextParent.appendChild(errorTextElement); | ||
errorTextElement.pristineDisplay = errorTextElement.style.display; | ||
} | ||
} | ||
return field.errorElements = [errorClassElement, errorTextElement]; | ||
} | ||
var elem = document.createElement(self.config.errorTextTag); | ||
elem.className = PRISTINE_ERROR + ' ' + self.config.errorTextClass; | ||
elem.innerHTML = messages.join('<br/>'); | ||
errorTextParent && errorTextParent.appendChild(elem); | ||
function _showError(field) { | ||
var errorElements = _getErrorElements(field); | ||
var errorClassElement = errorElements[0], | ||
errorTextElement = errorElements[1]; | ||
if (errorClassElement) { | ||
errorClassElement.classList.remove(self.config.successClass); | ||
errorClassElement.classList.add(self.config.errorClass); | ||
} | ||
if (errorTextElement) { | ||
errorTextElement.innerHTML = field.errors.join('<br/>'); | ||
errorTextElement.style.display = errorTextElement.pristineDisplay || ''; | ||
} | ||
} | ||
self.addError = function (input, error) { | ||
input = input.length ? input[0] : input; | ||
input.pristine.errors.push(error); | ||
_showError(input.pristine); | ||
}; | ||
function _removeError(field) { | ||
var errorClassElement = findAncestor(field.input, self.config.classTo); | ||
errorClassElement && errorClassElement.classList.remove(self.config.errorClass, self.config.successClass); | ||
var errorTextParent = findAncestor(field.input, self.config.errorTextParent); | ||
var existing = errorTextParent ? errorTextParent.querySelector('.' + PRISTINE_ERROR) : null; | ||
if (existing) { | ||
existing.parentNode.removeChild(existing); | ||
var errorElements = _getErrorElements(field); | ||
var errorClassElement = errorElements[0], | ||
errorTextElement = errorElements[1]; | ||
if (errorClassElement) { | ||
// IE > 9 doesn't support multiple class removal | ||
errorClassElement.classList.remove(self.config.errorClass); | ||
errorClassElement.classList.remove(self.config.successClass); | ||
} | ||
return [errorClassElement, errorTextParent]; | ||
if (errorTextElement) { | ||
errorTextElement.innerHTML = ''; | ||
errorTextElement.style.display = 'none'; | ||
} | ||
return errorElements; | ||
} | ||
@@ -228,2 +274,5 @@ | ||
self.reset = function () { | ||
for (var i in self.fields) { | ||
self.fields[i].errorElements = null; | ||
} | ||
Array.from(self.form.querySelectorAll('.' + PRISTINE_ERROR)).map(function (elem) { | ||
@@ -233,3 +282,4 @@ elem.parentNode.removeChild(elem); | ||
Array.from(self.form.querySelectorAll('.' + self.config.classTo)).map(function (elem) { | ||
elem.classList.remove(self.config.successClass, self.config.errorClass); | ||
elem.classList.remove(self.config.successClass); | ||
elem.classList.remove(self.config.errorClass); | ||
}); | ||
@@ -236,0 +286,0 @@ }; |
@@ -1,1 +0,1 @@ | ||
!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):r.Pristine=e()}(this,function(){"use strict";function r(r,e){for(;(r=r.parentElement)&&!r.classList.contains(e););return r}function e(r){var e=arguments;return this.replace(/\${([^{}]*)}/g,function(r,t){return e[t]})}function t(t,n,f){function c(e){var t=r(e.input,m.config.classTo);t&&t.classList.remove(m.config.errorClass,m.config.successClass);var n=r(e.input,m.config.errorTextParent),i=n?n.querySelector("."+s):null;return i&&i.parentNode.removeChild(i),[t,n]}function p(r,e,t,n){var i=u[t];if(i&&(r.push(i),n)){var s=n.split(",");s.unshift(null),e[t]=s}}var m=this;return function(r,e,t){m.form=r,m.config=e||i,m.online=!(!1===t),m.fields=Array.from(r.querySelectorAll(a)).map(function(r){var e=[],t={};return o.forEach(function(n){var i=r.getAttribute(n);null!==i&&p(e,t,n,i)}),[].forEach.call(r.attributes,function(r){if(/^data-pristine-/.test(r.name)){var n=r.name.substr(14);"type"===n&&(n=r.value),p(e,t,n,r.value)}}),p(e,t,r.getAttribute("type")),e.sort(function(r,e){return e.priority-r.priority}),m.online&&r.addEventListener(~["radio","checkbox"].indexOf(r.getAttribute("type"))?"change":"input",function(r){m.validate(r.target)}.bind(m)),r.pristine={input:r,validators:e,params:t},r.pristine}.bind(m))}(t,n,f),m.validate=function(r,t){t=r&&!0===t||!0===r;var n=m.fields;!0!==r&&!1!==r&&(r instanceof HTMLElement?n=[r.pristine]:(r instanceof NodeList||r instanceof(window.$||Array)||r instanceof Array)&&(n=Array.from(r).map(function(r){return r.pristine})));var i=!0;for(var a in n){var o=n[a];!function(r){var t=[],n=!0;for(var i in r.validators){var s=r.validators[i],a=r.params[s.name]?r.params[s.name]:[];if(a[0]=r.input.value,!s.fn.apply(r.input,a)&&(n=!1,t.push(e.apply(s.msg,a)),!0===s.halt))break}return r.messages=t,n}(o)?(i=!1,!t&&function(r,e){var t=c(r),n=t[0],i=t[1];n&&n.classList.add(m.config.errorClass);var a=document.createElement(m.config.errorTextTag);a.className=s+" "+m.config.errorTextClass,a.innerHTML=e.join("<br/>"),i&&i.appendChild(a)}(o,o.messages)):!t&&function(r){var e=c(r)[0];e&&e.classList.add(m.config.successClass)}(o)}return i},m.getErrorMessages=function(r){return r.length?r[0].pristine.messages:r.pristine.messages},m.addValidator=function(r,e,t,n,i){"string"==typeof r?l(r,{fn:e,msg:t,priority:n,halt:i}):r instanceof HTMLElement&&(r.pristine.validators.push({fn:e,msg:t,priority:n,halt:i}),r.pristine.validators.sort(function(r,e){return e.priority-r.priority}))},m.reset=function(){Array.from(m.form.querySelectorAll("."+s)).map(function(r){r.parentNode.removeChild(r)}),Array.from(m.form.querySelectorAll("."+m.config.classTo)).map(function(r){r.classList.remove(m.config.successClass,m.config.errorClass)})},m.destroy=function(){m.reset(),m.fields.forEach(function(r){delete r.input.pristine}),m.fields=[]},m.setGlobalConfig=function(r){i=r},m}var n={required:"This field is required",email:"This field requires a valid e-mail address",number:"This field requires a number",url:"This field requires a valid website URL",tel:"This field requires a valid telephone number",maxlength:"This fields length must be < ${1}",minlength:"This fields length must be > ${1}",min:"Minimum value for this field is ${1}",max:"Maximum value for this field is ${1}"},i={classTo:"form-group",errorClass:"has-danger",successClass:"has-success",errorTextParent:"form-group",errorTextTag:"div",errorTextClass:"text-help"},s="pristine-error",a="input:not([type^=hidden]):not([type^=submit]), select, textarea",o=["required","min","max","minlength","maxlength"],u={},l=function(r,e){e.name=r,e.msg||(e.msg=n[r]),void 0===e.priority&&(e.priority=1),u[r]=e};return l("text",{fn:function(r){return!0},priority:0}),l("required",{fn:function(r){return""!==r},priority:99,halt:!0}),l("email",{fn:function(r){return/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(r)},priority:1}),l("number",{fn:function(r){return parseFloat(r)},priority:1}),l("minlength",{fn:function(r,e){return console.log(r,e)||r&&r.length>=parseInt(e)},priority:1}),l("maxlength",{fn:function(r,e){return r&&r.length<=parseInt(e)},priority:1}),l("min",{fn:function(r,e){return parseFloat(r)>=parseFloat(e)},priority:1}),l("max",{fn:function(r,e){return parseFloat(r)<=parseFloat(e)},priority:1}),t}); | ||
!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):e.Pristine=r()}(this,function(){"use strict";function e(e){var r=arguments;return this.replace(/\${([^{}]*)}/g,function(e,t){return r[t]})}function r(e){return e.pristine.self.form.querySelectorAll('input[name="'+e.getAttribute("name")+'"]:checked').length}function t(r,t,n){function f(e){if(e.errorElements)return e.errorElements;var r=function(e,r){for(;(e=e.parentElement)&&!e.classList.contains(r););return e}(e.input,d.config.classTo),t=null,n=null;return(t=d.config.classTo===d.config.errorTextParent?r:r.querySelector(d.errorTextParent))&&((n=t.querySelector("."+s))||((n=document.createElement(d.config.errorTextTag)).className=s+" "+d.config.errorTextClass,t.appendChild(n),n.pristineDisplay=n.style.display)),e.errorElements=[r,n]}function c(e){var r=f(e),t=r[0],n=r[1];t&&(t.classList.remove(d.config.successClass),t.classList.add(d.config.errorClass)),n&&(n.innerHTML=e.errors.join("<br/>"),n.style.display=n.pristineDisplay||"")}function p(e){var r=function(e){var r=f(e),t=r[0],n=r[1];return t&&(t.classList.remove(d.config.errorClass),t.classList.remove(d.config.successClass)),n&&(n.innerHTML="",n.style.display="none"),r}(e)[0];r&&r.classList.add(d.config.successClass)}function m(e,r,t,n){var i=l[t];if(i&&(e.push(i),n)){var s=n.split(",");s.unshift(null),r[t]=s}}var d=this;return function(e,r,t){d.form=e,d.config=r||i,d.live=!(!1===t),d.fields=Array.from(e.querySelectorAll(a)).map(function(e){var r=[],t={},n={};return[].forEach.call(e.attributes,function(e){if(/^data-pristine-/.test(e.name)){var i=e.name.substr(14);if(i.endsWith("-message"))return void(n[i.slice(0,i.length-8)]=e.value);"type"===i&&(i=e.value),m(r,t,i,e.value)}else~o.indexOf(e.name)?m(r,t,e.name,e.value):"type"===e.name&&m(r,t,e.value)}),r.sort(function(e,r){return r.priority-e.priority}),d.live&&e.addEventListener(~["radio","checkbox"].indexOf(e.getAttribute("type"))?"change":"input",function(e){d.validate(e.target)}.bind(d)),e.pristine={input:e,validators:r,params:t,messages:n,self:d}}.bind(d))}(r,t,n),d.validate=function(r,t){t=r&&!0===t||!0===r;var n=d.fields;!0!==r&&!1!==r&&(r instanceof HTMLElement?n=[r.pristine]:(r instanceof NodeList||r instanceof(window.$||Array)||r instanceof Array)&&(n=Array.from(r).map(function(e){return e.pristine})));var i=!0;for(var s in n){var a=n[s];!function(r){var t=[],n=!0;for(var i in r.validators){var s=r.validators[i],a=r.params[s.name]?r.params[s.name]:[];if(a[0]=r.input.value,!s.fn.apply(r.input,a)){n=!1;var o=r.messages[s.name]||s.msg;if(t.push(e.apply(o,a)),!0===s.halt)break}}return r.errors=t,n}(a)?(i=!1,!t&&c(a)):!t&&p(a)}return i},d.getErrors=function(e){return e.length?e[0].pristine.errors:e.pristine.errors},d.addValidator=function(e,r,t,n,i){"string"==typeof e?u(e,{fn:r,msg:t,priority:n,halt:i}):e instanceof HTMLElement&&(e.pristine.validators.push({fn:r,msg:t,priority:n,halt:i}),e.pristine.validators.sort(function(e,r){return r.priority-e.priority}))},d.addError=function(e,r){(e=e.length?e[0]:e).pristine.errors.push(r),c(e.pristine)},d.reset=function(){for(var e in d.fields)d.fields[e].errorElements=null;Array.from(d.form.querySelectorAll("."+s)).map(function(e){e.parentNode.removeChild(e)}),Array.from(d.form.querySelectorAll("."+d.config.classTo)).map(function(e){e.classList.remove(d.config.successClass),e.classList.remove(d.config.errorClass)})},d.destroy=function(){d.reset(),d.fields.forEach(function(e){delete e.input.pristine}),d.fields=[]},d.setGlobalConfig=function(e){i=e},d}var n={required:"This field is required",email:"This field requires a valid e-mail address",number:"This field requires a number",url:"This field requires a valid website URL",tel:"This field requires a valid telephone number",maxlength:"This fields length must be < ${1}",minlength:"This fields length must be > ${1}",min:"Minimum value for this field is ${1}",max:"Maximum value for this field is ${1}",pattern:"Input must match the pattern ${1}"},i={classTo:"form-group",errorClass:"has-danger",successClass:"has-success",errorTextParent:"form-group",errorTextTag:"div",errorTextClass:"text-help"},s="pristine-error",a="input:not([type^=hidden]):not([type^=submit]), select, textarea",o=["required","min","max","minlength","maxlength","pattern"],l={},u=function(e,r){r.name=e,r.msg||(r.msg=n[e]),void 0===r.priority&&(r.priority=1),l[e]=r};return u("text",{fn:function(e){return!0},priority:0}),u("required",{fn:function(e){return"radio"===this.type||"checkbox"===this.type?r(this):void 0!==e&&""!==e},priority:99,halt:!0}),u("email",{fn:function(e){return!e||/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e)}}),u("number",{fn:function(e){return!e||!isNaN(parseFloat(e))},priority:2}),u("integer",{fn:function(e){return e&&/^\d+$/.test(e)}}),u("minlength",{fn:function(e,r){return!e||e.length>=parseInt(r)}}),u("maxlength",{fn:function(e,r){return!e||e.length<=parseInt(r)}}),u("min",{fn:function(e,t){return!e||("checkbox"===this.type?r(this)>=parseInt(t):parseFloat(e)>=parseFloat(t))}}),u("max",{fn:function(e,t){return!e||("checkbox"===this.type?r(this)<=parseInt(t):parseFloat(e)<=parseFloat(t))}}),u("pattern",{fn:function(e,r){var t=r.match(new RegExp("^/(.*?)/([gimy]*)$"));return!e||new RegExp(t[1],t[2]).test(e)}}),t}); |
{ | ||
"name": "pristinejs", | ||
"version": "0.1.1", | ||
"version": "0.1.2", | ||
"description": "A tiny vanilla javascript form validation library", | ||
@@ -5,0 +5,0 @@ "main": "dist/pristine.js", |
@@ -5,7 +5,4 @@ # Pristine - Vanilla javascript form validation library | ||
## Install | ||
**~4kb minified, ~2kb gzipped, no dependencies** | ||
```sh | ||
$ npm install pristinejs --save | ||
``` | ||
@@ -43,4 +40,13 @@ ## Usage | ||
It automatically validates `required, min, max, minlength, maxlength` and type attributes like `email, number` amd more | ||
It automatically validates `required, min, max, minlength, maxlength` attributes and the value of type attributes | ||
like `email, number` and more.. | ||
## Install | ||
```sh | ||
$ npm install pristinejs --save | ||
``` | ||
`Pristine` takes `3` parameters | ||
@@ -50,3 +56,3 @@ | ||
- **config** A object containing the configuration. Default is bootstrap's configuration which is | ||
- **config** An object containing the configuration. Default is bootstrap's configuration which is | ||
@@ -68,3 +74,3 @@ ```javascript | ||
- **online** boolean value indicating whether pristine should validate as you type, default is `true` | ||
- **live** A boolean value indicating whether pristine should validate as you type, default is `true` | ||
@@ -101,3 +107,3 @@ | ||
}, "The value must be between {$0} and {$1}", 5, false); | ||
}, "The value (${0}) must be between ${1} and ${2}", 5, false); | ||
``` | ||
@@ -109,2 +115,5 @@ | ||
<input type="text" class="form-control" data-pristine-my-range="10,30" /> | ||
``` | ||
``` | ||
> The goal of this library is not to provide every possible type of validation and thus becoming a bloat. | ||
> The goal is to provide most common types of validations and a neat way to add custom validators. |
@@ -11,2 +11,3 @@ export const lang = { | ||
max: "Maximum value for this field is ${1}", | ||
pattern: "Input must match the pattern ${1}", | ||
}; |
import { lang } from './lang'; | ||
import { tmpl, findAncestor, groupedElemCount } from './utils'; | ||
@@ -14,3 +15,3 @@ let defaultConfig = { | ||
const SELECTOR = "input:not([type^=hidden]):not([type^=submit]), select, textarea"; | ||
const ALLOWED_ATTRIBUTES = ["required", "min", "max", 'minlength', 'maxlength']; | ||
const ALLOWED_ATTRIBUTES = ["required", "min", "max", 'minlength', 'maxlength', 'pattern']; | ||
@@ -28,31 +29,24 @@ const validators = {}; | ||
_('text', {fn: (val) => true, priority: 0}); | ||
_('required', {fn: (val) => val !== '', priority: 99, halt: true}); | ||
_('email', {fn: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val), priority: 1}); | ||
_('number', {fn: (val) => parseFloat(val), priority: 1}); | ||
_('minlength', {fn: (val, length) => console.log(val, length) || val && val.length >= parseInt(length), priority: 1}); | ||
_('maxlength', {fn: (val, length) => val && val.length <= parseInt(length), priority: 1}); | ||
_('min', {fn: (val, limit) => parseFloat(val) >= parseFloat(limit), priority: 1}); | ||
_('max', {fn: (val, limit) => parseFloat(val) <= parseFloat(limit), priority: 1}); | ||
_('text', { fn: (val) => true, priority: 0}); | ||
_('required', { fn: function(val){ return (this.type === 'radio' || this.type === 'checkbox') ? groupedElemCount(this) : val !== undefined && val !== ''}, priority: 99, halt: true}); | ||
_('email', { fn: (val) => !val || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)}); | ||
_('number', { fn: (val) => !val || !isNaN(parseFloat(val)), priority: 2 }); | ||
_('integer', { fn: (val) => val && /^\d+$/.test(val) }); | ||
_('minlength', { fn: (val, length) => !val || val.length >= parseInt(length) }); | ||
_('maxlength', { fn: (val, length) => !val || val.length <= parseInt(length) }); | ||
_('min', { fn: function(val, limit){ return !val || (this.type === 'checkbox' ? groupedElemCount(this) >= parseInt(limit) : parseFloat(val) >= parseFloat(limit)); } }); | ||
_('max', { fn: function(val, limit){ return !val || (this.type === 'checkbox' ? groupedElemCount(this) <= parseInt(limit) : parseFloat(val) <= parseFloat(limit)); } }); | ||
_('pattern', { fn: (val, pattern) => { let m = pattern.match(new RegExp('^/(.*?)/([gimy]*)$')); return !val || (new RegExp(m[1], m[2])).test(val);} }); | ||
function findAncestor (el, cls) { | ||
while ((el = el.parentElement) && !el.classList.contains(cls)); | ||
return el; | ||
} | ||
function tmpl(o) { | ||
return this.replace(/\${([^{}]*)}/g, (a, b) => arguments[b]); | ||
} | ||
export default function Pristine(form, config, online){ | ||
export default function Pristine(form, config, live){ | ||
let self = this; | ||
init(form, config, online); | ||
init(form, config, live); | ||
function init(form, config, online){ | ||
function init(form, config, live){ | ||
self.form = form; | ||
self.config = config || defaultConfig; | ||
self.online = !(online === false); | ||
self.live = !(live === false); | ||
self.fields = Array.from(form.querySelectorAll(SELECTOR)).map(function (input) { | ||
@@ -62,30 +56,27 @@ | ||
let params = {}; | ||
let messages = {}; | ||
ALLOWED_ATTRIBUTES.forEach(function (item) { | ||
let val = input.getAttribute(item); | ||
if (val !== null){ | ||
_addValidatorToField(fns, params, item, val); | ||
} | ||
}); | ||
[].forEach.call(input.attributes, function (attr) { | ||
if (/^data-pristine-/.test(attr.name)) { | ||
let name = attr.name.substr(14); | ||
if (name.endsWith('-message')){ | ||
messages[name.slice(0, name.length-8)] = attr.value; | ||
return; | ||
} | ||
if (name === 'type') name = attr.value; | ||
_addValidatorToField(fns, params, name, attr.value); | ||
} else if (~ALLOWED_ATTRIBUTES.indexOf(attr.name)){ | ||
_addValidatorToField(fns, params, attr.name, attr.value); | ||
} else if (attr.name === 'type'){ | ||
_addValidatorToField(fns, params, attr.value); | ||
} | ||
}); | ||
_addValidatorToField(fns, params, input.getAttribute('type')); | ||
fns.sort( (a, b) => b.priority - a.priority); | ||
fns.sort(function (a, b) { | ||
return b.priority - a.priority; | ||
}); | ||
self.online && input.addEventListener((!~['radio', 'checkbox'].indexOf(input.getAttribute('type')) ? 'input':'change'), function(e) { | ||
self.live && input.addEventListener((!~['radio', 'checkbox'].indexOf(input.getAttribute('type')) ? 'input':'change'), function(e) { | ||
self.validate(e.target); | ||
}.bind(self)); | ||
input.pristine = {input, validators: fns, params}; | ||
return input.pristine; | ||
return input.pristine = {input, validators: fns, params, messages, self}; | ||
@@ -114,3 +105,3 @@ }.bind(self)); | ||
valid = false; | ||
!silent && _showError(field, field.messages); | ||
!silent && _showError(field); | ||
} | ||
@@ -121,8 +112,8 @@ } | ||
self.getErrorMessages = function(input) { | ||
return input.length ? input[0].pristine.messages : input.pristine.messages; | ||
self.getErrors = function(input) { | ||
return input.length ? input[0].pristine.errors : input.pristine.errors; | ||
}; | ||
function _validateField(field){ | ||
let messages = []; | ||
let errors = []; | ||
let valid = true; | ||
@@ -135,3 +126,4 @@ for(let i in field.validators){ | ||
valid = false; | ||
messages.push(tmpl.apply(validator.msg, params)); | ||
let error = field.messages[validator.name] || validator.msg; | ||
errors.push(tmpl.apply(error, params)); | ||
if (validator.halt === true){ | ||
@@ -142,3 +134,3 @@ break; | ||
} | ||
field.messages = messages; | ||
field.errors = errors; | ||
return valid; | ||
@@ -151,32 +143,63 @@ } | ||
} else if (elemOrName instanceof HTMLElement){ | ||
//TODO check if pristine field | ||
elemOrName.pristine.validators.push({fn, msg, priority, halt}); | ||
elemOrName.pristine.validators.sort(function (a, b) { | ||
return b.priority - a.priority; | ||
}); | ||
elemOrName.pristine.validators.sort( (a, b) => b.priority - a.priority); | ||
} | ||
}; | ||
function _showError(field, messages){ | ||
let ret = _removeError(field); | ||
let errorClassElement = ret[0], errorTextParent = ret[1]; | ||
errorClassElement && errorClassElement.classList.add(self.config.errorClass); | ||
function _getErrorElements(field) { | ||
if (field.errorElements){ | ||
return field.errorElements; | ||
} | ||
let errorClassElement = findAncestor(field.input, self.config.classTo); | ||
let errorTextParent = null, errorTextElement = null; | ||
if (self.config.classTo === self.config.errorTextParent){ | ||
errorTextParent = errorClassElement; | ||
} else { | ||
errorTextParent = errorClassElement.querySelector(self.errorTextParent); | ||
} | ||
if (errorTextParent){ | ||
errorTextElement = errorTextParent.querySelector('.' + PRISTINE_ERROR); | ||
if (!errorTextElement){ | ||
errorTextElement = document.createElement(self.config.errorTextTag); | ||
errorTextElement.className = PRISTINE_ERROR + ' ' + self.config.errorTextClass; | ||
errorTextParent.appendChild(errorTextElement); | ||
errorTextElement.pristineDisplay = errorTextElement.style.display; | ||
} | ||
} | ||
return field.errorElements = [errorClassElement, errorTextElement] | ||
} | ||
let elem = document.createElement(self.config.errorTextTag); | ||
elem.className = PRISTINE_ERROR + ' ' + self.config.errorTextClass; | ||
elem.innerHTML = messages.join('<br/>'); | ||
errorTextParent && errorTextParent.appendChild(elem); | ||
function _showError(field){ | ||
let errorElements = _getErrorElements(field); | ||
let errorClassElement = errorElements[0], errorTextElement = errorElements[1]; | ||
if(errorClassElement){ | ||
errorClassElement.classList.remove(self.config.successClass); | ||
errorClassElement.classList.add(self.config.errorClass); | ||
} | ||
if (errorTextElement){ | ||
errorTextElement.innerHTML = field.errors.join('<br/>'); | ||
errorTextElement.style.display = errorTextElement.pristineDisplay || ''; | ||
} | ||
} | ||
self.addError = function(input, error) { | ||
input = input.length ? input[0] : input; | ||
input.pristine.errors.push(error); | ||
_showError(input.pristine); | ||
}; | ||
function _removeError(field){ | ||
let errorClassElement = findAncestor(field.input, self.config.classTo); | ||
errorClassElement && errorClassElement.classList.remove(self.config.errorClass, self.config.successClass); | ||
let errorTextParent = findAncestor(field.input, self.config.errorTextParent); | ||
var existing = errorTextParent ? errorTextParent.querySelector('.' + PRISTINE_ERROR) : null; | ||
if (existing){ | ||
existing.parentNode.removeChild(existing); | ||
let errorElements = _getErrorElements(field); | ||
let errorClassElement = errorElements[0], errorTextElement = errorElements[1]; | ||
if (errorClassElement){ | ||
// IE > 9 doesn't support multiple class removal | ||
errorClassElement.classList.remove(self.config.errorClass); | ||
errorClassElement.classList.remove(self.config.successClass); | ||
} | ||
return [errorClassElement, errorTextParent] | ||
if (errorTextElement){ | ||
errorTextElement.innerHTML = ''; | ||
errorTextElement.style.display = 'none'; | ||
} | ||
return errorElements; | ||
} | ||
@@ -190,2 +213,5 @@ | ||
self.reset = function () { | ||
for(var i in self.fields){ | ||
self.fields[i].errorElements = null; | ||
} | ||
Array.from(self.form.querySelectorAll('.' + PRISTINE_ERROR)).map(function (elem) { | ||
@@ -195,3 +221,4 @@ elem.parentNode.removeChild(elem); | ||
Array.from(self.form.querySelectorAll('.' + self.config.classTo)).map(function (elem) { | ||
elem.classList.remove(self.config.successClass, self.config.errorClass); | ||
elem.classList.remove(self.config.successClass); | ||
elem.classList.remove(self.config.errorClass); | ||
}); | ||
@@ -198,0 +225,0 @@ |
31131
23.35%9
12.5%545
21.11%114
9.62%