From 45f29ff27a2290880c5d9b1e920c6fda1ee77746 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Sun, 11 Oct 2020 00:52:18 +0300 Subject: [PATCH] refactor: Validation callbacks logic simplification, typehints --- src/FileUpload.ts | 4 +- src/Formulario.ts | 4 +- src/FormularioInput.vue | 14 +- src/RuleValidationMessages.ts | 4 +- src/libs/registry.ts | 8 +- src/{libs => validation}/rules.ts | 147 ++++++++---------- .../types.ts} | 0 test/unit/FormularioGrouping.test.js | 4 +- test/unit/rules.test.js | 2 +- test/unit/utils.test.js | 4 +- 10 files changed, 90 insertions(+), 101 deletions(-) rename src/{libs => validation}/rules.ts (64%) rename src/{validation.types.ts => validation/types.ts} (100%) diff --git a/src/FileUpload.ts b/src/FileUpload.ts index 6210268..272dd15 100644 --- a/src/FileUpload.ts +++ b/src/FileUpload.ts @@ -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)) { diff --git a/src/Formulario.ts b/src/Formulario.ts index dc9061f..89695c3 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -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 diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index 7806652..322d36d 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -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) diff --git a/src/RuleValidationMessages.ts b/src/RuleValidationMessages.ts index f2b611f..603f3fd 100644 --- a/src/RuleValidationMessages.ts +++ b/src/RuleValidationMessages.ts @@ -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 }) } diff --git a/src/libs/registry.ts b/src/libs/registry.ts index 7e9255b..90ca5d3 100644 --- a/src/libs/registry.ts +++ b/src/libs/registry.ts @@ -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 + private ctx: FormularioForm + private registry: Map /** * 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) } diff --git a/src/libs/rules.ts b/src/validation/rules.ts similarity index 64% rename from src/libs/rules.ts rename to src/validation/rules.ts index 2cb9c1a..fb13af4 100644 --- a/src/libs/rules.ts +++ b/src/validation/rules.ts @@ -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 { 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 { + 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 { 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 { 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 { + 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 { 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 { 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 { + return Promise.resolve(format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value))) }, /** * Rule: tests */ - email ({ value }: { value: string}) { + email ({ value }: { value: string }): Promise { 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 { 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 { + 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 { 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 { + 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 { 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 { 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 { + 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 { + 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 { 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 { 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 { return Promise.resolve(isUrl(value)) }, /** * Rule: not a true rule — more like a compiler flag. */ - bail () { + bail (): Promise { return Promise.resolve(true) } } diff --git a/src/validation.types.ts b/src/validation/types.ts similarity index 100% rename from src/validation.types.ts rename to src/validation/types.ts diff --git a/test/unit/FormularioGrouping.test.js b/test/unit/FormularioGrouping.test.js index cc6dcf3..37fa8e0 100644 --- a/test/unit/FormularioGrouping.test.js +++ b/test/unit/FormularioGrouping.test.js @@ -14,8 +14,8 @@ describe('FormularioGrouping', () => { slots: { default: ` - - + + ` diff --git a/test/unit/rules.test.js b/test/unit/rules.test.js index 3052028..b91ba00 100644 --- a/test/unit/rules.test.js +++ b/test/unit/rules.test.js @@ -1,4 +1,4 @@ -import rules from '@/libs/rules' +import rules from '@/validation/rules.ts' import FileUpload from '../../src/FileUpload' diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index 0b2edd2..63506eb 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -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', () => {