1
0
mirror of synced 2024-11-29 00:26:12 +03:00

Adds initial validation rules and support

This commit is contained in:
Justin Schroeder 2019-11-07 17:03:34 -05:00
parent a53edaa342
commit e952e46aad
14 changed files with 1579 additions and 69 deletions

View File

@ -9,10 +9,11 @@ export default {
exports: 'named', exports: 'named',
globals: { globals: {
'is-plain-object': 'isPlainObject', 'is-plain-object': 'isPlainObject',
'nanoid': 'nanoid' 'nanoid': 'nanoid',
'is-url': 'isUrl'
} }
}, },
external: ['is-plain-object', 'nanoid'], external: ['is-plain-object', 'nanoid', 'is-url'],
plugins: [ plugins: [
commonjs(), commonjs(),
vue({ vue({

340
dist/formulate.esm.js vendored
View File

@ -1,3 +1,4 @@
import isUrl from 'is-url';
import isPlainObject from 'is-plain-object'; import isPlainObject from 'is-plain-object';
import nanoid from 'nanoid'; import nanoid from 'nanoid';
@ -160,6 +161,291 @@ function arrayify (item) {
return [] return []
} }
/**
* Given an array or string return an array of callables.
* @param {array|string} validation
* @param {array} rules and array of functions
* @return {array} an array of functions
*/
function parseRules (validation, rules) {
if (typeof validation === 'string') {
return parseRules(validation.split('|'), rules)
}
if (!Array.isArray(validation)) {
return []
}
return validation.map(function (rule) { return parseRule(rule, rules); }).filter(function (f) { return !!f; })
}
/**
* Given a string or function, parse it and return the an array in the format
* [fn, [...arguments]]
* @param {string|function} rule
*/
function parseRule (rule, rules) {
if (typeof rule === 'function') {
return [rule, []]
}
if (Array.isArray(rule) && rule.length) {
if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) {
return [rules[rule.shift()], rule]
}
if (typeof rule[0] === 'function') {
return [rule.shift(), rule]
}
}
if (typeof rule === 'string') {
var segments = rule.split(':');
var functionName = segments.shift();
if (rules.hasOwnProperty(functionName)) {
return [rules[functionName], segments.length ? segments.join(':').split(',') : []]
} else {
throw new Error(("Unknown validation rule " + rule))
}
}
return false
}
/**
* Library of rules
*/
var rules = {
/**
* Rule: the value must be "yes", "on", "1", or true
*/
accepted: function (value) {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
},
/**
* Rule: must be a value
*/
required: function (value, isRequired) {
if ( isRequired === void 0 ) isRequired = true;
return Promise.resolve((function () {
if (!isRequired || ['no', 'false'].includes(isRequired)) {
return true
}
if (Array.isArray(value)) {
return !!value.length
}
if (typeof value === 'string') {
return !!value
}
if (typeof value === 'object') {
return (!value) ? false : !!Object.keys(value).length
}
return true
})())
},
/**
* Rule: Value is in an array (stack).
*/
in: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) !== undefined)
},
/**
* Rule: Value is not in stack.
*/
not: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) === undefined)
},
/**
* Rule: Match the value against a (stack) of patterns or strings
*/
matches: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(!!stack.find(function (pattern) {
if (pattern instanceof RegExp) {
return pattern.test(value)
}
return pattern === value
}))
},
/**
* Rule: checks if a string is a valid url
*/
url: function (value) {
return Promise.resolve(isUrl(value))
},
/**
* Rule: ensures the value is a date according to Date.parse()
*/
date: function (value) {
return Promise.resolve(!isNaN(Date.parse(value)))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
after: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
before: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
},
/**
* Rule: checks if the value is only alpha numeric
*/
alpha: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-z][A-Z]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is only alpha numeric
*/
number: function (value) {
return Promise.resolve(!isNaN(value))
},
/**
* Rule: checks if the value is alpha numeric
*/
alphanumeric: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is between two other values
*/
between: function (value, from, to) {
return Promise.resolve((function () {
if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false
}
from = Number(from);
to = Number(to);
if (!isNaN(value)) {
value = Number(value);
return (value > from && value < to)
}
if (typeof value === 'string') {
return value.length > from && value.length < to
}
return false
})())
},
/**
* Rule: tests
*/
email: function (value) {
// eslint-disable-next-line
var isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
return Promise.resolve(isEmail.test(value))
},
/**
* Check the file type is correct.
*/
mime: function (files) {
var types = [], len = arguments.length - 1;
while ( len-- > 0 ) types[ len ] = arguments[ len + 1 ];
return Promise.resolve((function () {
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false
}
}
}
return true
})())
},
/**
* Check the minimum value of a particular.
*/
min: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value >= minimum
}
if (typeof value === 'string') {
return value.length >= minimum
}
if (Array.isArray(value)) {
return value.length >= minimum
}
return false
})())
},
/**
* Check the minimum value of a particular.
*/
max: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value <= minimum
}
if (typeof value === 'string') {
return value.length <= minimum
}
if (Array.isArray(value)) {
return value.length <= minimum
}
return false
})())
}
};
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
* render that element. * render that element.
@ -186,7 +472,8 @@ var context = {
elementAttributes: elementAttributes, elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition, logicalLabelPosition: logicalLabelPosition,
isVmodeled: isVmodeled, isVmodeled: isVmodeled,
mergedErrors: mergedErrors mergedErrors: mergedErrors,
hasErrors: hasErrors
}; };
/** /**
@ -299,6 +586,13 @@ function mergedErrors () {
.reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, []) .reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, [])
} }
/**
* Does this computed property have errors.
*/
function hasErrors () {
return !!this.mergedErrors.length
}
/** /**
* Defines the model used throughout the existing context. * Defines the model used throughout the existing context.
* @param {object} context * @param {object} context
@ -397,6 +691,17 @@ var script = {
type: [String, Array, Boolean], type: [String, Array, Boolean],
default: false default: false
}, },
validation: {
type: [String, Boolean, Array],
default: false
},
validationBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live'].includes(value)
}
},
error: { error: {
type: [String, Boolean], type: [String, Boolean],
default: false default: false
@ -406,7 +711,8 @@ var script = {
return { return {
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue internalModelProxy: this.formulateValue,
validationErrors: []
} }
}, },
computed: Object.assign({}, context, computed: Object.assign({}, context,
@ -425,6 +731,7 @@ var script = {
deep: true deep: true
}, },
internalModelProxy: function internalModelProxy (newValue, oldValue) { internalModelProxy: function internalModelProxy (newValue, oldValue) {
this.performValidation();
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) { if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue; this.context.model = newValue;
} }
@ -440,12 +747,29 @@ var script = {
this.formulateFormRegister(this.nameOrFallback, this); this.formulateFormRegister(this.nameOrFallback, this);
} }
this.updateLocalAttributes(this.$attrs); this.updateLocalAttributes(this.$attrs);
this.performValidation();
}, },
methods: { methods: {
updateLocalAttributes: function updateLocalAttributes (value) { updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value; this.localAttributes = value;
} }
},
performValidation: function performValidation () {
var this$1 = this;
var rules = parseRules(this.validation, this.$formulate.rules());
Promise.all(
rules.map(function (ref) {
var rule = ref[0];
var args = ref[1];
return rule.apply(void 0, [ this$1.context.model ].concat( args ))
.then(function (res) { return res ? false : 'Validation error!'; })
})
)
.then(function (result) { return result.filter(function (result) { return result; }); })
.then(function (errorMessages) { this$1.validationErrors = errorMessages; });
} }
} }
}; };
@ -539,6 +863,7 @@ var __vue_render__ = function() {
staticClass: "formulate-input", staticClass: "formulate-input",
attrs: { attrs: {
"data-classification": _vm.classification, "data-classification": _vm.classification,
"data-has-errors": _vm.hasErrors,
"data-type": _vm.type "data-type": _vm.type
} }
}, },
@ -1601,7 +1926,8 @@ var Formulate = function Formulate () {
FormulateInputSelect: FormulateInputSelect, FormulateInputSelect: FormulateInputSelect,
FormulateInputTextArea: FormulateInputTextArea FormulateInputTextArea: FormulateInputTextArea
}, },
library: library library: library,
rules: rules
}; };
}; };
@ -1663,6 +1989,14 @@ Formulate.prototype.component = function component (type) {
return false return false
}; };
/**
* Get validation rules.
* @return {object} object of validation functions
*/
Formulate.prototype.rules = function rules () {
return this.options.rules
};
var Formulate$1 = new Formulate(); var Formulate$1 = new Formulate();
export default Formulate$1; export default Formulate$1;

344
dist/formulate.min.js vendored
View File

@ -1,6 +1,7 @@
var Formulate = (function (exports, isPlainObject, nanoid) { var Formulate = (function (exports, isUrl, isPlainObject, nanoid) {
'use strict'; 'use strict';
isUrl = isUrl && isUrl.hasOwnProperty('default') ? isUrl['default'] : isUrl;
isPlainObject = isPlainObject && isPlainObject.hasOwnProperty('default') ? isPlainObject['default'] : isPlainObject; isPlainObject = isPlainObject && isPlainObject.hasOwnProperty('default') ? isPlainObject['default'] : isPlainObject;
nanoid = nanoid && nanoid.hasOwnProperty('default') ? nanoid['default'] : nanoid; nanoid = nanoid && nanoid.hasOwnProperty('default') ? nanoid['default'] : nanoid;
@ -163,6 +164,291 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
return [] return []
} }
/**
* Given an array or string return an array of callables.
* @param {array|string} validation
* @param {array} rules and array of functions
* @return {array} an array of functions
*/
function parseRules (validation, rules) {
if (typeof validation === 'string') {
return parseRules(validation.split('|'), rules)
}
if (!Array.isArray(validation)) {
return []
}
return validation.map(function (rule) { return parseRule(rule, rules); }).filter(function (f) { return !!f; })
}
/**
* Given a string or function, parse it and return the an array in the format
* [fn, [...arguments]]
* @param {string|function} rule
*/
function parseRule (rule, rules) {
if (typeof rule === 'function') {
return [rule, []]
}
if (Array.isArray(rule) && rule.length) {
if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) {
return [rules[rule.shift()], rule]
}
if (typeof rule[0] === 'function') {
return [rule.shift(), rule]
}
}
if (typeof rule === 'string') {
var segments = rule.split(':');
var functionName = segments.shift();
if (rules.hasOwnProperty(functionName)) {
return [rules[functionName], segments.length ? segments.join(':').split(',') : []]
} else {
throw new Error(("Unknown validation rule " + rule))
}
}
return false
}
/**
* Library of rules
*/
var rules = {
/**
* Rule: the value must be "yes", "on", "1", or true
*/
accepted: function (value) {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
},
/**
* Rule: must be a value
*/
required: function (value, isRequired) {
if ( isRequired === void 0 ) isRequired = true;
return Promise.resolve((function () {
if (!isRequired || ['no', 'false'].includes(isRequired)) {
return true
}
if (Array.isArray(value)) {
return !!value.length
}
if (typeof value === 'string') {
return !!value
}
if (typeof value === 'object') {
return (!value) ? false : !!Object.keys(value).length
}
return true
})())
},
/**
* Rule: Value is in an array (stack).
*/
in: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) !== undefined)
},
/**
* Rule: Value is not in stack.
*/
not: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) === undefined)
},
/**
* Rule: Match the value against a (stack) of patterns or strings
*/
matches: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(!!stack.find(function (pattern) {
if (pattern instanceof RegExp) {
return pattern.test(value)
}
return pattern === value
}))
},
/**
* Rule: checks if a string is a valid url
*/
url: function (value) {
return Promise.resolve(isUrl(value))
},
/**
* Rule: ensures the value is a date according to Date.parse()
*/
date: function (value) {
return Promise.resolve(!isNaN(Date.parse(value)))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
after: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
before: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
},
/**
* Rule: checks if the value is only alpha numeric
*/
alpha: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-z][A-Z]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is only alpha numeric
*/
number: function (value) {
return Promise.resolve(!isNaN(value))
},
/**
* Rule: checks if the value is alpha numeric
*/
alphanumeric: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is between two other values
*/
between: function (value, from, to) {
return Promise.resolve((function () {
if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false
}
from = Number(from);
to = Number(to);
if (!isNaN(value)) {
value = Number(value);
return (value > from && value < to)
}
if (typeof value === 'string') {
return value.length > from && value.length < to
}
return false
})())
},
/**
* Rule: tests
*/
email: function (value) {
// eslint-disable-next-line
var isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
return Promise.resolve(isEmail.test(value))
},
/**
* Check the file type is correct.
*/
mime: function (files) {
var types = [], len = arguments.length - 1;
while ( len-- > 0 ) types[ len ] = arguments[ len + 1 ];
return Promise.resolve((function () {
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false
}
}
}
return true
})())
},
/**
* Check the minimum value of a particular.
*/
min: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value >= minimum
}
if (typeof value === 'string') {
return value.length >= minimum
}
if (Array.isArray(value)) {
return value.length >= minimum
}
return false
})())
},
/**
* Check the minimum value of a particular.
*/
max: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value <= minimum
}
if (typeof value === 'string') {
return value.length <= minimum
}
if (Array.isArray(value)) {
return value.length <= minimum
}
return false
})())
}
};
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
* render that element. * render that element.
@ -189,7 +475,8 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
elementAttributes: elementAttributes, elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition, logicalLabelPosition: logicalLabelPosition,
isVmodeled: isVmodeled, isVmodeled: isVmodeled,
mergedErrors: mergedErrors mergedErrors: mergedErrors,
hasErrors: hasErrors
}; };
/** /**
@ -302,6 +589,13 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
.reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, []) .reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, [])
} }
/**
* Does this computed property have errors.
*/
function hasErrors () {
return !!this.mergedErrors.length
}
/** /**
* Defines the model used throughout the existing context. * Defines the model used throughout the existing context.
* @param {object} context * @param {object} context
@ -400,6 +694,17 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
type: [String, Array, Boolean], type: [String, Array, Boolean],
default: false default: false
}, },
validation: {
type: [String, Boolean, Array],
default: false
},
validationBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live'].includes(value)
}
},
error: { error: {
type: [String, Boolean], type: [String, Boolean],
default: false default: false
@ -409,7 +714,8 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
return { return {
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue internalModelProxy: this.formulateValue,
validationErrors: []
} }
}, },
computed: Object.assign({}, context, computed: Object.assign({}, context,
@ -428,6 +734,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
deep: true deep: true
}, },
internalModelProxy: function internalModelProxy (newValue, oldValue) { internalModelProxy: function internalModelProxy (newValue, oldValue) {
this.performValidation();
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) { if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue; this.context.model = newValue;
} }
@ -443,12 +750,29 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
this.formulateFormRegister(this.nameOrFallback, this); this.formulateFormRegister(this.nameOrFallback, this);
} }
this.updateLocalAttributes(this.$attrs); this.updateLocalAttributes(this.$attrs);
this.performValidation();
}, },
methods: { methods: {
updateLocalAttributes: function updateLocalAttributes (value) { updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value; this.localAttributes = value;
} }
},
performValidation: function performValidation () {
var this$1 = this;
var rules = parseRules(this.validation, this.$formulate.rules());
Promise.all(
rules.map(function (ref) {
var rule = ref[0];
var args = ref[1];
return rule.apply(void 0, [ this$1.context.model ].concat( args ))
.then(function (res) { return res ? false : 'Validation error!'; })
})
)
.then(function (result) { return result.filter(function (result) { return result; }); })
.then(function (errorMessages) { this$1.validationErrors = errorMessages; });
} }
} }
}; };
@ -542,6 +866,7 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
staticClass: "formulate-input", staticClass: "formulate-input",
attrs: { attrs: {
"data-classification": _vm.classification, "data-classification": _vm.classification,
"data-has-errors": _vm.hasErrors,
"data-type": _vm.type "data-type": _vm.type
} }
}, },
@ -1604,7 +1929,8 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
FormulateInputSelect: FormulateInputSelect, FormulateInputSelect: FormulateInputSelect,
FormulateInputTextArea: FormulateInputTextArea FormulateInputTextArea: FormulateInputTextArea
}, },
library: library library: library,
rules: rules
}; };
}; };
@ -1666,10 +1992,18 @@ var Formulate = (function (exports, isPlainObject, nanoid) {
return false return false
}; };
/**
* Get validation rules.
* @return {object} object of validation functions
*/
Formulate.prototype.rules = function rules () {
return this.options.rules
};
var Formulate$1 = new Formulate(); var Formulate$1 = new Formulate();
exports.default = Formulate$1; exports.default = Formulate$1;
return exports; return exports;
}({}, isPlainObject, nanoid)); }({}, isUrl, isPlainObject, nanoid));

348
dist/formulate.umd.js vendored
View File

@ -1,9 +1,10 @@
(function (global, factory) { (function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('is-plain-object'), require('nanoid')) : typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('is-url'), require('is-plain-object'), require('nanoid')) :
typeof define === 'function' && define.amd ? define(['exports', 'is-plain-object', 'nanoid'], factory) : typeof define === 'function' && define.amd ? define(['exports', 'is-url', 'is-plain-object', 'nanoid'], factory) :
(global = global || self, factory(global.Formulate = {}, global.isPlainObject, global.nanoid)); (global = global || self, factory(global.Formulate = {}, global.isUrl, global.isPlainObject, global.nanoid));
}(this, (function (exports, isPlainObject, nanoid) { 'use strict'; }(this, (function (exports, isUrl, isPlainObject, nanoid) { 'use strict';
isUrl = isUrl && isUrl.hasOwnProperty('default') ? isUrl['default'] : isUrl;
isPlainObject = isPlainObject && isPlainObject.hasOwnProperty('default') ? isPlainObject['default'] : isPlainObject; isPlainObject = isPlainObject && isPlainObject.hasOwnProperty('default') ? isPlainObject['default'] : isPlainObject;
nanoid = nanoid && nanoid.hasOwnProperty('default') ? nanoid['default'] : nanoid; nanoid = nanoid && nanoid.hasOwnProperty('default') ? nanoid['default'] : nanoid;
@ -166,6 +167,291 @@
return [] return []
} }
/**
* Given an array or string return an array of callables.
* @param {array|string} validation
* @param {array} rules and array of functions
* @return {array} an array of functions
*/
function parseRules (validation, rules) {
if (typeof validation === 'string') {
return parseRules(validation.split('|'), rules)
}
if (!Array.isArray(validation)) {
return []
}
return validation.map(function (rule) { return parseRule(rule, rules); }).filter(function (f) { return !!f; })
}
/**
* Given a string or function, parse it and return the an array in the format
* [fn, [...arguments]]
* @param {string|function} rule
*/
function parseRule (rule, rules) {
if (typeof rule === 'function') {
return [rule, []]
}
if (Array.isArray(rule) && rule.length) {
if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) {
return [rules[rule.shift()], rule]
}
if (typeof rule[0] === 'function') {
return [rule.shift(), rule]
}
}
if (typeof rule === 'string') {
var segments = rule.split(':');
var functionName = segments.shift();
if (rules.hasOwnProperty(functionName)) {
return [rules[functionName], segments.length ? segments.join(':').split(',') : []]
} else {
throw new Error(("Unknown validation rule " + rule))
}
}
return false
}
/**
* Library of rules
*/
var rules = {
/**
* Rule: the value must be "yes", "on", "1", or true
*/
accepted: function (value) {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
},
/**
* Rule: must be a value
*/
required: function (value, isRequired) {
if ( isRequired === void 0 ) isRequired = true;
return Promise.resolve((function () {
if (!isRequired || ['no', 'false'].includes(isRequired)) {
return true
}
if (Array.isArray(value)) {
return !!value.length
}
if (typeof value === 'string') {
return !!value
}
if (typeof value === 'object') {
return (!value) ? false : !!Object.keys(value).length
}
return true
})())
},
/**
* Rule: Value is in an array (stack).
*/
in: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) !== undefined)
},
/**
* Rule: Value is not in stack.
*/
not: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(stack.find(function (item) {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) === undefined)
},
/**
* Rule: Match the value against a (stack) of patterns or strings
*/
matches: function (value) {
var stack = [], len = arguments.length - 1;
while ( len-- > 0 ) stack[ len ] = arguments[ len + 1 ];
return Promise.resolve(!!stack.find(function (pattern) {
if (pattern instanceof RegExp) {
return pattern.test(value)
}
return pattern === value
}))
},
/**
* Rule: checks if a string is a valid url
*/
url: function (value) {
return Promise.resolve(isUrl(value))
},
/**
* Rule: ensures the value is a date according to Date.parse()
*/
date: function (value) {
return Promise.resolve(!isNaN(Date.parse(value)))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
after: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
before: function (value, compare) {
if ( compare === void 0 ) compare = false;
var timestamp = Date.parse(compare || new Date());
var fieldValue = Date.parse(value);
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
},
/**
* Rule: checks if the value is only alpha numeric
*/
alpha: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-z][A-Z]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is only alpha numeric
*/
number: function (value) {
return Promise.resolve(!isNaN(value))
},
/**
* Rule: checks if the value is alpha numeric
*/
alphanumeric: function (value, set) {
if ( set === void 0 ) set = 'default';
var sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]$/
};
var selectedSet = sets.hasOwnProperty(set) ? set : 'default';
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is between two other values
*/
between: function (value, from, to) {
return Promise.resolve((function () {
if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false
}
from = Number(from);
to = Number(to);
if (!isNaN(value)) {
value = Number(value);
return (value > from && value < to)
}
if (typeof value === 'string') {
return value.length > from && value.length < to
}
return false
})())
},
/**
* Rule: tests
*/
email: function (value) {
// eslint-disable-next-line
var isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
return Promise.resolve(isEmail.test(value))
},
/**
* Check the file type is correct.
*/
mime: function (files) {
var types = [], len = arguments.length - 1;
while ( len-- > 0 ) types[ len ] = arguments[ len + 1 ];
return Promise.resolve((function () {
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (var i in files) {
if (!types.includes(files[i].type)) {
return false
}
}
}
return true
})())
},
/**
* Check the minimum value of a particular.
*/
min: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value >= minimum
}
if (typeof value === 'string') {
return value.length >= minimum
}
if (Array.isArray(value)) {
return value.length >= minimum
}
return false
})())
},
/**
* Check the minimum value of a particular.
*/
max: function (value, minimum) {
return Promise.resolve((function () {
minimum = Number(minimum);
if (!isNaN(value)) {
value = Number(value);
return value <= minimum
}
if (typeof value === 'string') {
return value.length <= minimum
}
if (Array.isArray(value)) {
return value.length <= minimum
}
return false
})())
}
};
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
* render that element. * render that element.
@ -192,7 +478,8 @@
elementAttributes: elementAttributes, elementAttributes: elementAttributes,
logicalLabelPosition: logicalLabelPosition, logicalLabelPosition: logicalLabelPosition,
isVmodeled: isVmodeled, isVmodeled: isVmodeled,
mergedErrors: mergedErrors mergedErrors: mergedErrors,
hasErrors: hasErrors
}; };
/** /**
@ -305,6 +592,13 @@
.reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, []) .reduce(function (errors, err) { return !errors.includes(err) ? errors.concat(err) : errors; }, [])
} }
/**
* Does this computed property have errors.
*/
function hasErrors () {
return !!this.mergedErrors.length
}
/** /**
* Defines the model used throughout the existing context. * Defines the model used throughout the existing context.
* @param {object} context * @param {object} context
@ -403,6 +697,17 @@
type: [String, Array, Boolean], type: [String, Array, Boolean],
default: false default: false
}, },
validation: {
type: [String, Boolean, Array],
default: false
},
validationBehavior: {
type: String,
default: 'blur',
validator: function (value) {
return ['blur', 'live'].includes(value)
}
},
error: { error: {
type: [String, Boolean], type: [String, Boolean],
default: false default: false
@ -412,7 +717,8 @@
return { return {
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue internalModelProxy: this.formulateValue,
validationErrors: []
} }
}, },
computed: Object.assign({}, context, computed: Object.assign({}, context,
@ -431,6 +737,7 @@
deep: true deep: true
}, },
internalModelProxy: function internalModelProxy (newValue, oldValue) { internalModelProxy: function internalModelProxy (newValue, oldValue) {
this.performValidation();
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) { if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue; this.context.model = newValue;
} }
@ -446,12 +753,29 @@
this.formulateFormRegister(this.nameOrFallback, this); this.formulateFormRegister(this.nameOrFallback, this);
} }
this.updateLocalAttributes(this.$attrs); this.updateLocalAttributes(this.$attrs);
this.performValidation();
}, },
methods: { methods: {
updateLocalAttributes: function updateLocalAttributes (value) { updateLocalAttributes: function updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value; this.localAttributes = value;
} }
},
performValidation: function performValidation () {
var this$1 = this;
var rules = parseRules(this.validation, this.$formulate.rules());
Promise.all(
rules.map(function (ref) {
var rule = ref[0];
var args = ref[1];
return rule.apply(void 0, [ this$1.context.model ].concat( args ))
.then(function (res) { return res ? false : 'Validation error!'; })
})
)
.then(function (result) { return result.filter(function (result) { return result; }); })
.then(function (errorMessages) { this$1.validationErrors = errorMessages; });
} }
} }
}; };
@ -545,6 +869,7 @@
staticClass: "formulate-input", staticClass: "formulate-input",
attrs: { attrs: {
"data-classification": _vm.classification, "data-classification": _vm.classification,
"data-has-errors": _vm.hasErrors,
"data-type": _vm.type "data-type": _vm.type
} }
}, },
@ -1607,7 +1932,8 @@
FormulateInputSelect: FormulateInputSelect, FormulateInputSelect: FormulateInputSelect,
FormulateInputTextArea: FormulateInputTextArea FormulateInputTextArea: FormulateInputTextArea
}, },
library: library library: library,
rules: rules
}; };
}; };
@ -1669,6 +1995,14 @@
return false return false
}; };
/**
* Get validation rules.
* @return {object} object of validation functions
*/
Formulate.prototype.rules = function rules () {
return this.options.rules
};
var Formulate$1 = new Formulate(); var Formulate$1 = new Formulate();
exports.default = Formulate$1; exports.default = Formulate$1;

2
dist/snow.css vendored
View File

@ -12,7 +12,7 @@
.formulate-input .formulate-input-help { .formulate-input .formulate-input-help {
color: #6d6d6d; color: #6d6d6d;
font-size: .7em; font-size: .7em;
font-weight: 300; font-weight: 400;
line-height: 1.5; line-height: 1.5;
margin-bottom: .25em; } margin-bottom: .25em; }
.formulate-input .formulate-input-errors { .formulate-input .formulate-input-errors {

4
dist/snow.min.css vendored

File diff suppressed because one or more lines are too long

74
package-lock.json generated
View File

@ -1129,7 +1129,8 @@
"version": "7.0.7", "version": "7.0.7",
"resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.7.tgz",
"integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==", "integrity": "sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==",
"dev": true "dev": true,
"optional": true
}, },
"@types/babel__core": { "@types/babel__core": {
"version": "7.1.3", "version": "7.1.3",
@ -1177,6 +1178,7 @@
"resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz", "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.5.tgz",
"integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==", "integrity": "sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"@types/babel-types": "*" "@types/babel-types": "*"
} }
@ -1412,6 +1414,7 @@
"resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
"integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"kind-of": "^3.0.2", "kind-of": "^3.0.2",
"longest": "^1.0.1", "longest": "^1.0.1",
@ -1423,6 +1426,7 @@
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
"integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"is-buffer": "^1.1.5" "is-buffer": "^1.1.5"
} }
@ -2441,7 +2445,8 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true "dev": true,
"optional": true
}, },
"to-regex-range": { "to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
@ -2692,6 +2697,7 @@
"resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz",
"integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==",
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"@types/babel-types": "^7.0.0", "@types/babel-types": "^7.0.0",
"@types/babylon": "^6.16.2", "@types/babylon": "^6.16.2",
@ -4226,7 +4232,8 @@
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@ -4247,12 +4254,14 @@
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
@ -4267,17 +4276,20 @@
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -4394,7 +4406,8 @@
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@ -4406,6 +4419,7 @@
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@ -4420,6 +4434,7 @@
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
@ -4427,12 +4442,14 @@
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.3.5", "version": "2.3.5",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.2", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
@ -4451,6 +4468,7 @@
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
@ -4531,7 +4549,8 @@
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@ -4543,6 +4562,7 @@
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@ -4628,7 +4648,8 @@
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@ -4664,6 +4685,7 @@
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@ -4683,6 +4705,7 @@
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"dev": true, "dev": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@ -4726,12 +4749,14 @@
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.3", "version": "3.0.3",
"bundled": true, "bundled": true,
"dev": true "dev": true,
"optional": true
} }
} }
}, },
@ -5645,6 +5670,11 @@
"integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=",
"dev": true "dev": true
}, },
"is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
},
"is-utf8": { "is-utf8": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@ -6241,7 +6271,8 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz",
"integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=", "integrity": "sha1-Fzb939lyTyijaCrcYjCufk6Weds=",
"dev": true "dev": true,
"optional": true
}, },
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
@ -6548,7 +6579,8 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
"integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=",
"dev": true "dev": true,
"optional": true
}, },
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
@ -7559,7 +7591,8 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.0.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.1.0.tgz",
"integrity": "sha512-uhnEDzAbrcJ8R3g2fANnSuXZMBtkpSjxTTgn2LeSiQlfmq72enQJWdQllXW24MBLYnA1SBD2vfvx2o0Zw3Ielw==", "integrity": "sha512-uhnEDzAbrcJ8R3g2fANnSuXZMBtkpSjxTTgn2LeSiQlfmq72enQJWdQllXW24MBLYnA1SBD2vfvx2o0Zw3Ielw==",
"dev": true "dev": true,
"optional": true
}, },
"pify": { "pify": {
"version": "2.3.0", "version": "2.3.0",
@ -8723,7 +8756,8 @@
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz", "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-1.3.3.tgz",
"integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==", "integrity": "sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==",
"dev": true "dev": true,
"optional": true
}, },
"pug-filters": { "pug-filters": {
"version": "3.1.1", "version": "3.1.1",
@ -8843,7 +8877,8 @@
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz", "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-2.0.5.tgz",
"integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==", "integrity": "sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==",
"dev": true "dev": true,
"optional": true
}, },
"pug-strip-comments": { "pug-strip-comments": {
"version": "1.0.4", "version": "1.0.4",
@ -8859,7 +8894,8 @@
"version": "1.1.8", "version": "1.1.8",
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz", "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.8.tgz",
"integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==", "integrity": "sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==",
"dev": true "dev": true,
"optional": true
}, },
"pump": { "pump": {
"version": "3.0.0", "version": "3.0.0",

View File

@ -77,6 +77,7 @@
}, },
"dependencies": { "dependencies": {
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"is-url": "^1.2.4",
"nanoid": "^2.1.6" "nanoid": "^2.1.6"
} }
} }

View File

@ -1,4 +1,5 @@
import library from './libs/library' import library from './libs/library'
import rules from './libs/rules'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
import FormulateInput from './FormulateInput.vue' import FormulateInput from './FormulateInput.vue'
import FormulateForm from './FormulateForm.vue' import FormulateForm from './FormulateForm.vue'
@ -8,6 +9,7 @@ import FormulateInputBox from './inputs/FormulateInputBox.vue'
import FormulateInputText from './inputs/FormulateInputText.vue' import FormulateInputText from './inputs/FormulateInputText.vue'
import FormulateInputSelect from './inputs/FormulateInputSelect.vue' import FormulateInputSelect from './inputs/FormulateInputSelect.vue'
import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue' import FormulateInputTextArea from './inputs/FormulateInputTextArea.vue'
/** /**
* The base formulate library. * The base formulate library.
*/ */
@ -27,7 +29,8 @@ class Formulate {
FormulateInputSelect, FormulateInputSelect,
FormulateInputTextArea FormulateInputTextArea
}, },
library library,
rules
} }
} }
@ -88,6 +91,14 @@ class Formulate {
} }
return false return false
} }
/**
* Get validation rules.
* @return {object} object of validation functions
*/
rules () {
return this.options.rules
}
} }
export default new Formulate() export default new Formulate()

View File

@ -48,7 +48,7 @@
<script> <script>
import context from './libs/context' import context from './libs/context'
import { shallowEqualObjects } from './libs/utils' import { shallowEqualObjects, parseRules } from './libs/utils'
import nanoid from 'nanoid' import nanoid from 'nanoid'
export default { export default {
@ -131,7 +131,8 @@ export default {
return { return {
defaultId: nanoid(9), defaultId: nanoid(9),
localAttributes: {}, localAttributes: {},
internalModelProxy: this.formulateValue internalModelProxy: this.formulateValue,
validationErrors: []
} }
}, },
computed: { computed: {
@ -152,6 +153,7 @@ export default {
deep: true deep: true
}, },
internalModelProxy (newValue, oldValue) { internalModelProxy (newValue, oldValue) {
this.performValidation()
if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) { if (!this.isVmodeled && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue this.context.model = newValue
} }
@ -167,12 +169,24 @@ export default {
this.formulateFormRegister(this.nameOrFallback, this) this.formulateFormRegister(this.nameOrFallback, this)
} }
this.updateLocalAttributes(this.$attrs) this.updateLocalAttributes(this.$attrs)
this.performValidation()
}, },
methods: { methods: {
updateLocalAttributes (value) { updateLocalAttributes (value) {
if (!shallowEqualObjects(value, this.localAttributes)) { if (!shallowEqualObjects(value, this.localAttributes)) {
this.localAttributes = value this.localAttributes = value
} }
},
performValidation () {
const rules = parseRules(this.validation, this.$formulate.rules())
Promise.all(
rules.map(([rule, args]) => {
return rule(this.context.model, ...args)
.then(res => res ? false : 'Validation error!')
})
)
.then(result => result.filter(result => result))
.then(errorMessages => { this.validationErrors = errorMessages })
} }
} }
} }

View File

@ -1,5 +1,5 @@
import nanoid from 'nanoid' import nanoid from 'nanoid'
import { map, arrayify, parseRules } from './utils' import { map, arrayify } from './utils'
/** /**
* For a single instance of an input, export all of the context needed to fully * For a single instance of an input, export all of the context needed to fully
@ -30,8 +30,7 @@ export default {
logicalLabelPosition, logicalLabelPosition,
isVmodeled, isVmodeled,
mergedErrors, mergedErrors,
hasErrors, hasErrors
validationRules
} }
/** /**
@ -149,13 +148,6 @@ function hasErrors () {
return !!this.mergedErrors.length return !!this.mergedErrors.length
} }
/**
* An array of validation rules to pass.
*/
function validationRules () {
return parseRules(this.validation)
}
/** /**
* Defines the model used throughout the existing context. * Defines the model used throughout the existing context.
* @param {object} context * @param {object} context

View File

@ -1,41 +1,220 @@
import isUrl from 'is-url'
import { shallowEqualObjects } from './utils' import { shallowEqualObjects } from './utils'
/** /**
* Library of rules * Library of rules
*/ */
export default { export default {
/**
* Rule: the value must be "yes", "on", "1", or true
*/
accepted: function (value) {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
},
/** /**
* Rule: must be a value * Rule: must be a value
*/ */
required: async function (value) { required: function (value, isRequired = true) {
if (Array.isArray(value)) { return Promise.resolve((() => {
return !!value.length if (!isRequired || ['no', 'false'].includes(isRequired)) {
} return true
if (typeof value === 'string') { }
return !!value if (Array.isArray(value)) {
} return !!value.length
if (typeof value === 'object') { }
return (!value) ? false : !!Object.keys(value).length if (typeof value === 'string') {
} return !!value
return true }
if (typeof value === 'object') {
return (!value) ? false : !!Object.keys(value).length
}
return true
})())
}, },
/** /**
* Rule: Value is in an array (stack). * Rule: Value is in an array (stack).
*/ */
in: async function (value, ...stack) { in: function (value, ...stack) {
return !!stack.find(item => shallowEqualObjects(item, value)) return Promise.resolve(stack.find(item => {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) !== undefined)
},
/**
* Rule: Value is not in stack.
*/
not: function (value, ...stack) {
return Promise.resolve(stack.find(item => {
if (typeof item === 'object') {
return shallowEqualObjects(item, value)
}
return item === value
}) === undefined)
}, },
/** /**
* Rule: Match the value against a (stack) of patterns or strings * Rule: Match the value against a (stack) of patterns or strings
*/ */
matches: async function (value, ...stack) { matches: function (value, ...stack) {
return !!stack.find(pattern => { return Promise.resolve(!!stack.find(pattern => {
if (pattern instanceof RegExp) { if (pattern instanceof RegExp) {
return pattern.test(value) return pattern.test(value)
} }
return pattern === value return pattern === value
}) }))
},
/**
* Rule: checks if a string is a valid url
*/
url: function (value) {
return Promise.resolve(isUrl(value))
},
/**
* Rule: ensures the value is a date according to Date.parse()
*/
date: function (value) {
return Promise.resolve(!isNaN(Date.parse(value)))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
after: function (value, compare = false) {
const timestamp = Date.parse(compare || new Date())
const fieldValue = Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
},
/**
* Rule: checks if a value is after a given date. Defaults to current time
*/
before: function (value, compare = false) {
const timestamp = Date.parse(compare || new Date())
const fieldValue = Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
},
/**
* Rule: checks if the value is only alpha numeric
*/
alpha: function (value, set = 'default') {
const sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-z][A-Z]$/
}
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is only alpha numeric
*/
number: function (value) {
return Promise.resolve(!isNaN(value))
},
/**
* Rule: checks if the value is alpha numeric
*/
alphanumeric: function (value, set = 'default') {
const sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]$/
}
const selectedSet = sets.hasOwnProperty(set) ? set : 'default'
return Promise.resolve(sets[selectedSet].test(value))
},
/**
* Rule: checks if the value is between two other values
*/
between: function (value, from, to) {
return Promise.resolve((() => {
if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false
}
from = Number(from)
to = Number(to)
if (!isNaN(value)) {
value = Number(value)
return (value > from && value < to)
}
if (typeof value === 'string') {
return value.length > from && value.length < to
}
return false
})())
},
/**
* Rule: tests
*/
email: function (value) {
// eslint-disable-next-line
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
return Promise.resolve(isEmail.test(value))
},
/**
* Check the file type is correct.
*/
mime: function (files, ...types) {
return Promise.resolve((() => {
if (typeof window !== 'undefined' && typeof FileReader !== 'undefined' && typeof Blob !== 'undefined') {
for (const i in files) {
if (!types.includes(files[i].type)) {
return false
}
}
}
return true
})())
},
/**
* Check the minimum value of a particular.
*/
min: function (value, minimum) {
return Promise.resolve((() => {
minimum = Number(minimum)
if (!isNaN(value)) {
value = Number(value)
return value >= minimum
}
if (typeof value === 'string') {
return value.length >= minimum
}
if (Array.isArray(value)) {
return value.length >= minimum
}
return false
})())
},
/**
* Check the minimum value of a particular.
*/
max: function (value, minimum) {
return Promise.resolve((() => {
minimum = Number(minimum)
if (!isNaN(value)) {
value = Number(value)
return value <= minimum
}
if (typeof value === 'string') {
return value.length <= minimum
}
if (Array.isArray(value)) {
return value.length <= minimum
}
return false
})())
} }
} }

View File

@ -19,6 +19,10 @@ describe('required', () => {
it('passes with a non empty array', async () => expect(await rules.required(['123'])).toBe(true)) it('passes with a non empty array', async () => expect(await rules.required(['123'])).toBe(true))
it('passes with a non empty object', async () => expect(await rules.required({a: 'b'})).toBe(true)) it('passes with a non empty object', async () => expect(await rules.required({a: 'b'})).toBe(true))
it('passes with empty value if second argument is false', async () => expect(await rules.required('', false)).toBe(true))
it('passes with empty value if second argument is false string', async () => expect(await rules.required('', 'false')).toBe(true))
}) })
@ -75,3 +79,273 @@ describe('matches', () => {
expect(await rules.matches('first-fourth', 'second', /^third/, /fourth$/)).toBe(true) expect(await rules.matches('first-fourth', 'second', /^third/, /fourth$/)).toBe(true)
}) })
}) })
/**
* Accepted rule
*/
describe('accepted', () => {
it('passes with true', async () => expect(await rules.accepted('yes')).toBe(true))
it('passes with on', async () => expect(await rules.accepted('on')).toBe(true))
it('passes with 1', async () => expect(await rules.accepted('1')).toBe(true))
it('passes with number 1', async () => expect(await rules.accepted(1)).toBe(true))
it('passes with boolean true', async () => expect(await rules.accepted(true)).toBe(true))
it('fail with boolean false', async () => expect(await rules.accepted(false)).toBe(false))
it('fail with "false"', async () => expect(await rules.accepted('false')).toBe(false))
})
/**
* Url rule.
*
* Note: these are just sanity checks because the actual package we use is
* well tested: https://github.com/segmentio/is-url/blob/master/test/index.js
*/
describe('url', () => {
it('passes with http://google.com', async () => expect(await rules.url('http://google.com')).toBe(true))
it('fails with google.com', async () => expect(await rules.url('google.com')).toBe(false))
})
/**
* Determines if the string is a date
*/
describe('date', () => {
it('passes with month day year', async () => expect(await rules.date('December 17, 2020')).toBe(true))
it('passes with month day', async () => expect(await rules.date('December 17')).toBe(true))
it('passes with short month day', async () => expect(await rules.date('Dec 17')).toBe(true))
it('passes with short month day', async () => expect(await rules.date('Dec 17 12:34:15')).toBe(true))
it('passes with out of bounds number', async () => expect(await rules.date('January 77')).toBe(true))
it('passes with only month', async () => expect(await rules.date('January')).toBe(false))
it('fails with only day of week', async () => expect(await rules.date('saturday')).toBe(false))
it('fails with random string', async () => expect(await rules.date('Pepsi 17')).toBe(false))
it('fails with random number', async () => expect(await rules.date('1872301237')).toBe(false))
})
/**
* Checks if a date is after another date
*/
describe('after', () => {
const today = new Date()
const tomorrow = new Date()
const yesterday = new Date()
tomorrow.setDate(today.getDate() + 1)
yesterday.setDate(today.getDate() - 1)
it('passes with tomorrows date object', async () => expect(await rules.after(tomorrow)).toBe(true))
it('passes with future date', async () => expect(await rules.after('January 15, 2999')).toBe(true))
it('passes with long past date', async () => expect(await rules.after(yesterday, 'Jan 15, 2000')).toBe(true))
it('fails with yesterdays date', async () => expect(await rules.after(yesterday)).toBe(false))
it('fails with old date string', async () => expect(await rules.after('January, 2000')).toBe(false))
it('fails with invalid value', async () => expect(await rules.after('')).toBe(false))
})
/**
* Checks if a date is after another date
*/
describe('before', () => {
const today = new Date()
const tomorrow = new Date()
const yesterday = new Date()
tomorrow.setDate(today.getDate() + 1)
yesterday.setDate(today.getDate() - 1)
it('fails with tomorrows date object', async () => expect(await rules.before(tomorrow)).toBe(false))
it('fails with future date', async () => expect(await rules.before('January 15, 2999')).toBe(false))
it('fails with long past date', async () => expect(await rules.before(yesterday, 'Jan 15, 2000')).toBe(false))
it('passes with yesterdays date', async () => expect(await rules.before(yesterday)).toBe(true))
it('passes with old date string', async () => expect(await rules.before('January, 2000')).toBe(true))
it('fails with invalid value', async () => expect(await rules.after('')).toBe(false))
})
/**
* Checks if a date is after another date
*/
describe('alpha', () => {
it('passes with simple string', async () => expect(await rules.alpha('abc')).toBe(true))
it('passes with long string', async () => expect(await rules.alpha('lkashdflaosuihdfaisudgflakjsdbflasidufg')).toBe(true))
it('passes with single character', async () => expect(await rules.alpha('z')).toBe(true))
it('passes with accented character', async () => expect(await rules.alpha('jüstin')).toBe(true))
it('passes with lots of accented characters', async () => expect(await rules.alpha('àáâäïíôöÆ')).toBe(true))
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alpha('àáâäïíôöÆ', 'russian')).toBe(true))
it('fails with lots of accented characters if latin', async () => expect(await rules.alpha('àáâäïíôöÆ', 'latin')).toBe(false))
it('fails with numbers', async () => expect(await rules.alpha('justin83')).toBe(false))
it('fails with symbols', async () => expect(await rules.alpha('-justin')).toBe(false))
})
/**
* Checks if a date is after another date
*/
describe('number', () => {
it('passes with simple number string', async () => expect(await rules.number('123')).toBe(true))
it('passes with simple number', async () => expect(await rules.number(19832461234)).toBe(true))
it('passes with float', async () => expect(await rules.number(198.32464)).toBe(true))
it('passes with decimal in string', async () => expect(await rules.number('567.23')).toBe(true))
it('fails with comma in number string', async () => expect(await rules.number('123,456')).toBe(false))
it('fails with alpha', async () => expect(await rules.number('123sdf')).toBe(false))
})
/**
* Checks if a date alpha and numeric
*/
describe('alphanumeric', () => {
it('passes with simple string', async () => expect(await rules.alphanumeric('567abc')).toBe(true))
it('passes with long string', async () => expect(await rules.alphanumeric('lkashdfla234osuihdfaisudgflakjsdbfla567sidufg')).toBe(true))
it('passes with single character', async () => expect(await rules.alphanumeric('z')).toBe(true))
it('passes with accented character', async () => expect(await rules.alphanumeric('jüst56in')).toBe(true))
it('passes with lots of accented characters', async () => expect(await rules.alphanumeric('àáâ7567567äïíôöÆ')).toBe(true))
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alphanumeric('123123àáâäï67íôöÆ', 'russian')).toBe(true))
it('fails with lots of accented characters if latin', async () => expect(await rules.alphanumeric('àáâäï123123íôöÆ', 'latin')).toBe(false))
it('fails with decimals in', async () => expect(await rules.alphanumeric('abcABC99.123')).toBe(false))
})
/**
* Checks if between
*/
describe('between', () => {
it('passes with simple number', async () => expect(await rules.between(5, 0, 10)).toBe(true))
it('passes with simple number string', async () => expect(await rules.between('5', '0', '10')).toBe(true))
it('passes with decimal number string', async () => expect(await rules.between('0.5', '0', '1')).toBe(true))
it('passes with string length', async () => expect(await rules.between('abc', 2, 4)).toBe(true))
it('fails with string length too long', async () => expect(await rules.between('abcdef', 2, 4)).toBe(false))
it('fails with string length too short', async () => expect(await rules.between('abc', 3, 10)).toBe(false))
it('fails with number to small', async () => expect(await rules.between(0, 3, 10)).toBe(false))
it('fails with number to large', async () => expect(await rules.between(15, 3, 10)).toBe(false))
})
/**
* Checks if email.
*
* Note: testing is light, regular expression used is here: http://jsfiddle.net/ghvj4gy9/embedded/result,js/
*/
describe('email', () => {
it('passes normal email', async () => expect(await rules.email('dev+123@wearebraid.com')).toBe(true))
it('passes numeric email', async () => expect(await rules.email('12345@google.com')).toBe(true))
it('passes unicode email', async () => expect(await rules.email('àlphä@❤️.ly')).toBe(true))
it('passes numeric with new tld', async () => expect(await rules.email('12345@google.photography')).toBe(true))
it('fails string without tld', async () => expect(await rules.email('12345@localhost')).toBe(false))
it('fails string without tld', async () => expect(await rules.email('12345@localhost')).toBe(false))
it('fails string without invalid name', async () => expect(await rules.email('1*(123)2345@localhost')).toBe(false))
})
/**
* Mime types.
*/
describe('mime', () => {
it('passes basic image/jpeg stack', async () => expect(await rules.mime([{type: 'image/jpeg'}], 'image/png', 'image/jpeg')).toBe(true))
it('fails when not in stack', async () => expect(await rules.mime([{type: 'application/json'}], 'image/png', 'image/jpeg')).toBe(false))
})
/**
* Minimum.
*/
describe('min', () => {
it('passes when a number string', async () => expect(await rules.min('5', '5')).toBe(true))
it('passes when a number', async () => expect(await rules.min(6, 5)).toBe(true))
it('passes when a string length', async () => expect(await rules.min('foobar', '6')).toBe(true))
it('passes when a array length', async () => expect(await rules.min(Array(6), '6')).toBe(true))
it('fails when a array length', async () => expect(await rules.min(Array(6), '7')).toBe(false))
it('fails when a string length', async () => expect(await rules.min('bar', 4)).toBe(false))
it('fails when a number', async () => expect(await rules.min(3, '7')).toBe(false))
})
/**
* Maximum.
*/
describe('max', () => {
it('passes when a number string', async () => expect(await rules.max('5', '5')).toBe(true))
it('passes when a number', async () => expect(await rules.max(5, 6)).toBe(true))
it('passes when a string length', async () => expect(await rules.max('foobar', '6')).toBe(true))
it('passes when a array length', async () => expect(await rules.max(Array(6), '6')).toBe(true))
it('fails when a array length', async () => expect(await rules.max(Array(6), '5')).toBe(false))
it('fails when a string length', async () => expect(await rules.max('bar', 2)).toBe(false))
it('fails when a number', async () => expect(await rules.max(10, '7')).toBe(false))
})
/**
* Maximum.
*/
describe('not', () => {
it('passes when a number string', async () => expect(await rules.not('5', '6')).toBe(true))
it('passes when a number', async () => expect(await rules.not(1, 30)).toBe(true))
it('passes when a string', async () => expect(await rules.not('abc', 'def')).toBe(true))
it('fails when a shallow equal array', async () => expect(await rules.not(['abc'], ['abc'])).toBe(false))
it('fails when a shallow equal object', async () => expect(await rules.not({a: 'abc'}, ['123'], {a: 'abc'})).toBe(false))
it('fails when string is in stack', async () => expect(await rules.not('a', 'b', 'c', 'd', 'a', 'f')).toBe(false))
})

View File

@ -20,7 +20,7 @@
.formulate-input-help { .formulate-input-help {
color: $formulate-gray-ddd; color: $formulate-gray-ddd;
font-size: .7em; font-size: .7em;
font-weight: 300; font-weight: 400;
line-height: 1.5; line-height: 1.5;
margin-bottom: .25em; margin-bottom: .25em;
} }