1
0
mirror of synced 2024-11-24 22:36:02 +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 results: any[] | boolean
constructor (input: DataTransfer, context: ObjectType, options: ObjectType = {}) {
constructor (input: DataTransfer, context: ObjectType = {}, options: ObjectType = {}) {
this.input = input
this.fileList = input.files
this.files = []
this.options = { ...{ mimes: {} }, ...options }
this.options = { mimes: {}, ...options }
this.results = false
this.context = context
if (Array.isArray(this.fileList)) {

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Formulario } from '@/Formulario'
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
@ -216,6 +216,6 @@ const validationMessages = {
* This creates a vue-formulario plugin that can be imported and used on each
* project.
*/
export default function (instance: Formulario) {
export default function (instance: Formulario): void {
instance.extend({ validationMessages })
}

View File

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

View File

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

View File

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

View File

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

View File

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