1
0
mirror of synced 2024-11-22 05:16:05 +03:00

refactor: Validation callbacks logic simplification, typehints

This commit is contained in:
Zaytsev Kirill 2020-10-11 00:52:18 +03:00
parent 99c3f8a4cd
commit 45f29ff27a
10 changed files with 90 additions and 101 deletions

View File

@ -36,11 +36,11 @@ class FileUpload {
public context: ObjectType public context: ObjectType
public results: any[] | boolean public results: any[] | boolean
constructor (input: DataTransfer, context: ObjectType, options: ObjectType = {}) { constructor (input: DataTransfer, context: ObjectType = {}, options: ObjectType = {}) {
this.input = input this.input = input
this.fileList = input.files this.fileList = input.files
this.files = [] this.files = []
this.options = { ...{ mimes: {} }, ...options } this.options = { mimes: {}, ...options }
this.results = false this.results = false
this.context = context this.context = context
if (Array.isArray(this.fileList)) { if (Array.isArray(this.fileList)) {

View File

@ -1,7 +1,7 @@
import { VueConstructor } from 'vue' import { VueConstructor } from 'vue'
import library from './libs/library' import library from './libs/library'
import rules from './libs/rules' import rules from './validation/rules'
import mimes from './libs/mimes' import mimes from './libs/mimes'
import FileUpload from './FileUpload' import FileUpload from './FileUpload'
import RuleValidationMessages from './RuleValidationMessages' import RuleValidationMessages from './RuleValidationMessages'
@ -13,7 +13,7 @@ import FormularioForm from '@/FormularioForm.vue'
import FormularioInput from '@/FormularioInput.vue' import FormularioInput from '@/FormularioInput.vue'
import FormularioGrouping from './FormularioGrouping.vue' import FormularioGrouping from './FormularioGrouping.vue'
import { ObjectType } from '@/common.types' import { ObjectType } from '@/common.types'
import { ValidationContext } from '@/validation.types' import { ValidationContext } from '@/validation/types'
interface ErrorHandler { interface ErrorHandler {
(error: any, formName?: string): any (error: any, formName?: string): any

View File

@ -25,7 +25,8 @@ import {
Watch, Watch,
} from 'vue-property-decorator' } from 'vue-property-decorator'
import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils' import { shallowEqualObjects, parseRules, snakeToCamel, has, arrayify, groupBails } from './libs/utils'
import { ValidationError } from '@/validation.types' import { ValidationError } from '@/validation/types'
import { ObjectType } from '@/common.types'
const ERROR_BEHAVIOR = { const ERROR_BEHAVIOR = {
BLUR: 'blur', BLUR: 'blur',
@ -87,12 +88,12 @@ export default class FormularioInput extends Vue {
@Prop({ @Prop({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}) validationRules!: Object }) validationRules!: ObjectType
@Prop({ @Prop({
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}) validationMessages!: Object }) validationMessages!: ObjectType
@Prop({ @Prop({
type: [Array, String, Boolean], type: [Array, String, Boolean],
@ -112,16 +113,16 @@ export default class FormularioInput extends Vue {
@Prop({ default: true }) preventWindowDrops!: boolean @Prop({ default: true }) preventWindowDrops!: boolean
defaultId: string = this.$formulario.nextId(this) defaultId: string = this.$formulario.nextId(this)
localAttributes: Object = {} localAttributes: ObjectType = {}
localErrors: ValidationError[] = [] localErrors: ValidationError[] = []
proxy: Object = this.getInitialValue() proxy: ObjectType = this.getInitialValue()
behavioralErrorVisibility: boolean = this.errorBehavior === 'live' behavioralErrorVisibility: boolean = this.errorBehavior === 'live'
formShouldShowErrors: boolean = false formShouldShowErrors: boolean = false
validationErrors: [] = [] validationErrors: [] = []
pendingValidation: Promise = Promise.resolve() pendingValidation: Promise = Promise.resolve()
// These registries are used for injected messages registrants only (mostly internal). // These registries are used for injected messages registrants only (mostly internal).
ruleRegistry: [] = [] ruleRegistry: [] = []
messageRegistry: Object = {} messageRegistry: ObjectType = {}
get context () { get context () {
return this.defineModel({ return this.defineModel({
@ -322,6 +323,7 @@ export default class FormularioInput extends Vue {
this.performValidation() this.performValidation()
} }
// noinspection JSUnusedGlobalSymbols
beforeDestroy () { beforeDestroy () {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') { if (!this.disableErrors && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors) this.removeErrorObserver(this.setErrors)

View File

@ -1,6 +1,6 @@
import { Formulario } from '@/Formulario' import { Formulario } from '@/Formulario'
import FormularioInput from '@/FormularioInput.vue' import FormularioInput from '@/FormularioInput.vue'
import { ValidationContext } from '@/validation.types' import { ValidationContext } from '@/validation/types'
/** /**
* This is an object of functions that each produce valid responses. There's no * This is an object of functions that each produce valid responses. There's no
@ -216,6 +216,6 @@ const validationMessages = {
* This creates a vue-formulario plugin that can be imported and used on each * This creates a vue-formulario plugin that can be imported and used on each
* project. * project.
*/ */
export default function (instance: Formulario) { export default function (instance: Formulario): void {
instance.extend({ validationMessages }) instance.extend({ validationMessages })
} }

View File

@ -8,8 +8,8 @@ import FormularioInput from '@/FormularioInput.vue'
* important for features such as grouped fields. * important for features such as grouped fields.
*/ */
export default class Registry { export default class Registry {
public ctx: FormularioForm private ctx: FormularioForm
private registry: Map<string, FormularioForm> private registry: Map<string, FormularioInput>
/** /**
* Create a new registry of components. * Create a new registry of components.
@ -23,7 +23,7 @@ export default class Registry {
/** /**
* Add an item to the registry. * Add an item to the registry.
*/ */
add (name: string, component: FormularioForm) { add (name: string, component: FormularioInput) {
this.registry.set(name, component) this.registry.set(name, component)
return this return this
} }
@ -52,7 +52,7 @@ export default class Registry {
/** /**
* Get a particular registry value. * Get a particular registry value.
*/ */
get (key: string): FormularioForm | undefined { get (key: string): FormularioInput | undefined {
return this.registry.get(key) return this.registry.get(key)
} }

View File

@ -1,9 +1,18 @@
// @ts-ignore // @ts-ignore
import isUrl from 'is-url' import isUrl from 'is-url'
import FileUpload from '../FileUpload' import FileUpload from '../FileUpload'
import { shallowEqualObjects, regexForFormat, has } from './utils' import { shallowEqualObjects, regexForFormat, has } from '@/libs/utils'
import { ObjectType } from '@/common.types' import { ObjectType } from '@/common.types'
interface ValidatableData {
value: any,
}
interface ConfirmValidatableData extends ValidatableData {
getFormValues: () => ObjectType,
name: string,
}
/** /**
* Library of rules * Library of rules
*/ */
@ -11,23 +20,23 @@ export default {
/** /**
* Rule: the value must be "yes", "on", "1", or true * Rule: the value must be "yes", "on", "1", or true
*/ */
accepted ({ value }: { value: any }) { accepted ({ value }: ValidatableData): Promise<boolean> {
return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value)) return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value))
}, },
/** /**
* Rule: checks if a value is after a given date. Defaults to current time * Rule: checks if a value is after a given date. Defaults to current time
*/ */
after ({ value }: { value: string }, compare: string | false = false) { after ({ value }: { value: Date|string }, compare: string | false = false): Promise<boolean> {
const timestamp = compare !== false ? Date.parse(compare) : new Date() const timestamp = compare !== false ? Date.parse(compare) : Date.now()
const fieldValue = Date.parse(value) const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp)) return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue > timestamp))
}, },
/** /**
* Rule: checks if the value is only alpha * Rule: checks if the value is only alpha
*/ */
alpha ({ value }: { value: string }, set: string = 'default') { alpha ({ value }: { value: string }, set: string = 'default'): Promise<boolean> {
const sets = { const sets = {
default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z]+$/ latin: /^[a-zA-Z]+$/
@ -40,7 +49,7 @@ export default {
/** /**
* Rule: checks if the value is alpha numeric * Rule: checks if the value is alpha numeric
*/ */
alphanumeric ({ value }: { value: string }, set = 'default') { alphanumeric ({ value }: { value: string }, set = 'default'): Promise<boolean> {
const sets = { const sets = {
default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/, default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/,
latin: /^[a-zA-Z0-9]+$/ latin: /^[a-zA-Z0-9]+$/
@ -53,16 +62,16 @@ export default {
/** /**
* Rule: checks if a value is after a given date. Defaults to current time * Rule: checks if a value is after a given date. Defaults to current time
*/ */
before ({ value }: { value: string }, compare: string | false = false) { before ({ value }: { value: Date|string }, compare: string|false = false): Promise<boolean> {
const timestamp = compare !== false ? Date.parse(compare) : new Date() const timestamp = compare !== false ? Date.parse(compare) : Date.now()
const fieldValue = Date.parse(value) const fieldValue = value instanceof Date ? value.getTime() : Date.parse(value)
return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp)) return Promise.resolve(isNaN(fieldValue) ? false : (fieldValue < timestamp))
}, },
/** /**
* Rule: checks if the value is between two other values * Rule: checks if the value is between two other values
*/ */
between ({ value }: { value: string | number }, from: number = 0, to: number = 10, force: string) { between ({ value }: { value: string|number }, from: number|any = 0, to: number|any = 10, force?: string): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve((() => {
if (from === null || to === null || isNaN(from) || isNaN(to)) { if (from === null || to === null || isNaN(from) || isNaN(to)) {
return false return false
@ -85,7 +94,7 @@ export default {
* Confirm that the value of one field is the same as another, mostly used * Confirm that the value of one field is the same as another, mostly used
* for password confirmations. * for password confirmations.
*/ */
confirm ({ value, getFormValues, name }: { value: any, getFormValues: () => ObjectType, name: string }, field: string) { confirm ({ value, getFormValues, name }: ConfirmValidatableData, field?: string): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve((() => {
const formValues = getFormValues() const formValues = getFormValues()
let confirmationFieldName = field let confirmationFieldName = field
@ -100,64 +109,51 @@ export default {
* Rule: ensures the value is a date according to Date.parse(), or a format * Rule: ensures the value is a date according to Date.parse(), or a format
* regex. * regex.
*/ */
date ({ value }: { value: string }, format: string | false = false) { date ({ value }: { value: string }, format: string | false = false): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve(format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value)))
if (format) {
return regexForFormat(format).test(value)
}
return !isNaN(Date.parse(value))
})())
}, },
/** /**
* Rule: tests * Rule: tests
*/ */
email ({ value }: { value: string}) { email ({ value }: { value: string }): Promise<boolean> {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(true)
} }
// eslint-disable-next-line // eslint-disable-next-line
const isEmail = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i const isEmail = /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i
return Promise.resolve(isEmail.test(value)) return Promise.resolve(isEmail.test(value))
}, },
/** /**
* Rule: Value ends with one of the given Strings * Rule: Value ends with one of the given Strings
*/ */
endsWith: function ({ value }: any, ...stack: any[]) { endsWith ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(true)
} }
return Promise.resolve((() => { if (typeof value === 'string') {
if (typeof value === 'string' && stack.length) { return Promise.resolve(stack.length === 0 || stack.some(str => value.endsWith(str)))
return stack.find(item => { }
return value.endsWith(item)
}) !== undefined return Promise.resolve(false)
} else if (typeof value === 'string' && stack.length === 0) {
return true
}
return false
})())
}, },
/** /**
* Rule: Value is in an array (stack). * Rule: Value is in an array (stack).
*/ */
in: function ({ value }: any, ...stack: any[]) { in ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
return Promise.resolve(stack.find(item => { return Promise.resolve(stack.some(item => {
if (typeof item === 'object') { return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
return shallowEqualObjects(item, value) }))
}
return item === value
}) !== undefined)
}, },
/** /**
* Rule: Match the value against a (stack) of patterns or strings * Rule: Match the value against a (stack) of patterns or strings
*/ */
matches: function ({ value }: any, ...stack: any[]) { matches ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
return Promise.resolve(!!stack.find(pattern => { return Promise.resolve(!!stack.find(pattern => {
if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') { if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') {
pattern = new RegExp(pattern.substr(1, pattern.length - 2)) pattern = new RegExp(pattern.substr(1, pattern.length - 2))
@ -172,25 +168,22 @@ export default {
/** /**
* Check the file type is correct. * Check the file type is correct.
*/ */
mime: function ({ value }: any, ...types: string[]) { mime ({ value }: { value: any }, ...types: string[]): Promise<boolean> {
return Promise.resolve((() => { if (value instanceof FileUpload) {
if (value instanceof FileUpload) { const files = value.getFiles()
const fileList = value.getFiles() const isMimeCorrect = (file: File) => types.includes(file.type)
for (let i = 0; i < fileList.length; i++) { const allValid: boolean = files.reduce((valid: boolean, { file }) => valid && isMimeCorrect(file), true)
const file = fileList[i].file
if (!types.includes(file.type)) { return Promise.resolve(allValid)
return false }
}
} return Promise.resolve(true)
}
return true
})())
}, },
/** /**
* Check the minimum value of a particular. * Check the minimum value of a particular.
*/ */
min: function ({ value }: any, minimum = 1, force: string) { min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve((() => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
minimum = !isNaN(minimum) ? Number(minimum) : minimum minimum = !isNaN(minimum) ? Number(minimum) : minimum
@ -211,10 +204,10 @@ export default {
/** /**
* Check the maximum value of a particular. * Check the maximum value of a particular.
*/ */
max: function ({ value }: any, maximum = 10, force: string) { max ({ value }: { value: any }, maximum: string | number = 10, force?: string): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve((() => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
maximum = !isNaN(maximum) ? Number(maximum) : maximum maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum
return value.length <= maximum return value.length <= maximum
} }
if ((!isNaN(value) && force !== 'length') || force === 'value') { if ((!isNaN(value) && force !== 'length') || force === 'value') {
@ -232,26 +225,23 @@ export default {
/** /**
* Rule: Value is not in stack. * Rule: Value is not in stack.
*/ */
not: function ({ value }: any, ...stack: any[]) { not ({ value }: { value: any }, ...stack: any[]): Promise<boolean> {
return Promise.resolve(stack.find(item => { return Promise.resolve(!stack.some(item => {
if (typeof item === 'object') { return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value
return shallowEqualObjects(item, value) }))
}
return item === value
}) === undefined)
}, },
/** /**
* Rule: checks if the value is only alpha numeric * Rule: checks if the value is only alpha numeric
*/ */
number ({ value }: { value: any }) { number ({ value }: { value: any }): Promise<boolean> {
return Promise.resolve(!isNaN(value)) return Promise.resolve(!isNaN(Number(value)))
}, },
/** /**
* Rule: must be a value * Rule: must be a value
*/ */
required ({ value }: any, isRequired: string|boolean = true) { required ({ value }: { value: any }, isRequired: string|boolean = true): Promise<boolean> {
return Promise.resolve((() => { return Promise.resolve((() => {
if (!isRequired || ['no', 'false'].includes(isRequired as string)) { if (!isRequired || ['no', 'false'].includes(isRequired as string)) {
return true return true
@ -275,32 +265,29 @@ export default {
/** /**
* Rule: Value starts with one of the given Strings * Rule: Value starts with one of the given Strings
*/ */
startsWith ({ value }: { value: any }, ...stack: any[]) { startsWith ({ value }: { value: any }, ...stack: string[]): Promise<boolean> {
if (!value) { if (!value) {
return Promise.resolve(() => { return true }) return Promise.resolve(true)
} }
return Promise.resolve((() => { if (typeof value === 'string') {
if (typeof value === 'string' && stack.length) { return Promise.resolve(stack.length === 0 || stack.some(str => value.startsWith(str)))
return stack.find(item => value.startsWith(item)) !== undefined }
} else if (typeof value === 'string' && stack.length === 0) {
return true return Promise.resolve(false)
}
return false
})())
}, },
/** /**
* Rule: checks if a string is a valid url * Rule: checks if a string is a valid url
*/ */
url ({ value }: { value: string }) { url ({ value }: { value: string }): Promise<boolean> {
return Promise.resolve(isUrl(value)) return Promise.resolve(isUrl(value))
}, },
/** /**
* Rule: not a true rule more like a compiler flag. * Rule: not a true rule more like a compiler flag.
*/ */
bail () { bail (): Promise<boolean> {
return Promise.resolve(true) return Promise.resolve(true)
} }
} }

View File

@ -14,8 +14,8 @@ describe('FormularioGrouping', () => {
slots: { slots: {
default: ` default: `
<FormularioGrouping name="sub"> <FormularioGrouping name="sub">
<FormularioInput name="text" v-slot="vSlot"> <FormularioInput name="text" v-slot="{ context }">
<input type="text" v-model="vSlot.context.model"> <input type="text" v-model="context.model">
</FormularioInput> </FormularioInput>
</FormularioGrouping> </FormularioGrouping>
` `

View File

@ -1,4 +1,4 @@
import rules from '@/libs/rules' import rules from '@/validation/rules.ts'
import FileUpload from '../../src/FileUpload' import FileUpload from '../../src/FileUpload'

View File

@ -1,6 +1,6 @@
import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils' import { parseRules, parseLocale, regexForFormat, cloneDeep, isScalar, snakeToCamel, groupBails } from '@/libs/utils'
import rules from '@/libs/rules' import rules from '@/validation/rules.ts'
import FileUpload from '@/FileUpload'; import FileUpload from '@/FileUpload'
describe('parseRules', () => { describe('parseRules', () => {
it('parses single string rules, returning empty arguments array', () => { it('parses single string rules, returning empty arguments array', () => {