commit
2456aead9e
@ -1,20 +1,23 @@
|
||||
import commonjs from '@rollup/plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import buble from '@rollup/plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import autoExternal from 'rollup-plugin-auto-external'
|
||||
import commonjs from 'rollup-plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import buble from 'rollup-plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
export default {
|
||||
input: 'src/Formulate.js', // Path relative to package.json
|
||||
output: {
|
||||
output: [
|
||||
{
|
||||
name: 'Formulate',
|
||||
exports: 'default',
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl'
|
||||
'is-url': 'isUrl',
|
||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
external: ['nanoid/non-secure'],
|
||||
plugins: [
|
||||
commonjs(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import resolve from '@rollup/plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import commonjs from '@rollup/plugin-commonjs' // Convert CommonJS modules to ES6
|
||||
import buble from '@rollup/plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import vue from 'rollup-plugin-vue' // Handle .vue SFC files
|
||||
import buble from 'rollup-plugin-buble' // Transpile/polyfill with reasonable browser support
|
||||
import internal from 'rollup-plugin-internal'
|
||||
import { terser } from 'rollup-plugin-terser'
|
||||
|
||||
@ -14,7 +14,8 @@ export default {
|
||||
globals: {
|
||||
'is-plain-object': 'isPlainObject',
|
||||
'nanoid/non-secure': 'nanoid',
|
||||
'is-url': 'isUrl'
|
||||
'is-url': 'isUrl',
|
||||
'@braid/vue-formulate-i18n': 'VueFormulateI18n'
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
@ -23,7 +24,7 @@ export default {
|
||||
preferBuiltins: false
|
||||
}),
|
||||
commonjs(),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url']),
|
||||
internal(['is-plain-object', 'nanoid/non-secure', 'is-url', '@braid/vue-formulate-i18n']),
|
||||
vue({
|
||||
css: true, // Dynamically inject css as a <style> tag
|
||||
compileTemplate: true // Explicitly convert template to render function
|
||||
|
2
dist/formulate.esm.js
vendored
2
dist/formulate.esm.js
vendored
File diff suppressed because one or more lines are too long
6
dist/formulate.min.js
vendored
6
dist/formulate.min.js
vendored
File diff suppressed because one or more lines are too long
1
dist/formulate.min.js.map
vendored
1
dist/formulate.min.js.map
vendored
@ -1 +0,0 @@
|
||||
{"version":3,"sources":["../node_modules/isobject/index.js","../node_modules/is-plain-object/index.js","Formulate.js"],"names":["isObject","val","Array","isArray","isObjectObject","o","Object","prototype","toString","call","isPlainObject","ctor","prot","constructor","hasOwnProperty","require","FormulateInput","Formulate","defaults","components","install","Vue","options","componentName","$formulate","extend","component","base","extendWith","merged","key","prop","module","exports"],"mappings":";AASC,aAFc,SAASA,EAASC,GACxBA,OAAO,MAAPA,GAA8B,iBAARA,IAA2C,IAAvBC,MAAMC,QAAQF,GAChE,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,QAAA;;ACyBA,aAAA,OAAA,eAAA,QAAA,aAAA,CAAA,OAAA,IAAA,QAAA,QAAA,EA3BD,IAAA,EAAA,EAAA,QAAA,aA2BC,SAAA,EAAA,GAAA,OAAA,GAAA,EAAA,WAAA,EAAA,CAAA,QAAA,GAzBD,SAASG,EAAeC,GACf,OAAgB,KAAhB,EAASA,EAAAA,SAAAA,IAC2B,oBAAtCC,OAAOC,UAAUC,SAASC,KAAKJ,GAGvB,SAASK,EAAcL,GAChCM,IAAAA,EAAKC,EAELR,OAAsB,IAAtBA,EAAeC,KAIC,mBADpBM,EAAON,EAAEQ,gBAKoB,IAAzBT,EADJQ,EAAOD,EAAKJ,aAIiC,IAAzCK,EAAKE,eAAe;;;;;AC5B1B,IAAIJ,EAAgBK,QAAQ,mBACxBC,EAAiBD,QAAQ,wBAKzBE,EAAY,WACTC,KAAAA,SAAW,CACdC,WAAY,CAEVH,eAAgBA,KAQtBC,EAAUV,UAAUa,QAAU,SAAUC,EAAKC,GAGtC,IAAA,IAAIC,KAFTF,EAAId,UAAUiB,WAAa,KACtBF,KAAAA,QAAU,KAAKG,OAAO,KAAKP,SAAUI,GAAW,IAC3B,KAAKA,QAAQH,WACrCE,EAAIK,UAAUH,EAAe,KAAKD,QAAQH,WAAWI,KASzDN,EAAUV,UAAUkB,OAAS,SAAUE,EAAMC,GACvCC,IAAAA,EAAS,GACR,IAAA,IAAIC,KAAOH,EACVC,EAAWd,eAAegB,GAC5BD,EAAOC,GAAOpB,EAAckB,EAAWE,KAASpB,EAAciB,EAAKG,IAC/D,KAAKL,OAAOE,EAAKG,GAAMF,EAAWE,IAClCF,EAAWE,GAEfD,EAAOC,GAAOH,EAAKG,GAGlB,IAAA,IAAIC,KAAQH,EACVC,EAAOf,eAAeiB,KACzBF,EAAOE,GAAQH,EAAWG,IAGvBF,OAAAA,GAGTG,OAAOC,QAAU,IAAIhB","file":"formulate.min.js","sourceRoot":"../src","sourcesContent":["/*!\n * isobject <https://github.com/jonschlinkert/isobject>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nexport default function isObject(val) {\n return val != null && typeof val === 'object' && Array.isArray(val) === false;\n};\n","/*!\n * is-plain-object <https://github.com/jonschlinkert/is-plain-object>\n *\n * Copyright (c) 2014-2017, Jon Schlinkert.\n * Released under the MIT License.\n */\n\nimport isObject from 'isobject';\n\nfunction isObjectObject(o) {\n return isObject(o) === true\n && Object.prototype.toString.call(o) === '[object Object]';\n}\n\nexport default function isPlainObject(o) {\n var ctor,prot;\n\n if (isObjectObject(o) === false) return false;\n\n // If has modified constructor\n ctor = o.constructor;\n if (typeof ctor !== 'function') return false;\n\n // If has modified prototype\n prot = ctor.prototype;\n if (isObjectObject(prot) === false) return false;\n\n // If constructor does not have an Object-specific method\n if (prot.hasOwnProperty('isPrototypeOf') === false) {\n return false;\n }\n\n // Most likely a plain Object\n return true;\n};\n","var isPlainObject = require('is-plain-object')\nvar FormulateInput = require('./FormulateInput.vue')\n\n/**\n * The base formulate libary.\n */\nvar Formulate = function () {\n this.defaults = {\n components: {\n // FormulateForm: FormulateForm,\n FormulateInput: FormulateInput\n }\n }\n}\n\n/**\n * Install vue formulate, and register it’s components.\n */\nFormulate.prototype.install = function (Vue, options) {\n Vue.prototype.$formulate = this\n this.options = this.extend(this.defaults, options || {})\n for (var componentName in this.options.components) {\n Vue.component(componentName, this.options.components[componentName])\n }\n}\n\n/**\n * Create a new object by copying properties of base and extendWith.\n * @param {Object} base\n * @param {Object} extendWith\n */\nFormulate.prototype.extend = function (base, extendWith) {\n var merged = {}\n for (var key in base) {\n if (extendWith.hasOwnProperty(key)) {\n merged[key] = isPlainObject(extendWith[key]) && isPlainObject(base[key])\n ? this.extend(base[key], extendWith[key])\n : extendWith[key]\n } else {\n merged[key] = base[key]\n }\n }\n for (var prop in extendWith) {\n if (!merged.hasOwnProperty(prop)) {\n merged[prop] = extendWith[prop]\n }\n }\n return merged\n}\n\nmodule.exports = new Formulate()\n"]}
|
2
dist/formulate.umd.js
vendored
2
dist/formulate.umd.js
vendored
File diff suppressed because one or more lines are too long
15
dist/snow.css
vendored
15
dist/snow.css
vendored
@ -497,3 +497,18 @@
|
||||
transition: all .25s; }
|
||||
.formulate-input[data-classification="file"] [data-type="image"] .formulate-input-upload-area .formulate-input-upload-area-mask::before {
|
||||
mask-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 90 71.05"><path d="M82.89,0H7.1A7.12,7.12,0,0,0,0,7.11V64a7.11,7.11,0,0,0,7.1,7.1H82.9A7.11,7.11,0,0,0,90,64V7.11A7.12,7.12,0,0,0,82.89,0ZM69.28,39.35a5.44,5.44,0,0,0-8,0L50.58,50.74,32.38,30.88a5.31,5.31,0,0,0-7.92,0L4.74,52.4V7.11A2.37,2.37,0,0,1,7.11,4.74H82.9a2.37,2.37,0,0,1,2.36,2.37V56.3Z"/><circle cx="67.74" cy="22.26" r="8.53"/></svg>'); }
|
||||
|
||||
.formulate-form-errors {
|
||||
margin: .75em 0;
|
||||
padding: 0;
|
||||
list-style-type: none; }
|
||||
.formulate-form-errors:first-child {
|
||||
margin-top: 0; }
|
||||
.formulate-form-errors:last-child {
|
||||
margin-bottom: 0; }
|
||||
.formulate-form-errors .formulate-form-error {
|
||||
color: #960505;
|
||||
font-size: .9em;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
margin-bottom: .25em; }
|
||||
|
2
dist/snow.min.css
vendored
2
dist/snow.min.css
vendored
File diff suppressed because one or more lines are too long
1013
package-lock.json
generated
1013
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
20
package.json
20
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@braid/vue-formulate",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"description": "The easiest way to build forms in Vue.",
|
||||
"main": "dist/formulate.umd.js",
|
||||
"module": "dist/formulate.esm.js",
|
||||
@ -15,14 +15,15 @@
|
||||
"./sfc": "src/Formulate.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:umd & npm run build:es & npm run build:unpkg & npm run build:css",
|
||||
"build:css": "node-sass themes/snow/snow.scss dist/snow.css && postcss --use autoprefixer -b '> 2%' < dist/snow.css | postcss --no-map --use cssnano > dist/snow.min.css",
|
||||
"build": "npm run build:esm & npm run build:umd & npm run build:iife & npm run build:css & wait && echo \"Build complete:\nesm: $(gzip -c dist/formulate.esm.js | wc -c)b gzip\numd: $(gzip -c dist/formulate.umd.js | wc -c)b gzip\nmin: $(gzip -c dist/formulate.min.js | wc -c)b gzip\"",
|
||||
"build:esm": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
|
||||
"build:umd": "rollup --config build/rollup.config.js --format umd --file dist/formulate.umd.js",
|
||||
"build:es": "rollup --config build/rollup.config.js --format es --file dist/formulate.esm.js",
|
||||
"build:unpkg": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulate.min.js",
|
||||
"build:iife": "rollup --config build/rollup.iife.config.js --format iife --file dist/formulate.min.js",
|
||||
"build:css": "node-sass themes/snow/snow.scss dist/snow.css && postcss --use autoprefixer -b '> 2%' < dist/snow.css | postcss --no-map --use cssnano > dist/snow.min.css",
|
||||
"test": "NODE_ENV=test jest --config test/jest.conf.js",
|
||||
"test:watch": "NODE_ENV=test jest --config test/jest.conf.js --watch",
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage"
|
||||
"test:coverage": "NODE_ENV=test jest --config test/jest.conf.js --coverage",
|
||||
"build:size": "gzip -c dist/formulate.esm.js | wc -c"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -46,6 +47,8 @@
|
||||
"@babel/core": "^7.8.4",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.8.3",
|
||||
"@babel/preset-env": "^7.8.4",
|
||||
"@rollup/plugin-buble": "^0.21.1",
|
||||
"@rollup/plugin-commonjs": "^11.0.2",
|
||||
"@rollup/plugin-node-resolve": "^7.1.1",
|
||||
"@vue/component-compiler-utils": "^3.1.1",
|
||||
"@vue/test-utils": "^1.0.0-beta.31",
|
||||
@ -54,6 +57,7 @@
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.9.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"cypress": "^4.1.0",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.20.1",
|
||||
@ -69,9 +73,8 @@
|
||||
"postcss-cli": "^6.1.3",
|
||||
"rollup": "^1.31.1",
|
||||
"rollup-plugin-auto-external": "^2.0.0",
|
||||
"rollup-plugin-buble": "^0.19.8",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-internal": "^1.0.4",
|
||||
"rollup-plugin-multi-input": "^1.1.1",
|
||||
"rollup-plugin-terser": "^5.2.0",
|
||||
"rollup-plugin-vue": "^5.1.6",
|
||||
"typescript": "^3.8.2",
|
||||
@ -83,6 +86,7 @@
|
||||
"watch": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate-i18n": "^1.0.3",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^2.1.11"
|
||||
|
@ -1,5 +1,4 @@
|
||||
import nanoid from 'nanoid/non-secure'
|
||||
import mimes from './libs/mimes'
|
||||
|
||||
/**
|
||||
* The file upload class holds and represents a file’s upload state durring
|
||||
@ -11,11 +10,11 @@ class FileUpload {
|
||||
* @param {FileList} fileList
|
||||
* @param {object} context
|
||||
*/
|
||||
constructor (input, context, options) {
|
||||
constructor (input, context, options = {}) {
|
||||
this.input = input
|
||||
this.fileList = input.files
|
||||
this.files = []
|
||||
this.options = options
|
||||
this.options = { ...{ mimes: {} }, ...options }
|
||||
this.results = false
|
||||
this.context = context
|
||||
if (Array.isArray(this.fileList)) {
|
||||
@ -35,7 +34,7 @@ class FileUpload {
|
||||
const key = this.options ? this.options.fileUrlKey : 'url'
|
||||
const url = item[key]
|
||||
const ext = (url && url.lastIndexOf('.') !== -1) ? url.substr(url.lastIndexOf('.') + 1) : false
|
||||
const mime = mimes[ext] || false
|
||||
const mime = this.options.mimes[ext] || false
|
||||
fileList.push(Object.assign({}, item, url ? {
|
||||
name: url.substr((url.lastIndexOf('/') + 1) || 0),
|
||||
type: item.type ? item.type : mime,
|
||||
|
112
src/Formulate.js
112
src/Formulate.js
@ -1,12 +1,14 @@
|
||||
import library from './libs/library'
|
||||
import rules from './libs/rules'
|
||||
import en from './locales/en'
|
||||
import mimes from './libs/mimes'
|
||||
import FileUpload from './FileUpload'
|
||||
import { arrayify, parseLocale } from './libs/utils'
|
||||
import isPlainObject from 'is-plain-object'
|
||||
import { en } from '@braid/vue-formulate-i18n'
|
||||
import fauxUploader from './libs/faux-uploader'
|
||||
import FormulateInput from './FormulateInput.vue'
|
||||
import FormulateForm from './FormulateForm.vue'
|
||||
import FormulateInputErrors from './FormulateInputErrors.vue'
|
||||
import FormulateErrors from './FormulateErrors.vue'
|
||||
import FormulateInputGroup from './FormulateInputGroup.vue'
|
||||
import FormulateInputBox from './inputs/FormulateInputBox.vue'
|
||||
import FormulateInputText from './inputs/FormulateInputText.vue'
|
||||
@ -29,7 +31,7 @@ class Formulate {
|
||||
components: {
|
||||
FormulateForm,
|
||||
FormulateInput,
|
||||
FormulateInputErrors,
|
||||
FormulateErrors,
|
||||
FormulateInputBox,
|
||||
FormulateInputText,
|
||||
FormulateInputFile,
|
||||
@ -41,16 +43,17 @@ class Formulate {
|
||||
},
|
||||
library,
|
||||
rules,
|
||||
locale: 'en',
|
||||
mimes,
|
||||
locale: false,
|
||||
uploader: fauxUploader,
|
||||
uploadUrl: false,
|
||||
fileUrlKey: 'url',
|
||||
uploadJustCompleteDuration: 1000,
|
||||
plugins: [],
|
||||
locales: {
|
||||
en
|
||||
}
|
||||
errorHandler: (err) => err,
|
||||
plugins: [ en ],
|
||||
locales: {}
|
||||
}
|
||||
this.registry = new Map()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -58,11 +61,13 @@ class Formulate {
|
||||
*/
|
||||
install (Vue, options) {
|
||||
Vue.prototype.$formulate = this
|
||||
this.options = this.merge(this.defaults, options || {})
|
||||
if (Array.isArray(this.options.plugins) && this.options.plugins.length) {
|
||||
this.options.plugins
|
||||
.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.options = this.defaults
|
||||
var plugins = this.defaults.plugins
|
||||
if (options && Array.isArray(options.plugins) && options.plugins.length) {
|
||||
plugins = plugins.concat(options.plugins)
|
||||
}
|
||||
plugins.forEach(plugin => (typeof plugin === 'function') ? plugin(this) : null)
|
||||
this.extend(options || {})
|
||||
for (var componentName in this.options.components) {
|
||||
Vue.component(componentName, this.options.components[componentName])
|
||||
}
|
||||
@ -141,11 +146,47 @@ class Formulate {
|
||||
return { ...this.options.rules, ...rules }
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to get the vue-i18n configured locale.
|
||||
*/
|
||||
i18n (vm) {
|
||||
if (vm.$i18n && vm.$i18n.locale) {
|
||||
return vm.$i18n.locale
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the proper locale to use.
|
||||
*/
|
||||
getLocale (vm) {
|
||||
if (!this.selectedLocale) {
|
||||
this.selectedLocale = [
|
||||
this.options.locale,
|
||||
this.i18n(vm),
|
||||
'en'
|
||||
].reduce((selection, locale) => {
|
||||
if (selection) {
|
||||
return selection
|
||||
}
|
||||
if (locale) {
|
||||
const option = parseLocale(locale)
|
||||
.find(locale => Object.prototype.hasOwnProperty.call(this.options.locales, locale))
|
||||
if (option) {
|
||||
selection = option
|
||||
}
|
||||
}
|
||||
return selection
|
||||
}, false)
|
||||
}
|
||||
return this.selectedLocale
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message for a particular error.
|
||||
*/
|
||||
validationMessage (rule, validationContext) {
|
||||
const generators = this.options.locales[this.options.locale]
|
||||
validationMessage (rule, validationContext, vm) {
|
||||
const generators = this.options.locales[this.getLocale(vm)]
|
||||
if (generators.hasOwnProperty(rule)) {
|
||||
return generators[rule](validationContext)
|
||||
} else if (rule[0] === '_' && generators.hasOwnProperty(rule.substr(1))) {
|
||||
@ -157,6 +198,49 @@ class Formulate {
|
||||
return 'This field does not have a valid value'
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a FormulateForm register it.
|
||||
* @param {vm} form
|
||||
*/
|
||||
register (form) {
|
||||
if (form.$options.name === 'FormulateForm' && form.name) {
|
||||
this.registry.set(form.name, form)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an instance of a form, remove it from the registry.
|
||||
* @param {vm} form
|
||||
*/
|
||||
deregister (form) {
|
||||
if (
|
||||
form.$options.name === 'FormulateForm' &&
|
||||
form.name &&
|
||||
this.registry.has(form.name)
|
||||
) {
|
||||
this.registry.delete(form.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array, this function will attempt to make sense of the given error
|
||||
* and hydrate a form with the resulting errors.
|
||||
*
|
||||
* @param {error} err
|
||||
* @param {string} formName
|
||||
* @param {error}
|
||||
*/
|
||||
handle (err, formName, skip = false) {
|
||||
const e = skip ? err : this.options.errorHandler(err)
|
||||
if (formName && this.registry.has(formName)) {
|
||||
this.registry.get(formName).applyErrors({
|
||||
formErrors: arrayify(e.formErrors),
|
||||
inputErrors: e.inputErrors || {}
|
||||
})
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file uploader.
|
||||
*/
|
||||
|
83
src/FormulateErrors.vue
Normal file
83
src/FormulateErrors.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="visibleErrors.length"
|
||||
:class="`formulate-${type}-errors`"
|
||||
>
|
||||
<li
|
||||
v-for="error in visibleErrors"
|
||||
:key="error"
|
||||
:class="`formulate-${type}-error`"
|
||||
v-text="error"
|
||||
/>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { arrayify } from './libs/utils'
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
observeErrors: {
|
||||
default: false
|
||||
},
|
||||
removeErrorObserver: {
|
||||
default: false
|
||||
}
|
||||
},
|
||||
props: {
|
||||
showValidationErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
errors: {
|
||||
type: [Array, Boolean],
|
||||
default: false
|
||||
},
|
||||
validationErrors: {
|
||||
type: [Array],
|
||||
default: () => ([])
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'form'
|
||||
},
|
||||
preventRegistration: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
fieldName: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
boundSetErrors: this.setErrors.bind(this),
|
||||
localErrors: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mergedErrors () {
|
||||
return arrayify(this.errors).concat(this.localErrors)
|
||||
},
|
||||
visibleErrors () {
|
||||
return Array.from(new Set(this.mergedErrors.concat(this.showValidationErrors ? this.validationErrors : [])))
|
||||
}
|
||||
},
|
||||
created () {
|
||||
if (!this.preventRegistration && typeof this.observeErrors === 'function' && (this.type === 'form' || this.fieldName)) {
|
||||
this.observeErrors({ callback: this.boundSetErrors, type: this.type, field: this.fieldName })
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
if (!this.preventRegistration && typeof this.removeErrorObserver === 'function') {
|
||||
this.removeErrorObserver(this.boundSetErrors)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setErrors (errors) {
|
||||
this.localErrors = arrayify(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,13 +1,20 @@
|
||||
<template>
|
||||
<form
|
||||
:class="classes"
|
||||
@submit.prevent="formSubmitted"
|
||||
>
|
||||
<FormulateErrors
|
||||
v-if="!hasFormErrorObservers"
|
||||
type="form"
|
||||
:errors="mergedFormErrors"
|
||||
:prevent-registration="true"
|
||||
/>
|
||||
<slot />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { shallowEqualObjects } from './libs/utils'
|
||||
import { shallowEqualObjects, arrayify } from './libs/utils'
|
||||
import FormSubmission from './FormSubmission'
|
||||
|
||||
export default {
|
||||
@ -15,7 +22,9 @@ export default {
|
||||
return {
|
||||
formulateFormSetter: this.setFieldValue,
|
||||
formulateFormRegister: this.register,
|
||||
getFormValues: this.getFormValues
|
||||
getFormValues: this.getFormValues,
|
||||
observeErrors: this.addErrorObserver,
|
||||
removeErrorObserver: this.removeErrorObserver
|
||||
}
|
||||
},
|
||||
name: 'FormulateForm',
|
||||
@ -35,13 +44,24 @@ export default {
|
||||
values: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
errors: {
|
||||
type: [Object, Boolean],
|
||||
default: false
|
||||
},
|
||||
formErrors: {
|
||||
type: Array,
|
||||
default: () => ([])
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
registry: {},
|
||||
internalFormModelProxy: {},
|
||||
formShouldShowErrors: false
|
||||
formShouldShowErrors: false,
|
||||
errorObservers: [],
|
||||
namedErrors: [],
|
||||
namedFieldErrors: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -72,6 +92,31 @@ export default {
|
||||
return Object.assign({}, this.values)
|
||||
}
|
||||
return {}
|
||||
},
|
||||
classes () {
|
||||
const classes = { 'formulate-form': true }
|
||||
if (this.name) {
|
||||
classes[`formulate-form--${this.name}`] = true
|
||||
}
|
||||
return classes
|
||||
},
|
||||
mergedFormErrors () {
|
||||
return this.formErrors.concat(this.namedErrors)
|
||||
},
|
||||
mergedFieldErrors () {
|
||||
const errors = {}
|
||||
if (this.errors) {
|
||||
for (const fieldName in this.errors) {
|
||||
errors[fieldName] = arrayify(this.errors[fieldName])
|
||||
}
|
||||
}
|
||||
for (const fieldName in this.namedFieldErrors) {
|
||||
errors[fieldName] = arrayify(this.namedFieldErrors[fieldName])
|
||||
}
|
||||
return errors
|
||||
},
|
||||
hasFormErrorObservers () {
|
||||
return !!this.errorObservers.filter(o => o.type === 'form').length
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -93,17 +138,52 @@ export default {
|
||||
}
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
mergedFormErrors (errors) {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'form')
|
||||
.forEach(o => o.callback(errors))
|
||||
},
|
||||
mergedFieldErrors: {
|
||||
handler (errors) {
|
||||
this.errorObservers
|
||||
.filter(o => o.type === 'input')
|
||||
.forEach(o => o.callback(errors[o.field] || []))
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$formulate.register(this)
|
||||
this.applyInitialValues()
|
||||
},
|
||||
destroyed () {
|
||||
this.$formulate.deregister(this)
|
||||
},
|
||||
methods: {
|
||||
applyInitialValues () {
|
||||
if (this.hasInitialValue) {
|
||||
this.internalFormModelProxy = this.initialValues
|
||||
}
|
||||
},
|
||||
applyErrors ({ formErrors, inputErrors }) {
|
||||
// given an object of errors, apply them to this form
|
||||
this.namedErrors = formErrors
|
||||
this.namedFieldErrors = inputErrors
|
||||
},
|
||||
addErrorObserver (observer) {
|
||||
if (!this.errorObservers.find(obs => observer.callback === obs.callback)) {
|
||||
this.errorObservers.push(observer)
|
||||
if (observer.type === 'form') {
|
||||
observer.callback(this.mergedFormErrors)
|
||||
} else if (Object.prototype.hasOwnProperty.call(this.mergedFieldErrors, observer.field)) {
|
||||
observer.callback(this.mergedFieldErrors[observer.field])
|
||||
}
|
||||
}
|
||||
},
|
||||
removeErrorObserver (observer) {
|
||||
this.errorObservers = this.errorObservers.filter(obs => obs.callback !== observer)
|
||||
},
|
||||
setFieldValue (field, value) {
|
||||
Object.assign(this.internalFormModelProxy, { [field]: value })
|
||||
this.$emit('input', Object.assign({}, this.internalFormModelProxy))
|
||||
@ -137,6 +217,11 @@ export default {
|
||||
this.setFieldValue(field, component.internalModelProxy)
|
||||
}
|
||||
},
|
||||
registerErrorComponent (component) {
|
||||
if (!this.errorComponents.includes(component)) {
|
||||
this.errorComponents.push(component)
|
||||
}
|
||||
},
|
||||
formSubmitted () {
|
||||
// perform validation here
|
||||
this.showErrors()
|
||||
|
@ -3,7 +3,7 @@
|
||||
class="formulate-input"
|
||||
:data-classification="classification"
|
||||
:data-has-errors="hasErrors"
|
||||
:data-is-showing-errors="hasErrors && showFieldErrors"
|
||||
:data-is-showing-errors="hasVisibleErrors"
|
||||
:data-type="type"
|
||||
>
|
||||
<div class="formulate-input-wrapper">
|
||||
@ -46,16 +46,20 @@
|
||||
class="formulate-input-help"
|
||||
v-text="help"
|
||||
/>
|
||||
<FormulateInputErrors
|
||||
v-if="showFieldErrors"
|
||||
:errors="mergedErrors"
|
||||
<FormulateErrors
|
||||
v-if="!disableErrors"
|
||||
:type="`input`"
|
||||
:errors="explicitErrors"
|
||||
:field-name="nameOrFallback"
|
||||
:validation-errors="validationErrors"
|
||||
:show-validation-errors="showValidationErrors"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import context from './libs/context'
|
||||
import { shallowEqualObjects, parseRules } from './libs/utils'
|
||||
import { shallowEqualObjects, parseRules, snakeToCamel } from './libs/utils'
|
||||
import nanoid from 'nanoid/non-secure'
|
||||
|
||||
export default {
|
||||
@ -177,6 +181,10 @@ export default {
|
||||
checked: {
|
||||
type: [String, Boolean],
|
||||
default: false
|
||||
},
|
||||
disableErrors: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@ -198,6 +206,20 @@ export default {
|
||||
},
|
||||
component () {
|
||||
return (this.classification === 'group') ? 'FormulateInputGroup' : this.$formulate.component(this.type)
|
||||
},
|
||||
parsedValidationRules () {
|
||||
const parsedValidationRules = {}
|
||||
Object.keys(this.validationRules).forEach((key) => {
|
||||
parsedValidationRules[snakeToCamel(key)] = this.validationRules[key]
|
||||
})
|
||||
return parsedValidationRules
|
||||
},
|
||||
messages () {
|
||||
const messages = {}
|
||||
Object.keys(this.validationMessages).forEach((key) => {
|
||||
messages[snakeToCamel(key)] = this.validationMessages[key]
|
||||
})
|
||||
return messages
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@ -258,7 +280,7 @@ export default {
|
||||
}
|
||||
},
|
||||
performValidation () {
|
||||
const rules = parseRules(this.validation, this.$formulate.rules(this.validationRules))
|
||||
const rules = parseRules(this.validation, this.$formulate.rules(this.parsedValidationRules))
|
||||
this.pendingValidation = Promise.all(
|
||||
rules.map(([rule, args]) => {
|
||||
var res = rule({
|
||||
@ -284,16 +306,17 @@ export default {
|
||||
})
|
||||
},
|
||||
getValidationFunction (rule) {
|
||||
const ruleName = rule.name.substr(0, 1) === '_' ? rule.name.substr(1) : rule.name
|
||||
if (this.validationMessages && typeof this.validationMessages === 'object' && typeof this.validationMessages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.validationMessages[ruleName]) {
|
||||
let ruleName = rule.name.substr(0, 1) === '_' ? rule.name.substr(1) : rule.name
|
||||
ruleName = snakeToCamel(ruleName)
|
||||
if (this.messages && typeof this.messages === 'object' && typeof this.messages[ruleName] !== 'undefined') {
|
||||
switch (typeof this.messages[ruleName]) {
|
||||
case 'function':
|
||||
return this.validationMessages[ruleName]
|
||||
return this.messages[ruleName]
|
||||
case 'string':
|
||||
return () => this.validationMessages[ruleName]
|
||||
return () => this.messages[ruleName]
|
||||
}
|
||||
}
|
||||
return (context) => this.$formulate.validationMessage(rule.name, context)
|
||||
return (context) => this.$formulate.validationMessage(rule.name, context, this)
|
||||
},
|
||||
hasValidationErrors () {
|
||||
return new Promise(resolve => {
|
||||
|
@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="errors.length"
|
||||
class="formulate-input-errors"
|
||||
>
|
||||
<!-- eslint-disable -->
|
||||
<li
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
v-html="error"
|
||||
class="formulate-input-error"
|
||||
/>
|
||||
<!-- eslint-enable -->
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
errors: {
|
||||
type: [Boolean, Array],
|
||||
required: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -6,7 +6,9 @@
|
||||
:key="optionContext.id"
|
||||
v-model="context.model"
|
||||
v-bind="optionContext"
|
||||
:disable-errors="true"
|
||||
class="formulate-input-group-item"
|
||||
@blur="context.blurHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -30,16 +30,21 @@ export default {
|
||||
...this.typeContext
|
||||
})
|
||||
},
|
||||
// Used in sub-context
|
||||
nameOrFallback,
|
||||
typeContext,
|
||||
elementAttributes,
|
||||
logicalLabelPosition,
|
||||
mergedUploadUrl,
|
||||
|
||||
// These items are not passed as context
|
||||
isVmodeled,
|
||||
mergedErrors,
|
||||
hasErrors,
|
||||
showFieldErrors,
|
||||
mergedValidationName,
|
||||
mergedUploadUrl
|
||||
explicitErrors,
|
||||
allErrors,
|
||||
hasErrors,
|
||||
hasVisibleErrors,
|
||||
showValidationErrors
|
||||
}
|
||||
|
||||
/**
|
||||
@ -125,10 +130,13 @@ function mergedUploadUrl () {
|
||||
* Determines if the field should show it's error (if it has one)
|
||||
* @return {boolean}
|
||||
*/
|
||||
function showFieldErrors () {
|
||||
function showValidationErrors () {
|
||||
if (this.showErrors || this.formShouldShowErrors) {
|
||||
return true
|
||||
}
|
||||
if (this.classification === 'file' && this.uploadBehavior === 'live' && this.context.model) {
|
||||
return true
|
||||
}
|
||||
return this.behavioralErrorVisibility
|
||||
}
|
||||
|
||||
@ -176,26 +184,40 @@ function createOptionList (options) {
|
||||
}
|
||||
|
||||
/**
|
||||
* The merged errors computed property.
|
||||
* These are errors we that have been explicity passed to us.
|
||||
*/
|
||||
function mergedErrors () {
|
||||
function explicitErrors () {
|
||||
return arrayify(this.errors)
|
||||
.concat(arrayify(this.error))
|
||||
.concat(arrayify(this.validationErrors))
|
||||
.reduce((errors, err) => !errors.includes(err) ? errors.concat(err) : errors, [])
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this computed property have errors.
|
||||
* The merged errors computed property.
|
||||
*/
|
||||
function allErrors () {
|
||||
return this.explicitErrors
|
||||
.concat(arrayify(this.validationErrors))
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this computed property have errors
|
||||
*/
|
||||
function hasErrors () {
|
||||
return !!this.mergedErrors.length
|
||||
return !!this.allErrors.length
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if form has actively visible errors.
|
||||
*/
|
||||
function hasVisibleErrors () {
|
||||
return ((this.validationErrors && this.showValidationErrors) || !!this.explicitErrors.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bound into the context object.
|
||||
*/
|
||||
function blurHandler () {
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
}
|
||||
|
11
src/libs/handler.js
Normal file
11
src/libs/handler.js
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* The default backend error handler assumes a failed axios instance. You can
|
||||
* easily override this function with fetch. The expected output is defined
|
||||
* on the documentation site vueformulate.com.
|
||||
*/
|
||||
export default function (err) {
|
||||
if (typeof err === 'object' && err.response) {
|
||||
|
||||
}
|
||||
return {}
|
||||
}
|
@ -1,108 +1,51 @@
|
||||
/**
|
||||
* library.js
|
||||
*
|
||||
* Note: We're shipping front end code here, file size is critical. This file is
|
||||
* overly terse for that reason alone, we wouldn't necessarily recommend this.
|
||||
*/
|
||||
const fi = 'FormulateInput'
|
||||
const add = (n, c) => ({
|
||||
classification: n,
|
||||
component: fi + (c || (n[0].toUpperCase() + n.substr(1)))
|
||||
})
|
||||
export default {
|
||||
// === SINGLE LINE TEXT STYLE INPUTS
|
||||
text: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
email: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
number: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
color: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
date: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
hidden: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
month: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
password: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
search: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
tel: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
time: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
url: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
week: {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
'datetime-local': {
|
||||
classification: 'text',
|
||||
component: 'FormulateInputText'
|
||||
},
|
||||
...[
|
||||
'text',
|
||||
'email',
|
||||
'number',
|
||||
'color',
|
||||
'date',
|
||||
'hidden',
|
||||
'month',
|
||||
'password',
|
||||
'search',
|
||||
'tel',
|
||||
'time',
|
||||
'url',
|
||||
'week',
|
||||
'datetime-local'
|
||||
].reduce((lib, type) => ({ ...lib, [type]: add('text') }), {}),
|
||||
|
||||
// === SLIDER INPUTS
|
||||
range: {
|
||||
classification: 'slider',
|
||||
component: 'FormulateInputSlider'
|
||||
},
|
||||
range: add('slider'),
|
||||
|
||||
// === MULTI LINE TEXT INPUTS
|
||||
textarea: {
|
||||
classification: 'textarea',
|
||||
component: 'FormulateInputTextArea'
|
||||
},
|
||||
textarea: add('textarea', 'TextArea'),
|
||||
|
||||
// === BOX STYLE INPUTS
|
||||
checkbox: {
|
||||
classification: 'box',
|
||||
component: 'FormulateInputBox'
|
||||
},
|
||||
radio: {
|
||||
classification: 'box',
|
||||
component: 'FormulateInputBox'
|
||||
},
|
||||
checkbox: add('box'),
|
||||
radio: add('box'),
|
||||
|
||||
// === BUTTON STYLE INPUTS
|
||||
submit: {
|
||||
classification: 'button',
|
||||
component: 'FormulateInputButton'
|
||||
},
|
||||
button: {
|
||||
classification: 'button',
|
||||
component: 'FormulateInputButton'
|
||||
},
|
||||
submit: add('button'),
|
||||
button: add('button'),
|
||||
|
||||
// === SELECT STYLE INPUTS
|
||||
select: {
|
||||
classification: 'select',
|
||||
component: 'FormulateInputSelect'
|
||||
},
|
||||
select: add('select'),
|
||||
|
||||
// === FILE TYPE
|
||||
|
||||
file: {
|
||||
classification: 'file',
|
||||
component: 'FormulateInputFile'
|
||||
},
|
||||
image: {
|
||||
classification: 'file',
|
||||
component: 'FormulateInputFile'
|
||||
}
|
||||
file: add('file'),
|
||||
image: add('file')
|
||||
}
|
||||
|
@ -1,74 +1,10 @@
|
||||
const i = 'image/'
|
||||
export default {
|
||||
'aac': 'audio/aac',
|
||||
'abw': 'application/x-abiword',
|
||||
'arc': 'application/x-freearc',
|
||||
'avi': 'video/x-msvideo',
|
||||
'azw': 'application/vnd.amazon.ebook',
|
||||
'bin': 'application/octet-stream',
|
||||
'bmp': 'image/bmp',
|
||||
'bz': 'application/x-bzip',
|
||||
'bz2': 'application/x-bzip2',
|
||||
'csh': 'application/x-csh',
|
||||
'css': 'text/css',
|
||||
'csv': 'text/csv',
|
||||
'doc': 'application/msword',
|
||||
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'eot': 'application/vnd.ms-fontobject',
|
||||
'epub': 'application/epub+zip',
|
||||
'gz': 'application/gzip',
|
||||
'gif': 'image/gif',
|
||||
'htm': 'text/html',
|
||||
'html': 'text/html',
|
||||
'ico': 'image/vnd.microsoft.icon',
|
||||
'ics': 'text/calendar',
|
||||
'jar': 'application/java-archive',
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'js': 'text/javascript',
|
||||
'json': 'application/json',
|
||||
'jsonld': 'application/ld+json',
|
||||
'midi': 'audio/x-midi',
|
||||
'mid': 'audio/midi',
|
||||
'mjs': 'text/javascript',
|
||||
'mp3': 'audio/mpeg',
|
||||
'mpeg': 'video/mpeg',
|
||||
'mpkg': 'application/vnd.apple.installer+xml',
|
||||
'odp': 'application/vnd.oasis.opendocument.presentation',
|
||||
'ods': 'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'odt': 'application/vnd.oasis.opendocument.text',
|
||||
'oga': 'audio/ogg',
|
||||
'ogv': 'video/ogg',
|
||||
'ogx': 'application/ogg',
|
||||
'opus': 'audio/opus',
|
||||
'otf': 'font/otf',
|
||||
'png': 'image/png',
|
||||
'gif': i + 'gif',
|
||||
'jpg': i + 'jpeg',
|
||||
'jpeg': i + 'jpeg',
|
||||
'png': i + 'png',
|
||||
'pdf': 'application/pdf',
|
||||
'php': 'application/php',
|
||||
'ppt': 'application/vnd.ms-powerpoint',
|
||||
'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'rar': 'application/vnd.rar',
|
||||
'rtf': 'application/rtf',
|
||||
'sh': 'application/x-sh',
|
||||
'svg': 'image/svg+xml',
|
||||
'swf': 'application/x-shockwave-flash',
|
||||
'tar': 'application/x-tar',
|
||||
'tif': 'image/tiff',
|
||||
'tiff': 'image/tiff',
|
||||
'ts': 'video/mp2t',
|
||||
'ttf': 'font/ttf',
|
||||
'txt': 'text/plain',
|
||||
'vsd': 'application/vnd.visio',
|
||||
'wav': 'audio/wav',
|
||||
'weba': 'audio/webm',
|
||||
'webm': 'video/webm',
|
||||
'webp': 'image/webp',
|
||||
'woff': 'font/woff',
|
||||
'woff2': 'font/woff2',
|
||||
'xhtml': 'application/xhtml+xml',
|
||||
'xls': 'application/vnd.ms-excel',
|
||||
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'xml': 'text/xml',
|
||||
'xul': 'application/vnd.mozilla.xul+xml',
|
||||
'zip': 'application/zip',
|
||||
'7z': 'application/x-7z-compressed'
|
||||
'svg': i + 'svg+xml'
|
||||
}
|
||||
|
@ -113,6 +113,22 @@ export default {
|
||||
return Promise.resolve(isEmail.test(value))
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value ends with one of the given Strings
|
||||
*/
|
||||
endsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.endsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is in an array (stack).
|
||||
*/
|
||||
@ -137,27 +153,6 @@ export default {
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the maximum value of a particular.
|
||||
*/
|
||||
max: function ({ value }, maximum = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
||||
return value.length <= maximum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value <= maximum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length <= maximum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the file type is correct.
|
||||
*/
|
||||
@ -197,6 +192,27 @@ export default {
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Check the maximum value of a particular.
|
||||
*/
|
||||
max: function ({ value }, maximum = 10, force) {
|
||||
return Promise.resolve((() => {
|
||||
if (Array.isArray(value)) {
|
||||
maximum = !isNaN(maximum) ? Number(maximum) : maximum
|
||||
return value.length <= maximum
|
||||
}
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
value = !isNaN(value) ? Number(value) : value
|
||||
return value <= maximum
|
||||
}
|
||||
if (typeof value === 'string' || (force === 'length')) {
|
||||
value = !isNaN(value) ? value.toString() : value
|
||||
return value.length <= maximum
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value is not in stack.
|
||||
*/
|
||||
@ -240,6 +256,22 @@ export default {
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: Value starts with one of the given Strings
|
||||
*/
|
||||
startsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (typeof value === 'string' && stack.length) {
|
||||
return stack.find(item => {
|
||||
return value.startsWith(item)
|
||||
}) !== undefined
|
||||
} else if (typeof value === 'string' && stack.length === 0) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if a string is a valid url
|
||||
*/
|
||||
|
@ -13,34 +13,6 @@ export function map (original, callback) {
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to filter an object's properties
|
||||
* @param {Object} original
|
||||
* @param {Function} callback
|
||||
*/
|
||||
export function filter (original, callback) {
|
||||
let obj = {}
|
||||
for (let key in original) {
|
||||
if (callback(key, original[key])) {
|
||||
obj[key] = original[key]
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to reduce an object's properties
|
||||
* @param {Object} original
|
||||
* @param {Function} callback
|
||||
* @param {*} accumulator
|
||||
*/
|
||||
export function reduce (original, callback, accumulator) {
|
||||
for (let key in original) {
|
||||
accumulator = callback(accumulator, key, original[key])
|
||||
}
|
||||
return accumulator
|
||||
}
|
||||
|
||||
/**
|
||||
* Shallow equal.
|
||||
* @param {} objA
|
||||
@ -71,6 +43,22 @@ export function shallowEqualObjects (objA, objB) {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, convert snake_case to camelCase
|
||||
* @param {String} string
|
||||
*/
|
||||
export function snakeToCamel (string) {
|
||||
if (typeof string === 'string') {
|
||||
return string.replace(/([_][a-z0-9])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, object, falsey, or array - return an array.
|
||||
* @param {mixed} item
|
||||
@ -91,17 +79,6 @@ export function arrayify (item) {
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* How to add an item.
|
||||
* @param {string} item
|
||||
*/
|
||||
export 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
|
||||
@ -119,7 +96,7 @@ export function parseRules (validation, rules) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string or function, parse it and return the an array in the format
|
||||
* Given a string or function, parse it and return an array in the format
|
||||
* [fn, [...arguments]]
|
||||
* @param {string|function} rule
|
||||
*/
|
||||
@ -129,6 +106,7 @@ function parseRule (rule, rules) {
|
||||
}
|
||||
if (Array.isArray(rule) && rule.length) {
|
||||
rule = rule.map(r => r) // light clone
|
||||
rule[0] = snakeToCamel(rule[0])
|
||||
if (typeof rule[0] === 'string' && rules.hasOwnProperty(rule[0])) {
|
||||
return [rules[rule.shift()], rule]
|
||||
}
|
||||
@ -138,7 +116,7 @@ function parseRule (rule, rules) {
|
||||
}
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const functionName = segments.shift()
|
||||
const functionName = snakeToCamel(segments.shift())
|
||||
if (rules.hasOwnProperty(functionName)) {
|
||||
return [rules[functionName], segments.length ? segments.join(':').split(',') : []]
|
||||
} else {
|
||||
@ -210,3 +188,17 @@ export function cloneDeep (obj) {
|
||||
}
|
||||
return newObj
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a locale string, parse the options.
|
||||
* @param {string} locale
|
||||
*/
|
||||
export function parseLocale (locale) {
|
||||
const segments = locale.split('-')
|
||||
return segments.reduce((options, segment) => {
|
||||
if (options.length) {
|
||||
options.unshift(`${options[0]}-${segment}`)
|
||||
}
|
||||
return options.length ? options : [segment]
|
||||
}, [])
|
||||
}
|
||||
|
9
src/locales/README.md
Normal file
9
src/locales/README.md
Normal file
@ -0,0 +1,9 @@
|
||||
# i18n moved
|
||||
|
||||
Locales have been removed from vue-formulate core to [vue-formulate-i18n](https://github.com/wearebraid/vue-formulate-i18n).
|
||||
This was done to allow for better tree-shaking by bundlers and allow
|
||||
for lots of additional language support without increasing the size of the core package.
|
||||
|
||||
## PRs welcome
|
||||
|
||||
[Please read the i18n contribution documentation](https://www.vueformulate.com/guide/contributing/#internationalization).
|
@ -1,171 +0,0 @@
|
||||
import { sentence as s } from '../libs/utils'
|
||||
/**
|
||||
* Validation error message generators.
|
||||
*/
|
||||
export default {
|
||||
|
||||
/**
|
||||
* Valid accepted value.
|
||||
*/
|
||||
accepted: function ({ name }) {
|
||||
return `Please accept the ${name}.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not after.
|
||||
*/
|
||||
after: function ({ name, args }) {
|
||||
if (Array.isArray(args) && args.length) {
|
||||
return `${s(name)} must be after ${args[0]}.`
|
||||
}
|
||||
return `${s(name)} must be a later date.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not a letter.
|
||||
*/
|
||||
alpha: function ({ name }) {
|
||||
return `${s(name)} can only contain alphabetical characters.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Rule: checks if the value is alpha numeric
|
||||
*/
|
||||
alphanumeric: function ({ name }) {
|
||||
return `${s(name)} can only contain letters and numbers.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The date is not before.
|
||||
*/
|
||||
before: function ({ name, args }) {
|
||||
if (Array.isArray(args) && args.length) {
|
||||
return `${s(name)} must be before ${args[0]}.`
|
||||
}
|
||||
return `${s(name)} must be an earlier date.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The value is not between two numbers or lengths
|
||||
*/
|
||||
between: function ({ name, value, args }) {
|
||||
if (!isNaN(value)) {
|
||||
return `${s(name)} must be between ${args[0]} and ${args[1]}.`
|
||||
}
|
||||
return `${s(name)} must be between ${args[0]} and ${args[1]} characters long.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The confirmation field does not match
|
||||
*/
|
||||
confirm: function ({ name, args }) {
|
||||
return `${s(name)} does not match.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid date.
|
||||
*/
|
||||
date: function ({ name, args }) {
|
||||
if (Array.isArray(args) && args.length) {
|
||||
return `${s(name)} is not a valid, please use the format ${args[0]}`
|
||||
}
|
||||
return `${s(name)} is not a valid date.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The default render method for error messages.
|
||||
*/
|
||||
default: function ({ name }) {
|
||||
return `This field isn’t valid.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Is not a valid email address.
|
||||
*/
|
||||
email: function ({ name, value }) {
|
||||
if (!value) {
|
||||
return 'Please enter a valid email address.'
|
||||
}
|
||||
return `“${value}” is not a valid email address.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is an allowed value.
|
||||
*/
|
||||
in: function ({ name, value }) {
|
||||
if (typeof value === 'string' && value) {
|
||||
return `“${s(value)}” is not an allowed ${name}.`
|
||||
}
|
||||
return `This is not an allowed ${name}.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a match.
|
||||
*/
|
||||
matches: function ({ name }) {
|
||||
return `${s(name)} is not an allowed value.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
max: function ({ name, value, args }) {
|
||||
if (Array.isArray(value)) {
|
||||
return `You may only select ${args[0]} ${name}.`
|
||||
}
|
||||
const force = Array.isArray(args) && args[1] ? args[1] : false
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
return `${s(name)} must be less than or equal to ${args[0]}.`
|
||||
}
|
||||
return `${s(name)} must be less than or equal to ${args[0]} characters long.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The (field-level) error message for mime errors.
|
||||
*/
|
||||
mime: function ({ name, args }) {
|
||||
return `${s(name)} must be of the the type: ${args[0] || 'No file formats allowed.'}`
|
||||
},
|
||||
|
||||
/**
|
||||
* The maximum value allowed.
|
||||
*/
|
||||
min: function ({ name, value, args }) {
|
||||
if (Array.isArray(value)) {
|
||||
return `You must select at least ${args[0]} ${name}.`
|
||||
}
|
||||
const force = Array.isArray(args) && args[1] ? args[1] : false
|
||||
if ((!isNaN(value) && force !== 'length') || force === 'value') {
|
||||
return `${s(name)} must be more than ${args[0]}.`
|
||||
}
|
||||
return `${s(name)} must be more than ${args[0]} characters long.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not an allowed value
|
||||
*/
|
||||
not: function ({ name, value }) {
|
||||
return `“${value}” is not an allowed ${name}.`
|
||||
},
|
||||
|
||||
/**
|
||||
* The field is not a number
|
||||
*/
|
||||
number: function ({ name }) {
|
||||
return `${s(name)} must be a number.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Required field.
|
||||
*/
|
||||
required: function ({ name }) {
|
||||
return `${s(name)} is required.`
|
||||
},
|
||||
|
||||
/**
|
||||
* Value is not a url.
|
||||
*/
|
||||
url: function ({ name }) {
|
||||
return `Please include a valid url.`
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
import Formulate from '../src/Formulate.js'
|
||||
|
||||
test('can merge simple object', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionA: false
|
||||
}
|
||||
expect(Formulate.merge(a, b)).toEqual({
|
||||
optionA: false,
|
||||
optionB: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
test('can add to simple array', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: ['first', 'second']
|
||||
}
|
||||
let b = {
|
||||
optionB: ['third']
|
||||
}
|
||||
expect(Formulate.merge(a, b, true)).toEqual({
|
||||
optionA: true,
|
||||
optionB: ['first', 'second', 'third']
|
||||
})
|
||||
})
|
||||
|
||||
test('can merge recursively', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '123',
|
||||
third: {
|
||||
a: 'b'
|
||||
}
|
||||
},
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionB: '567',
|
||||
optionC: {
|
||||
first: '1234',
|
||||
second: '789',
|
||||
}
|
||||
}
|
||||
expect(Formulate.merge(a, b)).toEqual({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '1234',
|
||||
third: {
|
||||
a: 'b'
|
||||
},
|
||||
second: '789'
|
||||
},
|
||||
optionB: '567'
|
||||
})
|
||||
})
|
||||
|
||||
test('installs on vue instance', () => {
|
||||
const components = [
|
||||
'FormulateForm',
|
||||
'FormulateInput',
|
||||
'FormulateInputErrors',
|
||||
'FormulateInputBox',
|
||||
'FormulateInputText',
|
||||
'FormulateInputFile',
|
||||
'FormulateInputGroup',
|
||||
'FormulateInputButton',
|
||||
'FormulateInputSelect',
|
||||
'FormulateInputSlider',
|
||||
'FormulateInputTextArea'
|
||||
]
|
||||
const registry = []
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {
|
||||
registry.push(name)
|
||||
}
|
||||
Formulate.install(Vue, { extended: true })
|
||||
expect(Vue.prototype.$formulate).toBe(Formulate)
|
||||
expect(registry).toEqual(components)
|
||||
})
|
||||
|
||||
test('can extend instance in a plugin', () => {
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const plugin = function (i) {
|
||||
i.extend({
|
||||
rules: {
|
||||
testRule: () => false
|
||||
}
|
||||
})
|
||||
}
|
||||
Formulate.install(Vue, {
|
||||
plugins: [ plugin ]
|
||||
})
|
||||
|
||||
expect(typeof Vue.prototype.$formulate.options.rules.testRule).toBe('function')
|
||||
})
|
169
test/unit/Formulate.test.js
Normal file
169
test/unit/Formulate.test.js
Normal file
@ -0,0 +1,169 @@
|
||||
import Formulate from '@/Formulate.js'
|
||||
|
||||
describe('Formulate', () => {
|
||||
it('can merge simple object', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionA: false
|
||||
}
|
||||
expect(Formulate.merge(a, b)).toEqual({
|
||||
optionA: false,
|
||||
optionB: '1234'
|
||||
})
|
||||
})
|
||||
|
||||
it('can add to simple array', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionB: ['first', 'second']
|
||||
}
|
||||
let b = {
|
||||
optionB: ['third']
|
||||
}
|
||||
expect(Formulate.merge(a, b, true)).toEqual({
|
||||
optionA: true,
|
||||
optionB: ['first', 'second', 'third']
|
||||
})
|
||||
})
|
||||
|
||||
it('can merge recursively', () => {
|
||||
let a = {
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '123',
|
||||
third: {
|
||||
a: 'b'
|
||||
}
|
||||
},
|
||||
optionB: '1234'
|
||||
}
|
||||
let b = {
|
||||
optionB: '567',
|
||||
optionC: {
|
||||
first: '1234',
|
||||
second: '789',
|
||||
}
|
||||
}
|
||||
expect(Formulate.merge(a, b)).toEqual({
|
||||
optionA: true,
|
||||
optionC: {
|
||||
first: '1234',
|
||||
third: {
|
||||
a: 'b'
|
||||
},
|
||||
second: '789'
|
||||
},
|
||||
optionB: '567'
|
||||
})
|
||||
})
|
||||
|
||||
it('installs on vue instance', () => {
|
||||
const components = [
|
||||
'FormulateForm',
|
||||
'FormulateInput',
|
||||
'FormulateErrors',
|
||||
'FormulateInputBox',
|
||||
'FormulateInputText',
|
||||
'FormulateInputFile',
|
||||
'FormulateInputGroup',
|
||||
'FormulateInputButton',
|
||||
'FormulateInputSelect',
|
||||
'FormulateInputSlider',
|
||||
'FormulateInputTextArea'
|
||||
]
|
||||
const registry = []
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {
|
||||
registry.push(name)
|
||||
}
|
||||
Formulate.install(Vue, { extended: true })
|
||||
expect(Vue.prototype.$formulate).toBe(Formulate)
|
||||
expect(registry).toEqual(components)
|
||||
})
|
||||
|
||||
it('can extend instance in a plugin', () => {
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const plugin = function (i) {
|
||||
i.extend({
|
||||
rules: {
|
||||
testRule: () => false
|
||||
}
|
||||
})
|
||||
}
|
||||
Formulate.install(Vue, {
|
||||
plugins: [ plugin ]
|
||||
})
|
||||
|
||||
expect(typeof Vue.prototype.$formulate.options.rules.testRule).toBe('function')
|
||||
})
|
||||
|
||||
it('locale default to en', () => {
|
||||
Formulate.selectedLocale = false // reset the memoization
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const vm = {}
|
||||
Formulate.install(Vue, {
|
||||
locales: {
|
||||
de: {},
|
||||
fr: {},
|
||||
cn: {},
|
||||
en: {}
|
||||
}
|
||||
})
|
||||
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('en')
|
||||
})
|
||||
|
||||
it('explicitly selects language', () => {
|
||||
Formulate.selectedLocale = false // reset the memoization
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const vm = {}
|
||||
Formulate.install(Vue, {
|
||||
locale: 'fr-CH',
|
||||
locales: {
|
||||
de: {},
|
||||
fr: {},
|
||||
cn: {},
|
||||
en: {}
|
||||
}
|
||||
})
|
||||
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('fr')
|
||||
})
|
||||
|
||||
it('can select a specific language tag', () => {
|
||||
Formulate.selectedLocale = false // reset the memoization
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const vm = {}
|
||||
Formulate.install(Vue, {
|
||||
locale: 'nl-BE',
|
||||
locales: {
|
||||
de: {},
|
||||
fr: {},
|
||||
'nl-BE': {},
|
||||
nl: {},
|
||||
cn: {},
|
||||
en: {}
|
||||
}
|
||||
})
|
||||
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('nl-BE')
|
||||
})
|
||||
|
||||
it('can select a matching locale using i18n', () => {
|
||||
Formulate.selectedLocale = false // reset the memoization
|
||||
function Vue () {}
|
||||
Vue.component = function (name, instance) {}
|
||||
const vm = { $i18n: {locale: 'cn-ET' } }
|
||||
Formulate.install(Vue, {
|
||||
locales: {
|
||||
cn: {},
|
||||
em: {}
|
||||
}
|
||||
})
|
||||
expect(Vue.prototype.$formulate.getLocale(vm)).toBe('cn')
|
||||
})
|
||||
})
|
@ -1,8 +1,8 @@
|
||||
import Vue from 'vue'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import FormSubmission from '../src/FormSubmission.js'
|
||||
import Formulate from '../../src/Formulate.js'
|
||||
import FormSubmission from '../../src/FormSubmission.js'
|
||||
import FormulateForm from '@/FormulateForm.vue'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
|
||||
@ -221,4 +221,172 @@ describe('FormulateForm', () => {
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.formulate-input-error').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('automatically registers with root plugin', async () => {
|
||||
const wrapper = mount(FormulateForm, {
|
||||
propsData: { formulateValue: { box3: [] }, name: 'login' }
|
||||
})
|
||||
expect(wrapper.vm.$formulate.registry.has('login')).toBe(true)
|
||||
expect(wrapper.vm.$formulate.registry.get('login')).toBe(wrapper.vm)
|
||||
})
|
||||
|
||||
it('errors are displayed on correctly named components', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<div>
|
||||
<FormulateForm
|
||||
name="login"
|
||||
/>
|
||||
<FormulateForm
|
||||
name="register"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
expect(wrapper.vm.$formulate.registry.has('login') && wrapper.vm.$formulate.registry.has('register')).toBe(true)
|
||||
wrapper.vm.$formulate.handle({ formErrors: ['This is an error message'] }, 'login')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-form').length).toBe(2)
|
||||
expect(wrapper.find('.formulate-form--login .formulate-form-errors').exists()).toBe(true)
|
||||
expect(wrapper.find('.formulate-form--register .formulate-form-errors').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('hides root FormError if another form error exists and renders in new location', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<FormulateForm
|
||||
name="login"
|
||||
>
|
||||
<h1>Login</h1>
|
||||
<FormulateErrors />
|
||||
<FormulateInput name="username" validation="required" error-behavior="live" />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
wrapper.vm.$formulate.handle({ formErrors: ['This is an error message'] }, 'login')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-form-errors').length).toBe(1)
|
||||
// Ensure that we moved the position of the errors
|
||||
expect(wrapper.find('h1 + *').is('.formulate-form-errors')).toBe(true)
|
||||
})
|
||||
|
||||
it('allows rendering multiple locations', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<FormulateForm
|
||||
name="login"
|
||||
>
|
||||
<h1>Login</h1>
|
||||
<FormulateErrors />
|
||||
<FormulateInput name="username" validation="required" error-behavior="live" />
|
||||
<FormulateErrors />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
wrapper.vm.$formulate.handle({ formErrors: ['This is an error message'] }, 'login')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-form-errors').length).toBe(2)
|
||||
})
|
||||
|
||||
it('receives a form-errors prop and displays it', async () => {
|
||||
const wrapper = mount(FormulateForm, {
|
||||
propsData: { formErrors: ['first', 'second'] },
|
||||
slots: {
|
||||
default: '<FormulateInput name="name" />'
|
||||
}
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-form-error').length).toBe(2)
|
||||
})
|
||||
|
||||
it('it aggregates form-errors prop with form-named errors', async () => {
|
||||
const wrapper = mount(FormulateForm, {
|
||||
propsData: { formErrors: ['first', 'second'], name: 'login' }
|
||||
})
|
||||
wrapper.vm.$formulate.handle({ formErrors: ['third'] }, 'login')
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-form-error').length).toBe(3)
|
||||
})
|
||||
|
||||
it('displays field errors on inputs with errors prop', async () => {
|
||||
const wrapper = mount(FormulateForm, {
|
||||
propsData: { errors: { sipple: ['This field has an error'] }},
|
||||
slots: {
|
||||
default: '<FormulateInput name="sipple" />'
|
||||
}
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.formulate-input .formulate-input-error').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('is able to display multiple errors on multiple elements', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<FormulateForm
|
||||
:errors="{inputA: ['first', 'second'], inputB: 'only one here', inputC: ['and one here']}"
|
||||
>
|
||||
<FormulateInput name="inputA" />
|
||||
<FormulateInput name="inputB" type="textarea" />
|
||||
<FormulateInput name="inputC" type="checkbox" />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(4)
|
||||
})
|
||||
|
||||
it('it can set multiple field errors with handle()', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<FormulateForm name="register">
|
||||
<FormulateInput name="inputA" />
|
||||
<FormulateInput name="inputB" type="textarea" />
|
||||
<FormulateInput name="inputC" type="checkbox" />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(0)
|
||||
wrapper.vm.$formulate.handle({ inputErrors: {inputA: ['first', 'second'], inputB: 'only one here', inputC: ['and one here']} }, "register")
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(4)
|
||||
})
|
||||
|
||||
it('only sets 1 error when used on a FormulateGroup input', async () => {
|
||||
const wrapper = mount({
|
||||
template: `
|
||||
<FormulateForm
|
||||
name="register"
|
||||
:errors="{order: 'this didnt work'}"
|
||||
>
|
||||
<FormulateInput name="order" type="checkbox" :options="{first: 'First', last: 'Last', middle: 'Middle'}" />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(1)
|
||||
})
|
||||
|
||||
it('properly de-registers an observer when removed', async () => {
|
||||
const wrapper = mount({
|
||||
data () {
|
||||
return {
|
||||
hasField: true
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<FormulateForm
|
||||
name="register"
|
||||
:errors="{order: 'this didnt work'}"
|
||||
>
|
||||
<FormulateInput v-if="hasField" name="order" type="checkbox" :options="{first: 'First', last: 'Last', middle: 'Middle'}" />
|
||||
</FormulateForm>
|
||||
`
|
||||
})
|
||||
await flushPromises()
|
||||
expect(wrapper.find(FormulateForm).vm.errorObservers.length).toBe(1)
|
||||
wrapper.setData({ hasField: false })
|
||||
await flushPromises()
|
||||
expect(wrapper.find(FormulateForm).vm.errorObservers.length).toBe(0)
|
||||
})
|
||||
})
|
@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import Formulate from '@/Formulate.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import Formulate from '../../src/Formulate.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputBox from '@/inputs/FormulateInputBox.vue'
|
||||
import FormulateInputGroup from '@/FormulateInputGroup.vue'
|
||||
@ -181,4 +181,24 @@ describe('FormulateInputBox', () => {
|
||||
expect(checkboxes.length).toBe(1)
|
||||
expect(checkboxes.at(0).element.value).toBe('fooey')
|
||||
})
|
||||
|
||||
it('shows validation errors when blurred', async () => {
|
||||
const wrapper = mount({
|
||||
data () {
|
||||
return {
|
||||
radioValue: 'fooey',
|
||||
options: {foo: 'Foo', bar: 'Bar', fooey: 'Fooey', gooey: 'Gooey'}
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<FormulateInput type="radio" v-model="radioValue" :options="options" validation="in:gooey" />
|
||||
</div>
|
||||
`
|
||||
})
|
||||
wrapper.find('input[value="fooey"]').trigger('blur')
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(wrapper.find('.formulate-input-error').exists()).toBe(true)
|
||||
})
|
||||
})
|
@ -1,6 +1,6 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import Formulate from '../../src/Formulate.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputButton from '@/inputs/FormulateInputButton.vue'
|
||||
|
@ -1,8 +1,8 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import flushPromises from 'flush-promises'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import FileUpload from '../src/FileUpload.js'
|
||||
import Formulate from '../../src/Formulate.js'
|
||||
import FileUpload from '../../src/FileUpload.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputFile from '@/inputs/FormulateInputFile.vue'
|
||||
|
||||
@ -20,6 +20,16 @@ describe('FormulateInputFile', () => {
|
||||
expect(wrapper.contains(FormulateInputFile)).toBe(true)
|
||||
})
|
||||
|
||||
it('forces an error-behavior live mode when upload-behavior is live and it has content', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'image', validation: 'mime:image/jpeg', value: [{ url: 'img.jpg' }] } })
|
||||
expect(wrapper.vm.showValidationErrors).toBe(true)
|
||||
})
|
||||
|
||||
it('wont show errors when upload-behavior is live and it is required but empty', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'image', validation: 'required|mime:image/jpeg' } })
|
||||
expect(wrapper.vm.showValidationErrors).toBe(false)
|
||||
})
|
||||
|
||||
/**
|
||||
* ===========================================================================
|
||||
* Currently there appears to be no way to properly mock upload data in
|
@ -1,8 +1,8 @@
|
||||
import Vue from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import FormulateInput from '../src/FormulateInput.vue'
|
||||
import FormulateInputSlider from '../src/inputs/FormulateInputSlider.vue'
|
||||
import Formulate from '@/Formulate.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputSlider from '@/inputs/FormulateInputSlider.vue'
|
||||
|
||||
Vue.use(Formulate)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import Vue from 'vue'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Formulate from '../src/Formulate.js'
|
||||
import Formulate from '../../src/Formulate.js'
|
||||
import FormulateInput from '@/FormulateInput.vue'
|
||||
import FormulateInputText from '@/inputs/FormulateInputText.vue'
|
||||
import { doesNotReject } from 'assert';
|
||||
@ -172,8 +172,8 @@ describe('FormulateInputText', () => {
|
||||
expect(wrapper.find('.formulate-input-errors').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts a single string as an error prop', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'live', error: 'This is an error' } })
|
||||
it('accepts a single string as an error prop', async () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', error: 'This is an error' } })
|
||||
expect(wrapper.find('.formulate-input-errors').exists()).toBe(true)
|
||||
})
|
||||
|
||||
@ -192,16 +192,23 @@ describe('FormulateInputText', () => {
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('does not initially show error-behavior blur errors', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', errors: ['Bad input'] } })
|
||||
it('Should always show explicitly set errors, but not validation errors', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', validation: 'required', errorBehavior: 'blur', errors: ['Bad input'] } })
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(1)
|
||||
})
|
||||
|
||||
it('Should show no errors when there are no errors', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text' } })
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(false)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.formulate-input-error').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('allows error-behavior blur to be overridden with show-errors', () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', showErrors: true, errors: ['Bad input'] } })
|
||||
it('allows error-behavior blur to be overridden with show-errors', async () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', showErrors: true, validation: 'required' } })
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(true)
|
||||
@ -209,14 +216,29 @@ describe('FormulateInputText', () => {
|
||||
})
|
||||
|
||||
it('shows errors on blur with error-behavior blur', async () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', errors: ['Bad input'] } })
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'text', errorBehavior: 'blur', validation: 'required' } })
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(false)
|
||||
expect(wrapper.findAll('.formulate-input-error').exists()).toBe(false)
|
||||
expect(wrapper.vm.showValidationErrors).toBe(false)
|
||||
wrapper.find('input').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.showValidationErrors).toBe(true)
|
||||
expect(wrapper.find('[data-is-showing-errors]').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(true)
|
||||
expect(wrapper.findAll('.formulate-input-error').length).toBe(1)
|
||||
// expect(wrapper.findAll('.formulate-input-errors').exists()).toBe(true)
|
||||
// expect(wrapper.findAll('.formulate-input-error').length).toBe(1)
|
||||
})
|
||||
|
||||
it('continues to show errors if validation fires more than one time', async () => {
|
||||
const wrapper = mount(FormulateInput, { propsData: { type: 'date', errorBehavior: 'live', validation: [['after', '01/01/2021']] , value: '01/01/1999' } })
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
wrapper.find('input').trigger('input')
|
||||
await wrapper.vm.$nextTick()
|
||||
await flushPromises()
|
||||
expect(wrapper.find('[data-has-errors]').exists()).toBe(true)
|
||||
})
|
||||
})
|
@ -1,35 +1,250 @@
|
||||
import rules from '@/libs/rules'
|
||||
import FileUpload from '../src/FileUpload'
|
||||
import FileUpload from '../../src/FileUpload'
|
||||
|
||||
|
||||
/**
|
||||
* Required rule
|
||||
* Accepted rule
|
||||
*/
|
||||
describe('required', () => {
|
||||
it('fails on empty string', async () => expect(await rules.required({ value: '' })).toBe(false))
|
||||
describe('accepted', () => {
|
||||
it('passes with true', async () => expect(await rules.accepted({ value: 'yes' })).toBe(true))
|
||||
|
||||
it('fails on empty array', async () => expect(await rules.required({ value: [] })).toBe(false))
|
||||
it('passes with on', async () => expect(await rules.accepted({ value: 'on' })).toBe(true))
|
||||
|
||||
it('fails on empty object', async () => expect(await rules.required({ value: {} })).toBe(false))
|
||||
it('passes with 1', async () => expect(await rules.accepted({ value: '1' })).toBe(true))
|
||||
|
||||
it('fails on null', async () => expect(await rules.required({ value: null })).toBe(false))
|
||||
it('passes with number 1', async () => expect(await rules.accepted({ value: 1 })).toBe(true))
|
||||
|
||||
it('passes with the number zero', async () => expect(await rules.required({ value: 0 })).toBe(true))
|
||||
it('passes with boolean true', async () => expect(await rules.accepted({ value: true })).toBe(true))
|
||||
|
||||
it('passes with the boolean false', async () => expect(await rules.required({ value: false })).toBe(true))
|
||||
it('fail with boolean false', async () => expect(await rules.accepted({ value: false })).toBe(false))
|
||||
|
||||
it('passes with a non empty array', async () => expect(await rules.required({ value: ['123'] })).toBe(true))
|
||||
|
||||
it('passes with a non empty object', async () => expect(await rules.required({ value: {a: 'b'} })).toBe(true))
|
||||
|
||||
it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true))
|
||||
|
||||
it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true))
|
||||
|
||||
it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true))
|
||||
|
||||
it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false))
|
||||
it('fail with "false"', async () => expect(await rules.accepted({ value: 'false' })).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 tomorrow’s date object', async () => expect(await rules.after({ value: tomorrow })).toBe(true))
|
||||
|
||||
it('passes with future date', async () => expect(await rules.after({ value: 'January 15, 2999' })).toBe(true))
|
||||
|
||||
it('passes with long past date', async () => expect(await rules.after({ value: yesterday }, 'Jan 15, 2000')).toBe(true))
|
||||
|
||||
it('fails with yesterday’s date', async () => expect(await rules.after({ value: yesterday })).toBe(false))
|
||||
|
||||
it('fails with old date string', async () => expect(await rules.after({ value: 'January, 2000' })).toBe(false))
|
||||
|
||||
it('fails with invalid value', async () => expect(await rules.after({ value: '' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a date is after another date
|
||||
*/
|
||||
describe('alpha', () => {
|
||||
it('passes with simple string', async () => expect(await rules.alpha({ value: 'abc' })).toBe(true))
|
||||
|
||||
it('passes with long string', async () => expect(await rules.alpha({ value: 'lkashdflaosuihdfaisudgflakjsdbflasidufg' })).toBe(true))
|
||||
|
||||
it('passes with single character', async () => expect(await rules.alpha({ value: 'z' })).toBe(true))
|
||||
|
||||
it('passes with accented character', async () => expect(await rules.alpha({ value: 'jüstin' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'russian')).toBe(true))
|
||||
|
||||
it('fails with lots of accented characters if latin', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'latin')).toBe(false))
|
||||
|
||||
it('fails with numbers', async () => expect(await rules.alpha({ value: 'justin83' })).toBe(false))
|
||||
|
||||
it('fails with symbols', async () => expect(await rules.alpha({ value: '-justin' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a date alpha and numeric
|
||||
*/
|
||||
describe('alphanumeric', () => {
|
||||
it('passes with simple string', async () => expect(await rules.alphanumeric({ value: '567abc' })).toBe(true))
|
||||
|
||||
it('passes with long string', async () => expect(await rules.alphanumeric({ value: 'lkashdfla234osuihdfaisudgflakjsdbfla567sidufg' })).toBe(true))
|
||||
|
||||
it('passes with single character', async () => expect(await rules.alphanumeric({ value: 'z' })).toBe(true))
|
||||
|
||||
it('passes with accented character', async () => expect(await rules.alphanumeric({ value: 'jüst56in' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters', async () => expect(await rules.alphanumeric({ value: 'àáâ7567567äïíôöÆ' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alphanumeric({ value: '123123àáâäï67íôöÆ' }, 'russian')).toBe(true))
|
||||
|
||||
it('fails with lots of accented characters if latin', async () => expect(await rules.alphanumeric({ value: 'àáâäï123123íôöÆ' }, 'latin')).toBe(false))
|
||||
|
||||
it('fails with decimals in', async () => expect(await rules.alphanumeric({ value: 'abcABC99.123' })).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 tomorrow’s date object', async () => expect(await rules.before({ value: tomorrow })).toBe(false))
|
||||
|
||||
it('fails with future date', async () => expect(await rules.before({ value: 'January 15, 2999' })).toBe(false))
|
||||
|
||||
it('fails with long past date', async () => expect(await rules.before({ value: yesterday }, 'Jan 15, 2000')).toBe(false))
|
||||
|
||||
it('passes with yesterday’s date', async () => expect(await rules.before({ value: yesterday })).toBe(true))
|
||||
|
||||
it('passes with old date string', async () => expect(await rules.before({ value: 'January, 2000' })).toBe(true))
|
||||
|
||||
it('fails with invalid value', async () => expect(await rules.after({ value: '' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if between
|
||||
*/
|
||||
describe('between', () => {
|
||||
it('passes with simple number', async () => expect(await rules.between({ value: 5 }, 0, 10)).toBe(true))
|
||||
|
||||
it('passes with simple number string', async () => expect(await rules.between({ value: '5' }, '0', '10')).toBe(true))
|
||||
|
||||
it('passes with decimal number string', async () => expect(await rules.between({ value: '0.5' }, '0', '1')).toBe(true))
|
||||
|
||||
it('passes with string length', async () => expect(await rules.between({ value: 'abc' }, 2, 4)).toBe(true))
|
||||
|
||||
it('fails with string length too long', async () => expect(await rules.between({ value: 'abcdef' }, 2, 4)).toBe(false))
|
||||
|
||||
it('fails with string length too short', async () => expect(await rules.between({ value: 'abc' }, 3, 10)).toBe(false))
|
||||
|
||||
it('fails with number to small', async () => expect(await rules.between({ value: 0 }, 3, 10)).toBe(false))
|
||||
|
||||
it('fails with number to large', async () => expect(await rules.between({ value: 15 }, 3, 10)).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Confirm
|
||||
*/
|
||||
describe('confirm', () => {
|
||||
it('passes when the values are the same strings', async () => expect(await rules.confirm(
|
||||
{ value: 'abc', name: 'password', getFormValues: () => ({ password_confirm: 'abc' }) }
|
||||
)).toBe(true))
|
||||
|
||||
it('passes when the values are the same integers', async () => expect(await rules.confirm(
|
||||
{ value: 4422132, name: 'xyz', getFormValues: () => ({ xyz_confirm: 4422132 }) }
|
||||
)).toBe(true))
|
||||
|
||||
it('passes when using a custom field', async () => expect(await rules.confirm(
|
||||
{ value: 4422132, name: 'name', getFormValues: () => ({ other_field: 4422132 }) },
|
||||
'other_field'
|
||||
)).toBe(true))
|
||||
|
||||
it('passes when using a field ends in _confirm', async () => expect(await rules.confirm(
|
||||
{ value: '$ecret', name: 'password_confirm', getFormValues: () => ({ password: '$ecret' }) }
|
||||
)).toBe(true))
|
||||
|
||||
it('fails when using different strings', async () => expect(await rules.confirm(
|
||||
{ value: 'Justin', name: 'name', getFormValues: () => ({ name_confirm: 'Daniel' }) },
|
||||
)).toBe(false))
|
||||
|
||||
it('fails when the types are different', async () => expect(await rules.confirm(
|
||||
{ value: '1234', name: 'num', getFormValues: () => ({ num_confirm: 1234 }) },
|
||||
)).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Determines if the string is a date
|
||||
*/
|
||||
describe('date', () => {
|
||||
it('passes with month day year', async () => expect(await rules.date({ value: 'December 17, 2020' })).toBe(true))
|
||||
|
||||
it('passes with month day', async () => expect(await rules.date({ value: 'December 17' })).toBe(true))
|
||||
|
||||
it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17' })).toBe(true))
|
||||
|
||||
it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17 12:34:15' })).toBe(true))
|
||||
|
||||
it('passes with out of bounds number', async () => expect(await rules.date({ value: 'January 77' })).toBe(true))
|
||||
|
||||
it('passes with only month', async () => expect(await rules.date({ value: 'January' })).toBe(false))
|
||||
|
||||
it('passes with valid date format', async () => expect(await rules.date({ value: '12/17/1987' }, 'MM/DD/YYYY')).toBe(true))
|
||||
|
||||
it('fails with simple number and date format', async () => expect(await rules.date({ value: '1234' }, 'MM/DD/YYYY')).toBe(false))
|
||||
|
||||
it('fails with only day of week', async () => expect(await rules.date({ value: 'saturday' })).toBe(false))
|
||||
|
||||
it('fails with random string', async () => expect(await rules.date({ value: 'Pepsi 17' })).toBe(false))
|
||||
|
||||
it('fails with random number', async () => expect(await rules.date({ value: '1872301237' })).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({ value: 'dev+123@wearebraid.com' })).toBe(true))
|
||||
|
||||
it('passes numeric email', async () => expect(await rules.email({ value: '12345@google.com' })).toBe(true))
|
||||
|
||||
it('passes unicode email', async () => expect(await rules.email({ value: 'àlphä@❤️.ly' })).toBe(true))
|
||||
|
||||
it('passes numeric with new tld', async () => expect(await rules.email({ value: '12345@google.photography' })).toBe(true))
|
||||
|
||||
it('fails string without tld', async () => expect(await rules.email({ value: '12345@localhost' })).toBe(false))
|
||||
|
||||
it('fails string without invalid name', async () => expect(await rules.email({ value: '1*(123)2345@localhost' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if value ends with a one of the specified Strings.
|
||||
*/
|
||||
describe('endsWith', () => {
|
||||
it('fails when value ending is not in stack of single value', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@gmail.com')).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when value ending is not in stack of multiple values', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@gmail.com', '@yahoo.com')).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when passed value is not a string', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com'}, ['@gmail.com', '@wearebraid.com'])).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when passed value is not a string', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com'}, {value: '@wearebraid.com'})).toBe(false)
|
||||
})
|
||||
|
||||
it('passes when a string value is present and matched even if non-string values also exist as arguments', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com'}, {value: 'bad data'}, ['no bueno'], '@wearebraid.com')).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when stack consists of zero values', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com' })).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when value ending is in stack of single value', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@wearebraid.com')).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when value ending is in stack of multiple values', async () => {
|
||||
expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@yahoo.com', '@wearebraid.com', '@gmail.com')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* In rule
|
||||
@ -85,217 +300,6 @@ describe('matches', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Accepted rule
|
||||
*/
|
||||
describe('accepted', () => {
|
||||
it('passes with true', async () => expect(await rules.accepted({ value: 'yes' })).toBe(true))
|
||||
|
||||
it('passes with on', async () => expect(await rules.accepted({ value: 'on' })).toBe(true))
|
||||
|
||||
it('passes with 1', async () => expect(await rules.accepted({ value: '1' })).toBe(true))
|
||||
|
||||
it('passes with number 1', async () => expect(await rules.accepted({ value: 1 })).toBe(true))
|
||||
|
||||
it('passes with boolean true', async () => expect(await rules.accepted({ value: true })).toBe(true))
|
||||
|
||||
it('fail with boolean false', async () => expect(await rules.accepted({ value: false })).toBe(false))
|
||||
|
||||
it('fail with "false"', async () => expect(await rules.accepted({ value: '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({ value: 'http://google.com' })).toBe(true))
|
||||
|
||||
it('fails with google.com', async () => expect(await rules.url({ value: 'google.com' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Determines if the string is a date
|
||||
*/
|
||||
describe('date', () => {
|
||||
it('passes with month day year', async () => expect(await rules.date({ value: 'December 17, 2020' })).toBe(true))
|
||||
|
||||
it('passes with month day', async () => expect(await rules.date({ value: 'December 17' })).toBe(true))
|
||||
|
||||
it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17' })).toBe(true))
|
||||
|
||||
it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17 12:34:15' })).toBe(true))
|
||||
|
||||
it('passes with out of bounds number', async () => expect(await rules.date({ value: 'January 77' })).toBe(true))
|
||||
|
||||
it('passes with only month', async () => expect(await rules.date({ value: 'January' })).toBe(false))
|
||||
|
||||
it('passes with valid date format', async () => expect(await rules.date({ value: '12/17/1987' }, 'MM/DD/YYYY')).toBe(true))
|
||||
|
||||
it('fails with simple number and date format', async () => expect(await rules.date({ value: '1234' }, 'MM/DD/YYYY')).toBe(false))
|
||||
|
||||
it('fails with only day of week', async () => expect(await rules.date({ value: 'saturday' })).toBe(false))
|
||||
|
||||
it('fails with random string', async () => expect(await rules.date({ value: 'Pepsi 17' })).toBe(false))
|
||||
|
||||
it('fails with random number', async () => expect(await rules.date({ value: '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 tomorrow’s date object', async () => expect(await rules.after({ value: tomorrow })).toBe(true))
|
||||
|
||||
it('passes with future date', async () => expect(await rules.after({ value: 'January 15, 2999' })).toBe(true))
|
||||
|
||||
it('passes with long past date', async () => expect(await rules.after({ value: yesterday }, 'Jan 15, 2000')).toBe(true))
|
||||
|
||||
it('fails with yesterday’s date', async () => expect(await rules.after({ value: yesterday })).toBe(false))
|
||||
|
||||
it('fails with old date string', async () => expect(await rules.after({ value: 'January, 2000' })).toBe(false))
|
||||
|
||||
it('fails with invalid value', async () => expect(await rules.after({ value: '' })).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 tomorrow’s date object', async () => expect(await rules.before({ value: tomorrow })).toBe(false))
|
||||
|
||||
it('fails with future date', async () => expect(await rules.before({ value: 'January 15, 2999' })).toBe(false))
|
||||
|
||||
it('fails with long past date', async () => expect(await rules.before({ value: yesterday }, 'Jan 15, 2000')).toBe(false))
|
||||
|
||||
it('passes with yesterday’s date', async () => expect(await rules.before({ value: yesterday })).toBe(true))
|
||||
|
||||
it('passes with old date string', async () => expect(await rules.before({ value: 'January, 2000' })).toBe(true))
|
||||
|
||||
it('fails with invalid value', async () => expect(await rules.after({ value: '' })).toBe(false))
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Checks if a date is after another date
|
||||
*/
|
||||
describe('alpha', () => {
|
||||
it('passes with simple string', async () => expect(await rules.alpha({ value: 'abc' })).toBe(true))
|
||||
|
||||
it('passes with long string', async () => expect(await rules.alpha({ value: 'lkashdflaosuihdfaisudgflakjsdbflasidufg' })).toBe(true))
|
||||
|
||||
it('passes with single character', async () => expect(await rules.alpha({ value: 'z' })).toBe(true))
|
||||
|
||||
it('passes with accented character', async () => expect(await rules.alpha({ value: 'jüstin' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'russian')).toBe(true))
|
||||
|
||||
it('fails with lots of accented characters if latin', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'latin')).toBe(false))
|
||||
|
||||
it('fails with numbers', async () => expect(await rules.alpha({ value: 'justin83' })).toBe(false))
|
||||
|
||||
it('fails with symbols', async () => expect(await rules.alpha({ value: '-justin' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a date is after another date
|
||||
*/
|
||||
describe('number', () => {
|
||||
it('passes with simple number string', async () => expect(await rules.number({ value: '123' })).toBe(true))
|
||||
|
||||
it('passes with simple number', async () => expect(await rules.number({ value: 19832461234 })).toBe(true))
|
||||
|
||||
it('passes with float', async () => expect(await rules.number({ value: 198.32464 })).toBe(true))
|
||||
|
||||
it('passes with decimal in string', async () => expect(await rules.number({ value: '567.23' })).toBe(true))
|
||||
|
||||
it('fails with comma in number string', async () => expect(await rules.number({ value: '123,456' })).toBe(false))
|
||||
|
||||
it('fails with alpha', async () => expect(await rules.number({ value: '123sdf' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if a date alpha and numeric
|
||||
*/
|
||||
describe('alphanumeric', () => {
|
||||
it('passes with simple string', async () => expect(await rules.alphanumeric({ value: '567abc' })).toBe(true))
|
||||
|
||||
it('passes with long string', async () => expect(await rules.alphanumeric({ value: 'lkashdfla234osuihdfaisudgflakjsdbfla567sidufg' })).toBe(true))
|
||||
|
||||
it('passes with single character', async () => expect(await rules.alphanumeric({ value: 'z' })).toBe(true))
|
||||
|
||||
it('passes with accented character', async () => expect(await rules.alphanumeric({ value: 'jüst56in' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters', async () => expect(await rules.alphanumeric({ value: 'àáâ7567567äïíôöÆ' })).toBe(true))
|
||||
|
||||
it('passes with lots of accented characters if invalid set', async () => expect(await rules.alphanumeric({ value: '123123àáâäï67íôöÆ' }, 'russian')).toBe(true))
|
||||
|
||||
it('fails with lots of accented characters if latin', async () => expect(await rules.alphanumeric({ value: 'àáâäï123123íôöÆ' }, 'latin')).toBe(false))
|
||||
|
||||
it('fails with decimals in', async () => expect(await rules.alphanumeric({ value: 'abcABC99.123' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if between
|
||||
*/
|
||||
describe('between', () => {
|
||||
it('passes with simple number', async () => expect(await rules.between({ value: 5 }, 0, 10)).toBe(true))
|
||||
|
||||
it('passes with simple number string', async () => expect(await rules.between({ value: '5' }, '0', '10')).toBe(true))
|
||||
|
||||
it('passes with decimal number string', async () => expect(await rules.between({ value: '0.5' }, '0', '1')).toBe(true))
|
||||
|
||||
it('passes with string length', async () => expect(await rules.between({ value: 'abc' }, 2, 4)).toBe(true))
|
||||
|
||||
it('fails with string length too long', async () => expect(await rules.between({ value: 'abcdef' }, 2, 4)).toBe(false))
|
||||
|
||||
it('fails with string length too short', async () => expect(await rules.between({ value: 'abc' }, 3, 10)).toBe(false))
|
||||
|
||||
it('fails with number to small', async () => expect(await rules.between({ value: 0 }, 3, 10)).toBe(false))
|
||||
|
||||
it('fails with number to large', async () => expect(await rules.between({ value: 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({ value: 'dev+123@wearebraid.com' })).toBe(true))
|
||||
|
||||
it('passes numeric email', async () => expect(await rules.email({ value: '12345@google.com' })).toBe(true))
|
||||
|
||||
it('passes unicode email', async () => expect(await rules.email({ value: 'àlphä@❤️.ly' })).toBe(true))
|
||||
|
||||
it('passes numeric with new tld', async () => expect(await rules.email({ value: '12345@google.photography' })).toBe(true))
|
||||
|
||||
it('fails string without tld', async () => expect(await rules.email({ value: '12345@localhost' })).toBe(false))
|
||||
|
||||
it('fails string without tld', async () => expect(await rules.email({ value: '12345@localhost' })).toBe(false))
|
||||
|
||||
it('fails string without invalid name', async () => expect(await rules.email({ value: '1*(123)2345@localhost' })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Mime types.
|
||||
*/
|
||||
@ -395,31 +399,97 @@ describe('not', () => {
|
||||
})
|
||||
|
||||
/**
|
||||
* Confirm
|
||||
* Checks if a date is after another date
|
||||
*/
|
||||
describe('confirm', () => {
|
||||
it('passes when the values are the same strings', async () => expect(await rules.confirm(
|
||||
{ value: 'abc', name: 'password', getFormValues: () => ({ password_confirm: 'abc' }) }
|
||||
)).toBe(true))
|
||||
describe('number', () => {
|
||||
it('passes with simple number string', async () => expect(await rules.number({ value: '123' })).toBe(true))
|
||||
|
||||
it('passes when the values are the same integers', async () => expect(await rules.confirm(
|
||||
{ value: 4422132, name: 'xyz', getFormValues: () => ({ xyz_confirm: 4422132 }) }
|
||||
)).toBe(true))
|
||||
it('passes with simple number', async () => expect(await rules.number({ value: 19832461234 })).toBe(true))
|
||||
|
||||
it('passes when using a custom field', async () => expect(await rules.confirm(
|
||||
{ value: 4422132, name: 'name', getFormValues: () => ({ other_field: 4422132 }) },
|
||||
'other_field'
|
||||
)).toBe(true))
|
||||
it('passes with float', async () => expect(await rules.number({ value: 198.32464 })).toBe(true))
|
||||
|
||||
it('passes when using a field ends in _confirm', async () => expect(await rules.confirm(
|
||||
{ value: '$ecret', name: 'password_confirm', getFormValues: () => ({ password: '$ecret' }) }
|
||||
)).toBe(true))
|
||||
it('passes with decimal in string', async () => expect(await rules.number({ value: '567.23' })).toBe(true))
|
||||
|
||||
it('fails when using different strings', async () => expect(await rules.confirm(
|
||||
{ value: 'Justin', name: 'name', getFormValues: () => ({ name_confirm: 'Daniel' }) },
|
||||
)).toBe(false))
|
||||
it('fails with comma in number string', async () => expect(await rules.number({ value: '123,456' })).toBe(false))
|
||||
|
||||
it('fails when the types are different', async () => expect(await rules.confirm(
|
||||
{ value: '1234', name: 'num', getFormValues: () => ({ num_confirm: 1234 }) },
|
||||
)).toBe(false))
|
||||
it('fails with alpha', async () => expect(await rules.number({ value: '123sdf' })).toBe(false))
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Required rule
|
||||
*/
|
||||
describe('required', () => {
|
||||
it('fails on empty string', async () => expect(await rules.required({ value: '' })).toBe(false))
|
||||
|
||||
it('fails on empty array', async () => expect(await rules.required({ value: [] })).toBe(false))
|
||||
|
||||
it('fails on empty object', async () => expect(await rules.required({ value: {} })).toBe(false))
|
||||
|
||||
it('fails on null', async () => expect(await rules.required({ value: null })).toBe(false))
|
||||
|
||||
it('passes with the number zero', async () => expect(await rules.required({ value: 0 })).toBe(true))
|
||||
|
||||
it('passes with the boolean false', async () => expect(await rules.required({ value: false })).toBe(true))
|
||||
|
||||
it('passes with a non empty array', async () => expect(await rules.required({ value: ['123'] })).toBe(true))
|
||||
|
||||
it('passes with a non empty object', async () => expect(await rules.required({ value: {a: 'b'} })).toBe(true))
|
||||
|
||||
it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true))
|
||||
|
||||
it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true))
|
||||
|
||||
it('passes with FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [{ name: 'j.png' }] }) })).toBe(true))
|
||||
|
||||
it('fails with empty FileUpload', async () => expect(await rules.required({ value: new FileUpload({ files: [] }) })).toBe(false))
|
||||
})
|
||||
|
||||
/**
|
||||
* Checks if value starts with a one of the specified Strings.
|
||||
*/
|
||||
describe('startsWith', () => {
|
||||
it('fails when value starting is not in stack of single value', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza')).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when value starting is not in stack of multiple values', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza', 'coffee')).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when passed value is not a string', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday'}, ['taco', 'pizza'])).toBe(false)
|
||||
})
|
||||
|
||||
it('fails when passed value is not a string', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday'}, {value: 'taco'})).toBe(false)
|
||||
})
|
||||
|
||||
it('passes when a string value is present and matched even if non-string values also exist as arguments', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday'}, {value: 'taco'}, ['taco'], 'taco')).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when stack consists of zero values', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday' })).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when value starting is in stack of single value', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday' }, 'taco')).toBe(true)
|
||||
})
|
||||
|
||||
it('passes when value starting is in stack of multiple values', async () => {
|
||||
expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza', 'taco', 'coffee')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 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({ value: 'http://google.com' })).toBe(true))
|
||||
|
||||
it('fails with google.com', async () => expect(await rules.url({ value: 'google.com' })).toBe(false))
|
||||
})
|
@ -1,4 +1,4 @@
|
||||
import { parseRules, regexForFormat, cloneDeep, isValueType } from '@/libs/utils'
|
||||
import { parseRules, parseLocale, regexForFormat, cloneDeep, isValueType, snakeToCamel } from '@/libs/utils'
|
||||
import rules from '@/libs/rules'
|
||||
import FileUpload from '@/FileUpload';
|
||||
|
||||
@ -117,3 +117,40 @@ describe('cloneDeep', () => {
|
||||
expect(clone.b === c).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('snakeToCamel', () => {
|
||||
it('converts underscore separated words to camelCase', () => {
|
||||
expect(snakeToCamel('this_is_snake_case')).toBe('thisIsSnakeCase')
|
||||
})
|
||||
|
||||
it('converts underscore separated words to camelCase even if they start with a number', () => {
|
||||
expect(snakeToCamel('this_is_snake_case_2nd_example')).toBe('thisIsSnakeCase2ndExample')
|
||||
})
|
||||
|
||||
it('has no effect on already camelCase words', () => {
|
||||
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||
})
|
||||
|
||||
it('does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
||||
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||
})
|
||||
|
||||
it('ignores double underscores anywhere in a word', () => {
|
||||
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||
})
|
||||
|
||||
it('has no effect hyphenated words', () => {
|
||||
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('parseLocale', () => {
|
||||
it('properly orders the options', () => {
|
||||
expect(parseLocale('en-US-VA')).toEqual(['en-US-VA', 'en-US', 'en'])
|
||||
})
|
||||
|
||||
it('properly parses a single option', () => {
|
||||
expect(parseLocale('en')).toEqual(['en'])
|
||||
})
|
||||
})
|
@ -107,3 +107,15 @@
|
||||
// image uploads
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Form-level errors
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.formulate-form-errors {
|
||||
|
||||
.formulate-form-error {
|
||||
// form errors (not specific to a field)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -632,7 +632,27 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.formulate-form-errors {
|
||||
margin: .75em 0;
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.formulate-form-error {
|
||||
color: $formulate-error;
|
||||
font-size: .9em;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
margin-bottom: .25em;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user