1
0
Fork 0
mirror of synced 2025-04-02 04:46:12 +03:00

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:
Justin Schroeder 2020-03-04 13:45:37 -05:00 committed by GitHub
parent 4bfe43719d
commit 4b36b9c4ba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 105 additions and 33 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

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

View file

@ -7,6 +7,7 @@
v-model="context.model"
v-bind="optionContext"
class="formulate-input-group-item"
@blur="context.blurHandler"
/>
</div>
</template>

View file

@ -196,6 +196,7 @@ function hasErrors () {
* Bound into the context object.
*/
function blurHandler () {
this.$emit('blur')
if (this.errorBehavior === 'blur') {
this.behavioralErrorVisibility = true
}

View file

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

View file

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

View file

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

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

View file

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