From ba24fc21e8106a208ff2b60901cb0948e0bb735e Mon Sep 17 00:00:00 2001 From: Justin Schroeder Date: Wed, 13 Nov 2019 16:10:17 -0500 Subject: [PATCH] Adds default validation messages for all rule types --- dist/formulate.esm.js | 574 +++++++++++++++++++------- dist/formulate.min.js | 574 +++++++++++++++++++------- dist/formulate.umd.js | 574 +++++++++++++++++++------- src/Formulate.js | 21 +- src/FormulateForm.vue | 6 +- src/FormulateInput.vue | 28 +- src/inputs/FormulateInputBox.vue | 1 + src/inputs/FormulateInputSelect.vue | 1 + src/inputs/FormulateInputText.vue | 1 + src/inputs/FormulateInputTextArea.vue | 1 + src/libs/context.js | 44 +- src/libs/rules.js | 356 ++++++++-------- src/libs/utils.js | 13 +- src/locales/en.js | 143 +++++++ test/FormulateInputText.test.js | 35 +- 15 files changed, 1755 insertions(+), 617 deletions(-) create mode 100644 src/locales/en.js diff --git a/dist/formulate.esm.js b/dist/formulate.esm.js index 98ba914..a31749e 100644 --- a/dist/formulate.esm.js +++ b/dist/formulate.esm.js @@ -161,6 +161,17 @@ function arrayify (item) { return [] } +/** + * How to add an item. + * @param {string} item + */ +function sentence (item) { + if (typeof item === 'string') { + return item[0].toUpperCase() + item.substr(1) + } + return item +} + /** * Given an array or string return an array of callables. * @param {array|string} validation @@ -187,6 +198,7 @@ function parseRule (rule, rules) { return [rule, []] } if (Array.isArray(rule) && rule.length) { + rule = rule.map(function (r) { return r; }); // light clone if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) { return [rules[rule.shift()], rule] } @@ -217,88 +229,6 @@ var rules = { 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 */ @@ -310,6 +240,34 @@ var rules = { return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp)) }, + /** + * Rule: checks if the value is only alpha + */ + alpha: function (value, set) { + if ( set === void 0 ) set = 'default'; + + var sets = { + default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, + latin: /^[a-zA-Z]+$/ + }; + var selectedSet = sets.hasOwnProperty(set) ? set : 'default'; + return Promise.resolve(sets[selectedSet].test(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 a value is after a given date. Defaults to current time */ @@ -321,45 +279,13 @@ var rules = { 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) { + if ( from === void 0 ) from = 0; + if ( to === void 0 ) to = 10; + return Promise.resolve((function () { if (from === null || to === null || isNaN(from) || isNaN(to)) { return false @@ -377,6 +303,13 @@ var rules = { })()) }, + /** + * Rule: ensures the value is a date according to Date.parse() + */ + date: function (value) { + return Promise.resolve(!isNaN(Date.parse(value))) + }, + /** * Rule: tests */ @@ -386,6 +319,58 @@ var rules = { return Promise.resolve(isEmail.test(value)) }, + /** + * 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: 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 + })) + }, + + /** + * Check the maximum value of a particular. + */ + max: function (value, minimum) { + if ( minimum === void 0 ) minimum = 10; + + 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 file type is correct. */ @@ -409,6 +394,8 @@ var rules = { * Check the minimum value of a particular. */ min: function (value, minimum) { + if ( minimum === void 0 ) minimum = 1; + return Promise.resolve((function () { minimum = Number(minimum); if (!isNaN(value)) { @@ -426,23 +413,243 @@ var rules = { }, /** - * Check the minimum value of a particular. + * Rule: Value is not in stack. */ - max: function (value, minimum) { - return Promise.resolve((function () { - minimum = Number(minimum); - if (!isNaN(value)) { - value = Number(value); - return value <= minimum + 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) } - if (typeof value === 'string') { - return value.length <= minimum + return item === value + }) === undefined) + }, + + /** + * Rule: checks if the value is only alpha numeric + */ + number: function (value) { + return Promise.resolve(!isNaN(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 <= minimum + return !!value.length } - return false + if (typeof value === 'string') { + return !!value + } + if (typeof value === 'object') { + return (!value) ? false : !!Object.keys(value).length + } + return true })()) + }, + + /** + * Rule: checks if a string is a valid url + */ + url: function (value) { + return Promise.resolve(isUrl(value)) + } +}; + +/** + * Validation error message generators. + */ +var en = { + + /** + * Valid accepted value. + */ + accepted: function (ref) { + var name = ref.name; + + return ("Please accept the " + name + ".") + }, + + /** + * The date is not after. + */ + after: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be after " + (args[0]) + ".") + } + return ((sentence(name)) + " must be a later date.") + }, + + /** + * The value is not a letter. + */ + alpha: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain alphabetical characters.") + }, + + /** + * Rule: checks if the value is alpha numeric + */ + alphanumeric: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain letters and numbers.") + }, + + /** + * The date is not before. + */ + before: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be before " + (args[0]) + ".") + } + return ((sentence(name)) + " must be an earlier date.") + }, + + /** + * The value is not between two numbers or lengths + */ + between: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + ".") + } + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + " characters long.") + }, + + /** + * Is not a valid date. + */ + date: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not a valid date.") + }, + + /** + * The default render method for error messages. + */ + default: function (ref) { + var name = ref.name; + + return "This field isn’t valid." + }, + + /** + * Is not a valid email address. + */ + email: function (ref) { + var name = ref.name; + var value = ref.value; + + return (value + " is not a valid email address.") + }, + + /** + * Value is an allowed value. + */ + in: function (ref) { + var name = ref.name; + var value = ref.value; + + if (typeof value === 'string') { + return ("“" + (sentence(value)) + "” is not an allowed " + name + ".") + } + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * Value is not a match. + */ + matches: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * The maximum value allowed. + */ + max: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be less than " + (args[0]) + ".") + } + return (name + " must be less than " + (args[0]) + " characters long.") + }, + + /** + * The maximum value allowed. + */ + min: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be more than " + (args[0]) + ".") + } + return (name + " must be more than " + (args[0]) + " characters long.") + }, + + /** + * The field is not an allowed value + */ + not: function (ref) { + var name = ref.name; + var value = ref.value; + + return ("“" + value + "” is not an allowed " + name + ".") + }, + + /** + * The field is not a number + */ + number: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " must be a number.") + }, + + /** + * Required field. + */ + required: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is required.") + }, + + /** + * Value is not a url. + */ + url: function (ref) { + var name = ref.name; + + return "Please include a valid url." } }; @@ -453,9 +660,6 @@ var rules = { */ var context = { context: function context () { - if (this.debug) { - console.log(((this.type) + " re-context")); - } return defineModel.call(this, Object.assign({}, {type: this.type, value: this.value, name: this.nameOrFallback, @@ -464,7 +668,8 @@ var context = { id: this.id || this.defaultId, label: this.label, labelPosition: this.logicalLabelPosition, - attributes: this.elementAttributes}, + attributes: this.elementAttributes, + blurHandler: blurHandler.bind(this)}, this.typeContext)) }, nameOrFallback: nameOrFallback, @@ -473,7 +678,9 @@ var context = { logicalLabelPosition: logicalLabelPosition, isVmodeled: isVmodeled, mergedErrors: mergedErrors, - hasErrors: hasErrors + hasErrors: hasErrors, + showFieldErrors: showFieldErrors, + mergedValidationName: mergedValidationName }; /** @@ -533,6 +740,33 @@ function logicalLabelPosition () { } } +/** + * The validation label to use. + */ +function mergedValidationName () { + if (this.validationName) { + return this.validationName + } + if (typeof this.name === 'string') { + return this.name + } + if (this.label) { + return this.label + } + return this.type +} + +/** + * Determines if the field should show it's error (if it has one) + * @return {boolean} + */ +function showFieldErrors () { + if (this.showErrors) { + return this.showErrors + } + return this.behavioralErrorVisibility +} + /** * Return the element’s name, or select a fallback. */ @@ -593,6 +827,15 @@ function hasErrors () { return !!this.mergedErrors.length } +/** + * Bound into the context object. + */ +function blurHandler () { + if (this.errorBehavior === 'blur') { + this.behavioralErrorVisibility = true; + } +} + /** * Defines the model used throughout the existing context. * @param {object} context @@ -636,7 +879,8 @@ var script = { inheritAttrs: false, inject: { formulateFormSetter: { default: undefined }, - formulateFormRegister: { default: undefined } + formulateFormRegister: { default: undefined }, + getFormValues: { default: function () { return function () { return ({}); }; } } }, model: { prop: 'formulateValue', @@ -695,15 +939,23 @@ var script = { type: [String, Boolean, Array], default: false }, - validationBehavior: { + validationName: { + type: [String, Boolean], + default: false + }, + error: { + type: [String, Boolean], + default: false + }, + errorBehavior: { type: String, default: 'blur', validator: function (value) { return ['blur', 'live'].includes(value) } }, - error: { - type: [String, Boolean], + showErrors: { + type: Boolean, default: false } }, @@ -712,6 +964,7 @@ var script = { defaultId: nanoid(9), localAttributes: {}, internalModelProxy: this.formulateValue, + behavioralErrorVisibility: (this.errorBehavior === 'live'), validationErrors: [] } }, @@ -765,7 +1018,13 @@ var script = { 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 (res) { return res ? false : this$1.$formulate.validationMessage(rule.name, { + args: args, + name: this$1.mergedValidationName, + value: this$1.context.model, + vm: this$1, + formValues: this$1.getFormValues() + }); }) }) ) .then(function (result) { return result.filter(function (result) { return result; }); }) @@ -864,6 +1123,7 @@ var __vue_render__ = function() { attrs: { "data-classification": _vm.classification, "data-has-errors": _vm.hasErrors, + "data-is-showing-errors": _vm.hasErrors && _vm.showFieldErrors, "data-type": _vm.type } }, @@ -926,7 +1186,9 @@ var __vue_render__ = function() { }) : _vm._e(), _vm._v(" "), - _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + _vm.showFieldErrors + ? _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + : _vm._e() ], 1 ) @@ -969,7 +1231,8 @@ var script$1 = { provide: function provide () { return { formulateFormSetter: this.setFieldValue, - formulateFormRegister: this.register + formulateFormRegister: this.register, + getFormValues: this.getFormValues } }, name: 'FormulateForm', @@ -1051,6 +1314,9 @@ var script$1 = { formSubmitted: function formSubmitted () { // perform validation here this.$emit('submit', this.formModel); + }, + getFormValues: function getFormValues () { + return this.internalFormModelProxy } } }; @@ -1379,6 +1645,7 @@ var __vue_render__$4 = function() { : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1427,6 +1694,7 @@ var __vue_render__$4 = function() { checked: _vm._q(_vm.context.model, _vm.context.value) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", _vm.context.value) } @@ -1455,6 +1723,7 @@ var __vue_render__$4 = function() { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1549,6 +1818,7 @@ var __vue_render__$5 = function() { : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1594,6 +1864,7 @@ var __vue_render__$5 = function() { attrs: { type: "radio" }, domProps: { checked: _vm._q(_vm.context.model, null) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", null) } @@ -1619,6 +1890,7 @@ var __vue_render__$5 = function() { attrs: { type: _vm.type }, domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1711,6 +1983,7 @@ var __vue_render__$6 = function() { ], attrs: { "data-placeholder-selected": _vm.placeholderSelected }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$selectedVal = Array.prototype.filter .call($event.target.options, function(o) { @@ -1863,6 +2136,7 @@ var __vue_render__$7 = function() { ], domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1927,7 +2201,11 @@ var Formulate = function Formulate () { FormulateInputTextArea: FormulateInputTextArea }, library: library, - rules: rules + rules: rules, + locale: 'en', + locales: { + en: en + } }; }; @@ -1997,6 +2275,20 @@ Formulate.prototype.rules = function rules () { return this.options.rules }; +/** + * Get the validation message for a particular error. + */ +Formulate.prototype.validationMessage = function validationMessage (rule, validationContext) { + var generators = this.options.locales[this.options.locale]; + if (generators.hasOwnProperty(rule)) { + return generators[rule](validationContext) + } + if (generators.hasOwnProperty('default')) { + return generators.default(validationContext) + } + return 'This field does not have a valid value' +}; + var Formulate$1 = new Formulate(); export default Formulate$1; diff --git a/dist/formulate.min.js b/dist/formulate.min.js index 71dca1f..a99741d 100644 --- a/dist/formulate.min.js +++ b/dist/formulate.min.js @@ -164,6 +164,17 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return [] } + /** + * How to add an item. + * @param {string} item + */ + function sentence (item) { + if (typeof item === 'string') { + return item[0].toUpperCase() + item.substr(1) + } + return item + } + /** * Given an array or string return an array of callables. * @param {array|string} validation @@ -190,6 +201,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return [rule, []] } if (Array.isArray(rule) && rule.length) { + rule = rule.map(function (r) { return r; }); // light clone if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) { return [rules[rule.shift()], rule] } @@ -220,88 +232,6 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { 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 */ @@ -313,6 +243,34 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp)) }, + /** + * Rule: checks if the value is only alpha + */ + alpha: function (value, set) { + if ( set === void 0 ) set = 'default'; + + var sets = { + default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, + latin: /^[a-zA-Z]+$/ + }; + var selectedSet = sets.hasOwnProperty(set) ? set : 'default'; + return Promise.resolve(sets[selectedSet].test(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 a value is after a given date. Defaults to current time */ @@ -324,45 +282,13 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { 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) { + if ( from === void 0 ) from = 0; + if ( to === void 0 ) to = 10; + return Promise.resolve((function () { if (from === null || to === null || isNaN(from) || isNaN(to)) { return false @@ -380,6 +306,13 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { })()) }, + /** + * Rule: ensures the value is a date according to Date.parse() + */ + date: function (value) { + return Promise.resolve(!isNaN(Date.parse(value))) + }, + /** * Rule: tests */ @@ -389,6 +322,58 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return Promise.resolve(isEmail.test(value)) }, + /** + * 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: 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 + })) + }, + + /** + * Check the maximum value of a particular. + */ + max: function (value, minimum) { + if ( minimum === void 0 ) minimum = 10; + + 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 file type is correct. */ @@ -412,6 +397,8 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { * Check the minimum value of a particular. */ min: function (value, minimum) { + if ( minimum === void 0 ) minimum = 1; + return Promise.resolve((function () { minimum = Number(minimum); if (!isNaN(value)) { @@ -429,23 +416,243 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { }, /** - * Check the minimum value of a particular. + * Rule: Value is not in stack. */ - max: function (value, minimum) { - return Promise.resolve((function () { - minimum = Number(minimum); - if (!isNaN(value)) { - value = Number(value); - return value <= minimum + 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) } - if (typeof value === 'string') { - return value.length <= minimum + return item === value + }) === undefined) + }, + + /** + * Rule: checks if the value is only alpha numeric + */ + number: function (value) { + return Promise.resolve(!isNaN(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 <= minimum + return !!value.length } - return false + if (typeof value === 'string') { + return !!value + } + if (typeof value === 'object') { + return (!value) ? false : !!Object.keys(value).length + } + return true })()) + }, + + /** + * Rule: checks if a string is a valid url + */ + url: function (value) { + return Promise.resolve(isUrl(value)) + } + }; + + /** + * Validation error message generators. + */ + var en = { + + /** + * Valid accepted value. + */ + accepted: function (ref) { + var name = ref.name; + + return ("Please accept the " + name + ".") + }, + + /** + * The date is not after. + */ + after: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be after " + (args[0]) + ".") + } + return ((sentence(name)) + " must be a later date.") + }, + + /** + * The value is not a letter. + */ + alpha: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain alphabetical characters.") + }, + + /** + * Rule: checks if the value is alpha numeric + */ + alphanumeric: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain letters and numbers.") + }, + + /** + * The date is not before. + */ + before: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be before " + (args[0]) + ".") + } + return ((sentence(name)) + " must be an earlier date.") + }, + + /** + * The value is not between two numbers or lengths + */ + between: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + ".") + } + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + " characters long.") + }, + + /** + * Is not a valid date. + */ + date: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not a valid date.") + }, + + /** + * The default render method for error messages. + */ + default: function (ref) { + var name = ref.name; + + return "This field isn’t valid." + }, + + /** + * Is not a valid email address. + */ + email: function (ref) { + var name = ref.name; + var value = ref.value; + + return (value + " is not a valid email address.") + }, + + /** + * Value is an allowed value. + */ + in: function (ref) { + var name = ref.name; + var value = ref.value; + + if (typeof value === 'string') { + return ("“" + (sentence(value)) + "” is not an allowed " + name + ".") + } + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * Value is not a match. + */ + matches: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * The maximum value allowed. + */ + max: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be less than " + (args[0]) + ".") + } + return (name + " must be less than " + (args[0]) + " characters long.") + }, + + /** + * The maximum value allowed. + */ + min: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be more than " + (args[0]) + ".") + } + return (name + " must be more than " + (args[0]) + " characters long.") + }, + + /** + * The field is not an allowed value + */ + not: function (ref) { + var name = ref.name; + var value = ref.value; + + return ("“" + value + "” is not an allowed " + name + ".") + }, + + /** + * The field is not a number + */ + number: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " must be a number.") + }, + + /** + * Required field. + */ + required: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is required.") + }, + + /** + * Value is not a url. + */ + url: function (ref) { + var name = ref.name; + + return "Please include a valid url." } }; @@ -456,9 +663,6 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { */ var context = { context: function context () { - if (this.debug) { - console.log(((this.type) + " re-context")); - } return defineModel.call(this, Object.assign({}, {type: this.type, value: this.value, name: this.nameOrFallback, @@ -467,7 +671,8 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { id: this.id || this.defaultId, label: this.label, labelPosition: this.logicalLabelPosition, - attributes: this.elementAttributes}, + attributes: this.elementAttributes, + blurHandler: blurHandler.bind(this)}, this.typeContext)) }, nameOrFallback: nameOrFallback, @@ -476,7 +681,9 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { logicalLabelPosition: logicalLabelPosition, isVmodeled: isVmodeled, mergedErrors: mergedErrors, - hasErrors: hasErrors + hasErrors: hasErrors, + showFieldErrors: showFieldErrors, + mergedValidationName: mergedValidationName }; /** @@ -536,6 +743,33 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { } } + /** + * The validation label to use. + */ + function mergedValidationName () { + if (this.validationName) { + return this.validationName + } + if (typeof this.name === 'string') { + return this.name + } + if (this.label) { + return this.label + } + return this.type + } + + /** + * Determines if the field should show it's error (if it has one) + * @return {boolean} + */ + function showFieldErrors () { + if (this.showErrors) { + return this.showErrors + } + return this.behavioralErrorVisibility + } + /** * Return the element’s name, or select a fallback. */ @@ -596,6 +830,15 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return !!this.mergedErrors.length } + /** + * Bound into the context object. + */ + function blurHandler () { + if (this.errorBehavior === 'blur') { + this.behavioralErrorVisibility = true; + } + } + /** * Defines the model used throughout the existing context. * @param {object} context @@ -639,7 +882,8 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { inheritAttrs: false, inject: { formulateFormSetter: { default: undefined }, - formulateFormRegister: { default: undefined } + formulateFormRegister: { default: undefined }, + getFormValues: { default: function () { return function () { return ({}); }; } } }, model: { prop: 'formulateValue', @@ -698,15 +942,23 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { type: [String, Boolean, Array], default: false }, - validationBehavior: { + validationName: { + type: [String, Boolean], + default: false + }, + error: { + type: [String, Boolean], + default: false + }, + errorBehavior: { type: String, default: 'blur', validator: function (value) { return ['blur', 'live'].includes(value) } }, - error: { - type: [String, Boolean], + showErrors: { + type: Boolean, default: false } }, @@ -715,6 +967,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { defaultId: nanoid(9), localAttributes: {}, internalModelProxy: this.formulateValue, + behavioralErrorVisibility: (this.errorBehavior === 'live'), validationErrors: [] } }, @@ -768,7 +1021,13 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { 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 (res) { return res ? false : this$1.$formulate.validationMessage(rule.name, { + args: args, + name: this$1.mergedValidationName, + value: this$1.context.model, + vm: this$1, + formValues: this$1.getFormValues() + }); }) }) ) .then(function (result) { return result.filter(function (result) { return result; }); }) @@ -867,6 +1126,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { attrs: { "data-classification": _vm.classification, "data-has-errors": _vm.hasErrors, + "data-is-showing-errors": _vm.hasErrors && _vm.showFieldErrors, "data-type": _vm.type } }, @@ -929,7 +1189,9 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { }) : _vm._e(), _vm._v(" "), - _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + _vm.showFieldErrors + ? _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + : _vm._e() ], 1 ) @@ -972,7 +1234,8 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { provide: function provide () { return { formulateFormSetter: this.setFieldValue, - formulateFormRegister: this.register + formulateFormRegister: this.register, + getFormValues: this.getFormValues } }, name: 'FormulateForm', @@ -1054,6 +1317,9 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { formSubmitted: function formSubmitted () { // perform validation here this.$emit('submit', this.formModel); + }, + getFormValues: function getFormValues () { + return this.internalFormModelProxy } } }; @@ -1382,6 +1648,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1430,6 +1697,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { checked: _vm._q(_vm.context.model, _vm.context.value) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", _vm.context.value) } @@ -1458,6 +1726,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1552,6 +1821,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1597,6 +1867,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { attrs: { type: "radio" }, domProps: { checked: _vm._q(_vm.context.model, null) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", null) } @@ -1622,6 +1893,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { attrs: { type: _vm.type }, domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1714,6 +1986,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { ], attrs: { "data-placeholder-selected": _vm.placeholderSelected }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$selectedVal = Array.prototype.filter .call($event.target.options, function(o) { @@ -1866,6 +2139,7 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { ], domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1930,7 +2204,11 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { FormulateInputTextArea: FormulateInputTextArea }, library: library, - rules: rules + rules: rules, + locale: 'en', + locales: { + en: en + } }; }; @@ -2000,6 +2278,20 @@ var Formulate = (function (exports, isUrl, isPlainObject, nanoid) { return this.options.rules }; + /** + * Get the validation message for a particular error. + */ + Formulate.prototype.validationMessage = function validationMessage (rule, validationContext) { + var generators = this.options.locales[this.options.locale]; + if (generators.hasOwnProperty(rule)) { + return generators[rule](validationContext) + } + if (generators.hasOwnProperty('default')) { + return generators.default(validationContext) + } + return 'This field does not have a valid value' + }; + var Formulate$1 = new Formulate(); exports.default = Formulate$1; diff --git a/dist/formulate.umd.js b/dist/formulate.umd.js index 15cd246..033a5d0 100644 --- a/dist/formulate.umd.js +++ b/dist/formulate.umd.js @@ -167,6 +167,17 @@ return [] } + /** + * How to add an item. + * @param {string} item + */ + function sentence (item) { + if (typeof item === 'string') { + return item[0].toUpperCase() + item.substr(1) + } + return item + } + /** * Given an array or string return an array of callables. * @param {array|string} validation @@ -193,6 +204,7 @@ return [rule, []] } if (Array.isArray(rule) && rule.length) { + rule = rule.map(function (r) { return r; }); // light clone if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) { return [rules[rule.shift()], rule] } @@ -223,88 +235,6 @@ 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 */ @@ -316,6 +246,34 @@ return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp)) }, + /** + * Rule: checks if the value is only alpha + */ + alpha: function (value, set) { + if ( set === void 0 ) set = 'default'; + + var sets = { + default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, + latin: /^[a-zA-Z]+$/ + }; + var selectedSet = sets.hasOwnProperty(set) ? set : 'default'; + return Promise.resolve(sets[selectedSet].test(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 a value is after a given date. Defaults to current time */ @@ -327,45 +285,13 @@ 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) { + if ( from === void 0 ) from = 0; + if ( to === void 0 ) to = 10; + return Promise.resolve((function () { if (from === null || to === null || isNaN(from) || isNaN(to)) { return false @@ -383,6 +309,13 @@ })()) }, + /** + * Rule: ensures the value is a date according to Date.parse() + */ + date: function (value) { + return Promise.resolve(!isNaN(Date.parse(value))) + }, + /** * Rule: tests */ @@ -392,6 +325,58 @@ return Promise.resolve(isEmail.test(value)) }, + /** + * 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: 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 + })) + }, + + /** + * Check the maximum value of a particular. + */ + max: function (value, minimum) { + if ( minimum === void 0 ) minimum = 10; + + 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 file type is correct. */ @@ -415,6 +400,8 @@ * Check the minimum value of a particular. */ min: function (value, minimum) { + if ( minimum === void 0 ) minimum = 1; + return Promise.resolve((function () { minimum = Number(minimum); if (!isNaN(value)) { @@ -432,23 +419,243 @@ }, /** - * Check the minimum value of a particular. + * Rule: Value is not in stack. */ - max: function (value, minimum) { - return Promise.resolve((function () { - minimum = Number(minimum); - if (!isNaN(value)) { - value = Number(value); - return value <= minimum + 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) } - if (typeof value === 'string') { - return value.length <= minimum + return item === value + }) === undefined) + }, + + /** + * Rule: checks if the value is only alpha numeric + */ + number: function (value) { + return Promise.resolve(!isNaN(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 <= minimum + return !!value.length } - return false + if (typeof value === 'string') { + return !!value + } + if (typeof value === 'object') { + return (!value) ? false : !!Object.keys(value).length + } + return true })()) + }, + + /** + * Rule: checks if a string is a valid url + */ + url: function (value) { + return Promise.resolve(isUrl(value)) + } + }; + + /** + * Validation error message generators. + */ + var en = { + + /** + * Valid accepted value. + */ + accepted: function (ref) { + var name = ref.name; + + return ("Please accept the " + name + ".") + }, + + /** + * The date is not after. + */ + after: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be after " + (args[0]) + ".") + } + return ((sentence(name)) + " must be a later date.") + }, + + /** + * The value is not a letter. + */ + alpha: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain alphabetical characters.") + }, + + /** + * Rule: checks if the value is alpha numeric + */ + alphanumeric: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " can only contain letters and numbers.") + }, + + /** + * The date is not before. + */ + before: function (ref) { + var name = ref.name; + var args = ref.args; + + if (Array.isArray(args) && args.length) { + return ((sentence(name)) + " must be before " + (args[0]) + ".") + } + return ((sentence(name)) + " must be an earlier date.") + }, + + /** + * The value is not between two numbers or lengths + */ + between: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + ".") + } + return ((sentence(name)) + " must be between " + (args[0]) + " and " + (args[1]) + " characters long.") + }, + + /** + * Is not a valid date. + */ + date: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not a valid date.") + }, + + /** + * The default render method for error messages. + */ + default: function (ref) { + var name = ref.name; + + return "This field isn’t valid." + }, + + /** + * Is not a valid email address. + */ + email: function (ref) { + var name = ref.name; + var value = ref.value; + + return (value + " is not a valid email address.") + }, + + /** + * Value is an allowed value. + */ + in: function (ref) { + var name = ref.name; + var value = ref.value; + + if (typeof value === 'string') { + return ("“" + (sentence(value)) + "” is not an allowed " + name + ".") + } + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * Value is not a match. + */ + matches: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is not an allowed value.") + }, + + /** + * The maximum value allowed. + */ + max: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be less than " + (args[0]) + ".") + } + return (name + " must be less than " + (args[0]) + " characters long.") + }, + + /** + * The maximum value allowed. + */ + min: function (ref) { + var name = ref.name; + var value = ref.value; + var args = ref.args; + + if (!isNaN(value)) { + return (name + " must be more than " + (args[0]) + ".") + } + return (name + " must be more than " + (args[0]) + " characters long.") + }, + + /** + * The field is not an allowed value + */ + not: function (ref) { + var name = ref.name; + var value = ref.value; + + return ("“" + value + "” is not an allowed " + name + ".") + }, + + /** + * The field is not a number + */ + number: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " must be a number.") + }, + + /** + * Required field. + */ + required: function (ref) { + var name = ref.name; + + return ((sentence(name)) + " is required.") + }, + + /** + * Value is not a url. + */ + url: function (ref) { + var name = ref.name; + + return "Please include a valid url." } }; @@ -459,9 +666,6 @@ */ var context = { context: function context () { - if (this.debug) { - console.log(((this.type) + " re-context")); - } return defineModel.call(this, Object.assign({}, {type: this.type, value: this.value, name: this.nameOrFallback, @@ -470,7 +674,8 @@ id: this.id || this.defaultId, label: this.label, labelPosition: this.logicalLabelPosition, - attributes: this.elementAttributes}, + attributes: this.elementAttributes, + blurHandler: blurHandler.bind(this)}, this.typeContext)) }, nameOrFallback: nameOrFallback, @@ -479,7 +684,9 @@ logicalLabelPosition: logicalLabelPosition, isVmodeled: isVmodeled, mergedErrors: mergedErrors, - hasErrors: hasErrors + hasErrors: hasErrors, + showFieldErrors: showFieldErrors, + mergedValidationName: mergedValidationName }; /** @@ -539,6 +746,33 @@ } } + /** + * The validation label to use. + */ + function mergedValidationName () { + if (this.validationName) { + return this.validationName + } + if (typeof this.name === 'string') { + return this.name + } + if (this.label) { + return this.label + } + return this.type + } + + /** + * Determines if the field should show it's error (if it has one) + * @return {boolean} + */ + function showFieldErrors () { + if (this.showErrors) { + return this.showErrors + } + return this.behavioralErrorVisibility + } + /** * Return the element’s name, or select a fallback. */ @@ -599,6 +833,15 @@ return !!this.mergedErrors.length } + /** + * Bound into the context object. + */ + function blurHandler () { + if (this.errorBehavior === 'blur') { + this.behavioralErrorVisibility = true; + } + } + /** * Defines the model used throughout the existing context. * @param {object} context @@ -642,7 +885,8 @@ inheritAttrs: false, inject: { formulateFormSetter: { default: undefined }, - formulateFormRegister: { default: undefined } + formulateFormRegister: { default: undefined }, + getFormValues: { default: function () { return function () { return ({}); }; } } }, model: { prop: 'formulateValue', @@ -701,15 +945,23 @@ type: [String, Boolean, Array], default: false }, - validationBehavior: { + validationName: { + type: [String, Boolean], + default: false + }, + error: { + type: [String, Boolean], + default: false + }, + errorBehavior: { type: String, default: 'blur', validator: function (value) { return ['blur', 'live'].includes(value) } }, - error: { - type: [String, Boolean], + showErrors: { + type: Boolean, default: false } }, @@ -718,6 +970,7 @@ defaultId: nanoid(9), localAttributes: {}, internalModelProxy: this.formulateValue, + behavioralErrorVisibility: (this.errorBehavior === 'live'), validationErrors: [] } }, @@ -771,7 +1024,13 @@ 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 (res) { return res ? false : this$1.$formulate.validationMessage(rule.name, { + args: args, + name: this$1.mergedValidationName, + value: this$1.context.model, + vm: this$1, + formValues: this$1.getFormValues() + }); }) }) ) .then(function (result) { return result.filter(function (result) { return result; }); }) @@ -870,6 +1129,7 @@ attrs: { "data-classification": _vm.classification, "data-has-errors": _vm.hasErrors, + "data-is-showing-errors": _vm.hasErrors && _vm.showFieldErrors, "data-type": _vm.type } }, @@ -932,7 +1192,9 @@ }) : _vm._e(), _vm._v(" "), - _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + _vm.showFieldErrors + ? _c("FormulateInputErrors", { attrs: { errors: _vm.mergedErrors } }) + : _vm._e() ], 1 ) @@ -975,7 +1237,8 @@ provide: function provide () { return { formulateFormSetter: this.setFieldValue, - formulateFormRegister: this.register + formulateFormRegister: this.register, + getFormValues: this.getFormValues } }, name: 'FormulateForm', @@ -1057,6 +1320,9 @@ formSubmitted: function formSubmitted () { // perform validation here this.$emit('submit', this.formModel); + }, + getFormValues: function getFormValues () { + return this.internalFormModelProxy } } }; @@ -1385,6 +1651,7 @@ : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1433,6 +1700,7 @@ checked: _vm._q(_vm.context.model, _vm.context.value) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", _vm.context.value) } @@ -1461,6 +1729,7 @@ value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1555,6 +1824,7 @@ : _vm.context.model }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$a = _vm.context.model, $$el = $event.target, @@ -1600,6 +1870,7 @@ attrs: { type: "radio" }, domProps: { checked: _vm._q(_vm.context.model, null) }, on: { + blur: _vm.context.blurHandler, change: function($event) { return _vm.$set(_vm.context, "model", null) } @@ -1625,6 +1896,7 @@ attrs: { type: _vm.type }, domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1717,6 +1989,7 @@ ], attrs: { "data-placeholder-selected": _vm.placeholderSelected }, on: { + blur: _vm.context.blurHandler, change: function($event) { var $$selectedVal = Array.prototype.filter .call($event.target.options, function(o) { @@ -1869,6 +2142,7 @@ ], domProps: { value: _vm.context.model }, on: { + blur: _vm.context.blurHandler, input: function($event) { if ($event.target.composing) { return @@ -1933,7 +2207,11 @@ FormulateInputTextArea: FormulateInputTextArea }, library: library, - rules: rules + rules: rules, + locale: 'en', + locales: { + en: en + } }; }; @@ -2003,6 +2281,20 @@ return this.options.rules }; + /** + * Get the validation message for a particular error. + */ + Formulate.prototype.validationMessage = function validationMessage (rule, validationContext) { + var generators = this.options.locales[this.options.locale]; + if (generators.hasOwnProperty(rule)) { + return generators[rule](validationContext) + } + if (generators.hasOwnProperty('default')) { + return generators.default(validationContext) + } + return 'This field does not have a valid value' + }; + var Formulate$1 = new Formulate(); exports.default = Formulate$1; diff --git a/src/Formulate.js b/src/Formulate.js index e38728b..6d09776 100644 --- a/src/Formulate.js +++ b/src/Formulate.js @@ -1,5 +1,6 @@ import library from './libs/library' import rules from './libs/rules' +import en from './locales/en' import isPlainObject from 'is-plain-object' import FormulateInput from './FormulateInput.vue' import FormulateForm from './FormulateForm.vue' @@ -30,7 +31,11 @@ class Formulate { FormulateInputTextArea }, library, - rules + rules, + locale: 'en', + locales: { + en + } } } @@ -99,6 +104,20 @@ class Formulate { rules () { return this.options.rules } + + /** + * Get the validation message for a particular error. + */ + validationMessage (rule, validationContext) { + const generators = this.options.locales[this.options.locale] + if (generators.hasOwnProperty(rule)) { + return generators[rule](validationContext) + } + if (generators.hasOwnProperty('default')) { + return generators.default(validationContext) + } + return 'This field does not have a valid value' + } } export default new Formulate() diff --git a/src/FormulateForm.vue b/src/FormulateForm.vue index 3483e71..429cabe 100644 --- a/src/FormulateForm.vue +++ b/src/FormulateForm.vue @@ -13,7 +13,8 @@ export default { provide () { return { formulateFormSetter: this.setFieldValue, - formulateFormRegister: this.register + formulateFormRegister: this.register, + getFormValues: this.getFormValues } }, name: 'FormulateForm', @@ -93,6 +94,9 @@ export default { formSubmitted () { // perform validation here this.$emit('submit', this.formModel) + }, + getFormValues () { + return this.internalFormModelProxy } } } diff --git a/src/FormulateInput.vue b/src/FormulateInput.vue index 5d32636..ad24005 100644 --- a/src/FormulateInput.vue +++ b/src/FormulateInput.vue @@ -3,6 +3,7 @@ class="formulate-input" :data-classification="classification" :data-has-errors="hasErrors" + :data-is-showing-errors="hasErrors && showFieldErrors" :data-type="type" >
@@ -41,6 +42,7 @@ v-text="help" />
@@ -56,7 +58,8 @@ export default { inheritAttrs: false, inject: { formulateFormSetter: { default: undefined }, - formulateFormRegister: { default: undefined } + formulateFormRegister: { default: undefined }, + getFormValues: { default: () => () => ({}) } }, model: { prop: 'formulateValue', @@ -115,15 +118,23 @@ export default { type: [String, Boolean, Array], default: false }, - validationBehavior: { + validationName: { + type: [String, Boolean], + default: false + }, + error: { + type: [String, Boolean], + default: false + }, + errorBehavior: { type: String, default: 'blur', validator: function (value) { return ['blur', 'live'].includes(value) } }, - error: { - type: [String, Boolean], + showErrors: { + type: Boolean, default: false } }, @@ -132,6 +143,7 @@ export default { defaultId: nanoid(9), localAttributes: {}, internalModelProxy: this.formulateValue, + behavioralErrorVisibility: (this.errorBehavior === 'live'), validationErrors: [] } }, @@ -182,7 +194,13 @@ export default { Promise.all( rules.map(([rule, args]) => { return rule(this.context.model, ...args) - .then(res => res ? false : 'Validation error!') + .then(res => res ? false : this.$formulate.validationMessage(rule.name, { + args, + name: this.mergedValidationName, + value: this.context.model, + vm: this, + formValues: this.getFormValues() + })) }) ) .then(result => result.filter(result => result)) diff --git a/src/inputs/FormulateInputBox.vue b/src/inputs/FormulateInputBox.vue index a49d756..18e9c18 100644 --- a/src/inputs/FormulateInputBox.vue +++ b/src/inputs/FormulateInputBox.vue @@ -8,6 +8,7 @@ :type="type" :value="context.value" v-bind="attributes" + @blur="context.blurHandler" >