1
0
mirror of synced 2024-11-22 13:26:06 +03:00

Merge pull request #23 from wearebraid/release/2.2.0

Release/2.2.0
This commit is contained in:
Justin Schroeder 2020-03-07 10:39:47 -05:00 committed by GitHub
commit 2456aead9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2282 additions and 950 deletions

View File

@ -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(),

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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 its 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"]}

File diff suppressed because one or more lines are too long

15
dist/snow.css vendored
View File

@ -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

File diff suppressed because one or more lines are too long

1013
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -1,5 +1,4 @@
import nanoid from 'nanoid/non-secure'
import mimes from './libs/mimes'
/**
* The file upload class holds and represents a files 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,

View File

@ -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
View 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>

View File

@ -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()

View File

@ -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 => {

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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 {}
}

View File

@ -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')
}

View 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'
}

View File

@ -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
*/

View File

@ -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
View 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).

View File

@ -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 isnt 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.`
}
}

View File

@ -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
View 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')
})
})

View File

@ -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)
})
})

View File

@ -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'

View File

@ -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)
})
})

View File

@ -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'

View File

@ -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

View File

@ -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)

View File

@ -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)
})
})

View File

@ -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 tomorrows 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 yesterdays 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 tomorrows 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 yesterdays 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 tomorrows 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 yesterdays 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 tomorrows 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 yesterdays 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))
})

View File

@ -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'])
})
})

View File

@ -107,3 +107,15 @@
// image uploads
}
}
// Form-level errors
// -----------------------------------------------------------------------------
.formulate-form-errors {
.formulate-form-error {
// form errors (not specific to a field)
}
}

View File

@ -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;
}
}