Adds validation rules endsWith, startsWith and fixes box classification blur handler
* adds language definitions for new rules * re-order rules file to order rule definitions alphabetically * adds support for endsWith validation rule and converts snake_case rules to camelCase function calls * adds support for startsWith field validation * better en.js definitions for new validations * adds tests for snakeCaseToCamelCase function * coerces all validation messages and validation rules to be camelCase under the hood * ensures that array syntax rules are properly converted internally to camelCase * adds more robust tests for non-string type data for endsWith and startsWith validation rules * adds support for words that start with numbers to snakeToCamel method renames snakeCaseToCamelCase to snakeToCamel to reduce package size * Reduces some property name lengths for byte savings * Fixes bug that caused validation rules to not be displayed on blur for the box classification Co-authored-by: Justin Schroeder <justin@wearebraid.com>
This commit is contained in:
parent
4bfe43719d
commit
4b36b9c4ba
11 changed files with 105 additions and 33 deletions
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
2
dist/formulate.umd.js
vendored
2
dist/formulate.umd.js
vendored
File diff suppressed because one or more lines are too long
|
@ -55,7 +55,7 @@
|
|||
|
||||
<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 {
|
||||
|
@ -198,6 +198,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 +272,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,13 +298,14 @@ 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)
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
v-model="context.model"
|
||||
v-bind="optionContext"
|
||||
class="formulate-input-group-item"
|
||||
@blur="context.blurHandler"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -196,6 +196,7 @@ function hasErrors () {
|
|||
* Bound into the context object.
|
||||
*/
|
||||
function blurHandler () {
|
||||
this.$emit('blur')
|
||||
if (this.errorBehavior === 'blur') {
|
||||
this.behavioralErrorVisibility = true
|
||||
}
|
||||
|
|
|
@ -118,12 +118,14 @@ export default {
|
|||
*/
|
||||
endsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (stack.length) {
|
||||
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 true
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
|
@ -259,12 +261,14 @@ export default {
|
|||
*/
|
||||
startsWith: function ({ value }, ...stack) {
|
||||
return Promise.resolve((() => {
|
||||
if (stack.length) {
|
||||
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 true
|
||||
return false
|
||||
})())
|
||||
},
|
||||
|
||||
|
|
|
@ -75,13 +75,16 @@ export function shallowEqualObjects (objA, objB) {
|
|||
* Given a string, convert snake_case to camelCase
|
||||
* @param {String} string
|
||||
*/
|
||||
export function snakeCaseToCamelCase (string) {
|
||||
return string.replace(/([_][a-z])/ig, ($1) => {
|
||||
if (string.indexOf($1) !== 0 && string[string.indexOf($1) - 1] !== '_') {
|
||||
return $1.toUpperCase().replace('_', '')
|
||||
}
|
||||
return $1
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -132,7 +135,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
|
||||
*/
|
||||
|
@ -141,7 +144,7 @@ function parseRule (rule, rules) {
|
|||
return [rule, []]
|
||||
}
|
||||
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]
|
||||
}
|
||||
|
@ -151,7 +154,7 @@ function parseRule (rule, rules) {
|
|||
}
|
||||
if (typeof rule === 'string') {
|
||||
const segments = rule.split(':')
|
||||
const functionName = snakeCaseToCamelCase(segments.shift())
|
||||
const functionName = snakeToCamel(segments.shift())
|
||||
if (rules.hasOwnProperty(functionName)) {
|
||||
return [rules[functionName], segments.length ? segments.join(':').split(',') : []]
|
||||
} else {
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -221,6 +221,18 @@ describe('endsWith', () => {
|
|||
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)
|
||||
})
|
||||
|
@ -445,6 +457,18 @@ describe('startsWith', () => {
|
|||
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)
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { parseRules, regexForFormat, cloneDeep, isValueType, snakeCaseToCamelCase } from '@/libs/utils'
|
||||
import { parseRules, regexForFormat, cloneDeep, isValueType, snakeToCamel } from '@/libs/utils'
|
||||
import rules from '@/libs/rules'
|
||||
import FileUpload from '@/FileUpload';
|
||||
|
||||
|
@ -118,24 +118,28 @@ describe('cloneDeep', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('snakeCaseToCamelCase', () => {
|
||||
describe('snakeToCamel', () => {
|
||||
it('converts underscore separated words to camelCase', () => {
|
||||
expect(snakeCaseToCamelCase('this_is_snake_case')).toBe('thisIsSnakeCase')
|
||||
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(snakeCaseToCamelCase('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||
expect(snakeToCamel('thisIsCamelCase')).toBe('thisIsCamelCase')
|
||||
})
|
||||
|
||||
it('does not capitalize the first word or strip first underscore if a phrase starts with an underscore', () => {
|
||||
expect(snakeCaseToCamelCase('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||
expect(snakeToCamel('_this_starts_with_an_underscore')).toBe('_thisStartsWithAnUnderscore')
|
||||
})
|
||||
|
||||
it('ignores double underscores anywhere in a word', () => {
|
||||
expect(snakeCaseToCamelCase('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||
expect(snakeToCamel('__unlikely__thing__')).toBe('__unlikely__thing__')
|
||||
})
|
||||
|
||||
it('has no effect hyphenated words', () => {
|
||||
expect(snakeCaseToCamelCase('not-a-good-name')).toBe('not-a-good-name')
|
||||
expect(snakeToCamel('not-a-good-name')).toBe('not-a-good-name')
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue