From 98b5c0935d1cf8e5d89d145d60efaef51df496bc Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Wed, 28 Oct 2020 21:25:20 +0300 Subject: [PATCH 01/14] test: Restored remaining tests for form --- test/unit/FormularioForm.test.js | 67 +++++++++++++++----------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/test/unit/FormularioForm.test.js b/test/unit/FormularioForm.test.js index e644f02..85106ac 100644 --- a/test/unit/FormularioForm.test.js +++ b/test/unit/FormularioForm.test.js @@ -48,7 +48,7 @@ describe('FormularioForm', () => { ` } }) - expect(wrapper.vm.registry.keys()).toEqual(['sub1', 'sub2']) + expect(wrapper.vm['registry'].keys()).toEqual(['sub1', 'sub2']) }) it('Removes subcomponents from the registry', async () => { @@ -62,10 +62,10 @@ describe('FormularioForm', () => { ` }) await flushPromises() - expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['sub1', 'sub2']) + expect(wrapper.findComponent(FormularioForm).vm['registry'].keys()).toEqual(['sub1', 'sub2']) wrapper.setData({ active: false }) await flushPromises() - expect(wrapper.findComponent(FormularioForm).vm.registry.keys()).toEqual(['sub2']) + expect(wrapper.findComponent(FormularioForm).vm['registry'].keys()).toEqual(['sub2']) }) it('Getting nested fields from registry', async () => { @@ -347,10 +347,21 @@ describe('FormularioForm', () => { it('Emits correct validation event on entry', async () => { const wrapper = mount(FormularioForm, { slots: { default: ` - - + + - + ` } }) wrapper.find('input[type="text"]').setValue('bar') @@ -361,37 +372,21 @@ describe('FormularioForm', () => { expect(wrapper.emitted('validation')).toBeTruthy() expect(wrapper.emitted('validation').length).toBe(1) expect(wrapper.emitted('validation')[0][0]).toEqual({ - name: 'foo', - violations: [ expect.any(Object) ], // @TODO: Check object structure + name: 'firstField', + violations: [ { + rule: expect.any(String), + args: ['foo'], + context: { + value: 'bar', + formValues: expect.any(Object), + name: 'firstField', + }, + message: expect.any(String), + } ], }) }) - return - - it('Removes field data when that field is de-registered', async () => { - const wrapper = mount({ - data: () => ({ values: {} }), - template: ` - - - - - - - `, - }) - - await flushPromises() - - wrapper.find('input[type="text"]').setValue('bar') - - await flushPromises() - - expect(wrapper.findComponent(FormularioForm).vm.proxy).toEqual({ foo: 'bar' }) - expect(wrapper.vm['values']).toEqual({ foo: 'bar' }) - }) - - it('Allows resetting a form, hiding validation and clearing inputs.', async () => { + it('Allows resetting a form, wiping validation.', async () => { const wrapper = mount({ data: () => ({ values: {} }), template: ` @@ -411,6 +406,7 @@ describe('FormularioForm', () => { }) const password = wrapper.find('input[type="password"]') + password.setValue('foo') password.trigger('blur') @@ -422,10 +418,9 @@ describe('FormularioForm', () => { // First make sure we caught the errors expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(1) wrapper.vm.$refs.form.resetValidation() - wrapper.vm.$refs.form.setValues({ }) await flushPromises() + expect(Object.keys(wrapper.vm.$refs.form.mergedFieldErrors).length).toBe(0) - expect(wrapper.vm['values']).toEqual({}) }) }) From edf1fc63407b83f0d204d0cd4a209a2fb54431af Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Wed, 28 Oct 2020 22:45:11 +0300 Subject: [PATCH 02/14] fix: cloning dates in clone util --- src/utils/clone.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/utils/clone.ts b/src/utils/clone.ts index 0492ace..acda4ec 100644 --- a/src/utils/clone.ts +++ b/src/utils/clone.ts @@ -14,7 +14,13 @@ export default function clone (value: any): any { for (const key in value) { if (has(value, key)) { - copy[key] = isScalar(value[key]) ? value[key] : clone(value[key]) + if (isScalar(value[key])) { + copy[key] = value[key] + } else if (value instanceof Date) { + copy[key] = new Date(copy[key]) + } else { + copy[key] = clone(value[key]) + } } } From 904119deb809667ca835cb84bd5b4644a19d2c57 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Wed, 28 Oct 2020 22:46:29 +0300 Subject: [PATCH 03/14] refactor: Renamed register() method to add(), old add() method removed --- src/FormularioForm.vue | 15 ++++----- src/FormularioInput.vue | 5 +-- src/form/registry.ts | 67 ++++++++++++++++++----------------------- 3 files changed, 39 insertions(+), 48 deletions(-) diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index 3935375..4dfc006 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -126,7 +126,7 @@ export default class FormularioForm extends Vue { @Provide('formularioRegister') register (field: string, component: FormularioInput): void { - this.registry.register(field, component) + this.registry.add(field, component) } @Provide('formularioDeregister') @@ -154,7 +154,7 @@ export default class FormularioForm extends Vue { const newValue = getNested(values, registryKey) if (!shallowEqualObjects(newValue, oldValue)) { - this.setFieldValue(registryKey, newValue, false) + this.setFieldValue(registryKey, newValue) proxyHasChanges = true } @@ -171,8 +171,7 @@ export default class FormularioForm extends Vue { } } - @Provide('formularioSetter') - setFieldValue (field: string, value: any, emit = true): void { + setFieldValue (field: string, value: any): void { if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { [field]: value, ...proxy } = this.proxy @@ -180,10 +179,12 @@ export default class FormularioForm extends Vue { } else { setNested(this.proxy, field, value) } + } - if (emit) { - this.$emit('input', Object.assign({}, this.proxy)) - } + @Provide('formularioSetter') + setFieldValueAndEmit (field: string, value: any): void { + this.setFieldValue(field, value) + this.$emit('input', { ...this.proxy }) } setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record }): void { diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index c2247a3..bbfc003 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -69,10 +69,7 @@ export default class FormularioInput extends Vue { get model (): any { const model = this.hasModel ? 'value' : 'proxy' - if (this[model] === undefined) { - return '' - } - return this[model] + return this[model] !== undefined ? this[model] : '' } set model (value: any) { diff --git a/src/form/registry.ts b/src/form/registry.ts index 8319922..98e917b 100644 --- a/src/form/registry.ts +++ b/src/form/registry.ts @@ -20,10 +20,37 @@ export default class Registry { } /** - * Add an item to the registry. + * Fully register a component. + * @param {string} field name of the field. + * @param {FormularioForm} component the actual component instance. */ - add (name: string, component: FormularioInput): void { - this.registry.set(name, component) + add (field: string, component: FormularioInput): void { + if (this.registry.has(field)) { + return + } + this.registry.set(field, component) + const hasModel = has(component.$options.propsData || {}, 'value') + if ( + !hasModel && + // @ts-ignore + this.ctx.hasInitialValue && + // @ts-ignore + getNested(this.ctx.initialValues, field) !== undefined + ) { + // In the case that the form is carrying an initial value and the + // element is not, set it directly. + // @ts-ignore + component.context.model = getNested(this.ctx.initialValues, field) + } else if ( + hasModel && + // @ts-ignore + !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field)) + ) { + // In this case, the field is v-modeled or has an initial value and the + // form has no value or a different value, so use the field value + // @ts-ignore + this.ctx.setFieldValueAndEmit(field, component.proxy) + } } /** @@ -103,40 +130,6 @@ export default class Registry { return Array.from(this.registry.keys()) } - /** - * Fully register a component. - * @param {string} field name of the field. - * @param {FormularioForm} component the actual component instance. - */ - register (field: string, component: FormularioInput): void { - if (this.registry.has(field)) { - return - } - this.registry.set(field, component) - const hasModel = has(component.$options.propsData || {}, 'value') - if ( - !hasModel && - // @ts-ignore - this.ctx.hasInitialValue && - // @ts-ignore - getNested(this.ctx.initialValues, field) !== undefined - ) { - // In the case that the form is carrying an initial value and the - // element is not, set it directly. - // @ts-ignore - component.context.model = getNested(this.ctx.initialValues, field) - } else if ( - hasModel && - // @ts-ignore - !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field)) - ) { - // In this case, the field is v-modeled or has an initial value and the - // form has no value or a different value, so use the field value - // @ts-ignore - this.ctx.setFieldValue(field, component.proxy) - } - } - /** * Reduce the registry. * @param {function} callback From aa35a16a9d4b111f7a464e3fcfeaaca4f6c2762b Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Mon, 2 Nov 2020 18:55:03 +0300 Subject: [PATCH 04/14] chore: Replaced old stories --- storybook/stories/ExampleAddressList.tale.vue | 122 ++++++++++++++++++ storybook/stories/FormularioGrouping.tale.vue | 117 +++++++++++++---- .../stories/FormularioGroupingGroup.tale.vue | 106 --------------- storybook/stories/FormularioInput.tale.vue | 78 ----------- storybook/stories/index.stories.js | 10 +- 5 files changed, 218 insertions(+), 215 deletions(-) create mode 100644 storybook/stories/ExampleAddressList.tale.vue delete mode 100644 storybook/stories/FormularioGroupingGroup.tale.vue delete mode 100644 storybook/stories/FormularioInput.tale.vue diff --git a/storybook/stories/ExampleAddressList.tale.vue b/storybook/stories/ExampleAddressList.tale.vue new file mode 100644 index 0000000..f010758 --- /dev/null +++ b/storybook/stories/ExampleAddressList.tale.vue @@ -0,0 +1,122 @@ + + + + + diff --git a/storybook/stories/FormularioGrouping.tale.vue b/storybook/stories/FormularioGrouping.tale.vue index 03dd074..d42333a 100644 --- a/storybook/stories/FormularioGrouping.tale.vue +++ b/storybook/stories/FormularioGrouping.tale.vue @@ -1,41 +1,106 @@ diff --git a/storybook/stories/FormularioGroupingGroup.tale.vue b/storybook/stories/FormularioGroupingGroup.tale.vue deleted file mode 100644 index d42333a..0000000 --- a/storybook/stories/FormularioGroupingGroup.tale.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/storybook/stories/FormularioInput.tale.vue b/storybook/stories/FormularioInput.tale.vue deleted file mode 100644 index 651e8d1..0000000 --- a/storybook/stories/FormularioInput.tale.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - diff --git a/storybook/stories/index.stories.js b/storybook/stories/index.stories.js index 7de7f1c..c0c491d 100644 --- a/storybook/stories/index.stories.js +++ b/storybook/stories/index.stories.js @@ -5,8 +5,8 @@ import { storiesOf } from '@storybook/vue' import Vue from 'vue' import VueFormulario from '../../dist/formulario.esm' -import FormularioGroupingTale from './FormularioGrouping.tale' -import FormularioInputTale from './FormularioInput.tale' +import ExampleAddressList from './ExampleAddressList.tale' +import FormularioGrouping from './FormularioGrouping.tale' Vue.mixin({ methods: { @@ -20,6 +20,6 @@ Vue.mixin({ }) Vue.use(VueFormulario) -storiesOf('FormularioInput', module) - .add('Default', () => FormularioInputTale) - .add('Grouping', () => FormularioGroupingTale) +storiesOf('Examples', module) + .add('Address list', () => ExampleAddressList) + .add('FormularioGrouping', () => FormularioGrouping) From a0345a605e79e1283fa779e47ad38c0c177d9b2c Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Mon, 2 Nov 2020 20:34:09 +0300 Subject: [PATCH 05/14] refactor: Moved install method out of Formulario class --- src/Formulario.ts | 20 ++--------- src/index.ts | 28 +++++++++++++-- test/unit/Formulario.test.js | 70 +++++++++++++++++++++++++++++------- 3 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index 90f046e..401d2c4 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -1,20 +1,14 @@ -import { VueConstructor } from 'vue' - import merge from '@/utils/merge' import validationRules from '@/validation/rules' import validationMessages from '@/validation/messages' -import FormularioForm from '@/FormularioForm.vue' -import FormularioGrouping from '@/FormularioGrouping.vue' -import FormularioInput from '@/FormularioInput.vue' - import { ValidationContext, CheckRuleFn, CreateMessageFn, } from '@/validation/validator' -interface FormularioOptions { +export interface FormularioOptions { validationRules?: any; validationMessages?: Record; } @@ -27,19 +21,9 @@ export default class Formulario { public validationRules: Record = {} public validationMessages: Record = {} - constructor () { + constructor (options?: FormularioOptions) { this.validationRules = validationRules this.validationMessages = validationMessages - } - - /** - * Install vue formulario, and register it’s components. - */ - install (Vue: VueConstructor, options?: FormularioOptions): void { - Vue.prototype.$formulario = this - Vue.component('FormularioForm', FormularioForm) - Vue.component('FormularioGrouping', FormularioGrouping) - Vue.component('FormularioInput', FormularioInput) this.extend(options || {}) } diff --git a/src/index.ts b/src/index.ts index 5bf2957..a8e34b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,27 @@ -import Formulario from '@/Formulario.ts' +import Formulario, { FormularioOptions } from '@/Formulario.ts' +import { VueConstructor } from 'vue' +import FormularioForm from '@/FormularioForm.vue' +import FormularioGrouping from '@/FormularioGrouping.vue' +import FormularioInput from '@/FormularioInput.vue' -export default new Formulario() +export default { + install (Vue: VueConstructor, options?: FormularioOptions): void { + Vue.component('FormularioForm', FormularioForm) + Vue.component('FormularioGrouping', FormularioGrouping) + Vue.component('FormularioInput', FormularioInput) + + Vue.mixin({ + beforeCreate () { + const o = this.$options as Record + + if (typeof o.formulario === 'function') { + this.$formulario = o.formulario() + } else if (o.parent && o.parent.$formulario) { + this.$formulario = o.parent.$formulario + } else { + this.$formulario = new Formulario(options) + } + } + }) + }, +} diff --git a/test/unit/Formulario.test.js b/test/unit/Formulario.test.js index 813de14..4cbb10f 100644 --- a/test/unit/Formulario.test.js +++ b/test/unit/Formulario.test.js @@ -1,18 +1,62 @@ -import Formulario from '@/index.ts' +import { createLocalVue, mount } from '@vue/test-utils' +import Formulario from '@/Formulario.ts' +import plugin from '@/index.ts' describe('Formulario', () => { it('Installs on vue instance', () => { - const registry = [] - function Vue () {} - Vue.component = function (name, instance) { - registry.push(name) - } - Formulario.install(Vue) - expect(Vue.prototype.$formulario).toBe(Formulario) - expect(registry).toEqual([ - 'FormularioForm', - 'FormularioGrouping', - 'FormularioInput', - ]) + const localVue = createLocalVue() + + localVue.use(plugin) + + expect(localVue.component('FormularioForm')).toBeTruthy() + expect(localVue.component('FormularioGrouping')).toBeTruthy() + expect(localVue.component('FormularioInput')).toBeTruthy() + + const wrapper = mount({ template: '
', }, { localVue }) + + expect(wrapper.vm.$formulario).toBeInstanceOf(Formulario) + }) + + it ('Pushes Formulario instance to child a component', () => { + const localVue = createLocalVue() + + localVue.use(plugin) + localVue.component('TestComponent', { + render (h) { + return h('div') + } + }) + + const wrapper = mount({ + render (h) { + return h('div', [h('TestComponent', { ref: 'test' })]) + }, + }, { localVue }) + + expect(wrapper.vm.$formulario === wrapper.vm.$refs.test.$formulario).toBe(true) + }) + + it ('Does not pushes Formulario instance to a child component, if it has its own', () => { + const localVue = createLocalVue() + + localVue.use(plugin) + // noinspection JSCheckFunctionSignatures + localVue.component('TestComponent', { + formulario () { + return new Formulario() + }, + + render (h) { + return h('div') + }, + }) + + const wrapper = mount({ + render (h) { + return h('div', [h('TestComponent', { ref: 'test' })]) + }, + }, { localVue }) + + expect(wrapper.vm.$formulario === wrapper.vm.$refs.test.$formulario).toBe(false) }) }) From 8e3bacab3c454db90502ac9e91193e2f2183c26b Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Fri, 6 Nov 2020 20:23:01 +0300 Subject: [PATCH 06/14] refactor: Got rid of promises in default validation rules fns, rewritten rules tests --- src/validation/rules.ts | 243 +++++----- test/unit/validation/rules.test.js | 701 ++++++++++++++--------------- 2 files changed, 449 insertions(+), 495 deletions(-) diff --git a/src/validation/rules.ts b/src/validation/rules.ts index 12c0820..4ecbfba 100644 --- a/src/validation/rules.ts +++ b/src/validation/rules.ts @@ -1,264 +1,259 @@ import isUrl from 'is-url' -import { shallowEqualObjects, regexForFormat, has } from '@/utils' +import { has, regexForFormat, shallowEqualObjects } from '@/utils' import { ValidationContext } from '@/validation/validator' -interface DateValidationContext extends ValidationContext { - value: Date|string; -} - export default { /** * Rule: the value must be "yes", "on", "1", or true */ - accepted ({ value }: ValidationContext): Promise { - return Promise.resolve(['yes', 'on', '1', 1, true, 'true'].includes(value)) + accepted ({ value }: ValidationContext): boolean { + return ['yes', 'on', '1', 1, true, 'true'].includes(value) }, /** * Rule: checks if a value is after a given date. Defaults to current time */ - after ({ value }: DateValidationContext, 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)) + after ({ value }: ValidationContext, compare: string | false = false): boolean { + const compareTimestamp = compare !== false ? Date.parse(compare) : Date.now() + const valueTimestamp = value instanceof Date ? value.getTime() : Date.parse(value) + + return isNaN(valueTimestamp) ? false : (valueTimestamp > compareTimestamp) }, /** * Rule: checks if the value is only alpha */ - alpha ({ value }: { value: string }, set = 'default'): Promise { + alpha ({ value }: ValidationContext, set = 'default'): boolean { const sets: Record = { default: /^[a-zA-ZÀ-ÖØ-öø-ÿ]+$/, - latin: /^[a-zA-Z]+$/ + latin: /^[a-zA-Z]+$/, } - const selectedSet = has(sets, set) ? set : 'default' - return Promise.resolve(sets[selectedSet].test(value)) + return typeof value === 'string' && sets[has(sets, set) ? set : 'default'].test(value) }, /** * Rule: checks if the value is alpha numeric */ - alphanumeric ({ value }: { value: string }, set = 'default'): Promise { + alphanumeric ({ value }: ValidationContext, set = 'default'): boolean { const sets: Record = { default: /^[a-zA-Z0-9À-ÖØ-öø-ÿ]+$/, latin: /^[a-zA-Z0-9]+$/ } - const selectedSet = has(sets, set) ? set : 'default' - return Promise.resolve(sets[selectedSet].test(value)) + return typeof value === 'string' && sets[has(sets, set) ? set : 'default'].test(value) }, /** * Rule: checks if a value is after a given date. Defaults to current time */ - before ({ value }: DateValidationContext, 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)) + before ({ value }: ValidationContext, compare: string|false = false): boolean { + const compareTimestamp = compare !== false ? Date.parse(compare) : Date.now() + const valueTimestamp = value instanceof Date ? value.getTime() : Date.parse(value) + + return isNaN(valueTimestamp) ? false : (valueTimestamp < compareTimestamp) }, /** * Rule: checks if the value is between two other values */ - between ({ value }: { value: string|number }, from: number|any = 0, to: number|any = 10, force?: string): Promise { - return Promise.resolve(((): boolean => { - if (from === null || to === null || isNaN(from) || isNaN(to)) { - return false - } - if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') { - value = Number(value) - from = Number(from) - to = Number(to) - return (value > from && value < to) - } - if (typeof value === 'string' || force === 'length') { - value = (!isNaN(Number(value)) ? value.toString() : value) as string - return value.length > from && value.length < to - } + between ({ value }: ValidationContext, from: number|any = 0, to: number|any = 10, force?: string): boolean { + if (from === null || to === null || isNaN(from) || isNaN(to)) { return false - })()) + } + + if ((!isNaN(Number(value)) && force !== 'length') || force === 'value') { + value = Number(value) + return (value > Number(from) && value < Number(to)) + } + + if (typeof value === 'string' || force === 'length') { + value = (!isNaN(Number(value)) ? value.toString() : value) as string + return value.length > from && value.length < to + } + + return false }, /** * Confirm that the value of one field is the same as another, mostly used * for password confirmations. */ - confirm ({ value, formValues, name }: ValidationContext, field?: string): Promise { - return Promise.resolve(((): boolean => { - let confirmationFieldName = field - if (!confirmationFieldName) { - confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm` - } - return formValues[confirmationFieldName] === value - })()) + confirm ({ value, formValues, name }: ValidationContext, field?: string): boolean { + let confirmationFieldName = field + if (!confirmationFieldName) { + confirmationFieldName = /_confirm$/.test(name) ? name.substr(0, name.length - 8) : `${name}_confirm` + } + return formValues[confirmationFieldName] === value }, /** * Rule: ensures the value is a date according to Date.parse(), or a format * regex. */ - date ({ value }: { value: string }, format: string | false = false): Promise { - return Promise.resolve(format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value))) + date ({ value }: ValidationContext, format: string | false = false): boolean { + return format ? regexForFormat(format).test(value) : !isNaN(Date.parse(value)) }, /** * Rule: tests */ - email ({ value }: { value: string }): Promise { + email ({ value }: ValidationContext): boolean { if (!value) { - return Promise.resolve(true) + return true } // eslint-disable-next-line const isEmail = /^(([^<>()\[\].,;:\s@"]+(\.[^<>()\[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i - return Promise.resolve(isEmail.test(value)) + return isEmail.test(value) }, /** * Rule: Value ends with one of the given Strings */ - endsWith ({ value }: { value: any }, ...stack: any[]): Promise { + endsWith ({ value }: ValidationContext, ...stack: any[]): boolean { if (!value) { - return Promise.resolve(true) + return true } if (typeof value === 'string') { - return Promise.resolve(stack.length === 0 || stack.some(str => value.endsWith(str))) + return stack.length === 0 || stack.some(str => value.endsWith(str)) } - return Promise.resolve(false) + return false }, /** * Rule: Value is in an array (stack). */ - in ({ value }: { value: any }, ...stack: any[]): Promise { - return Promise.resolve(stack.some(item => { - return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value - })) + in ({ value }: ValidationContext, ...stack: any[]): boolean { + return stack.some(item => typeof item === 'object' ? shallowEqualObjects(item, value) : item === value) }, /** * Rule: Match the value against a (stack) of patterns or strings */ - matches ({ value }: { value: any }, ...stack: any[]): Promise { - return Promise.resolve(!!stack.find(pattern => { + matches ({ value }: ValidationContext, ...stack: any[]): boolean { + return !!stack.find(pattern => { if (typeof pattern === 'string' && pattern.substr(0, 1) === '/' && pattern.substr(-1) === '/') { pattern = new RegExp(pattern.substr(1, pattern.length - 2)) } + if (pattern instanceof RegExp) { return pattern.test(value) } + return pattern === value - })) + }) }, /** * Check the maximum value of a particular. */ - max ({ value }: { value: any }, maximum: string | number = 10, force?: string): Promise { - return Promise.resolve(((): boolean => { - if (Array.isArray(value)) { - maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum - return value.length <= maximum - } - if ((!isNaN(value) && force !== 'length') || force === 'value') { - value = !isNaN(value) ? Number(value) : value - return value <= maximum - } - if (typeof value === 'string' || (force === 'length')) { - value = !isNaN(value) ? value.toString() : value - return value.length <= maximum - } - return false - })()) + max ({ value }: ValidationContext, maximum: string | number = 10, force?: string): boolean { + if (Array.isArray(value)) { + maximum = !isNaN(Number(maximum)) ? Number(maximum) : maximum + return value.length <= maximum + } + + if ((!isNaN(value) && force !== 'length') || force === 'value') { + value = !isNaN(value) ? Number(value) : value + return value <= maximum + } + + if (typeof value === 'string' || (force === 'length')) { + value = !isNaN(value) ? value.toString() : value + return value.length <= maximum + } + + return false }, /** * Check the minimum value of a particular. */ - min ({ value }: { value: any }, minimum: number | any = 1, force?: string): Promise { - return Promise.resolve(((): boolean => { - if (Array.isArray(value)) { - minimum = !isNaN(minimum) ? Number(minimum) : minimum - return value.length >= minimum - } - if ((!isNaN(value) && force !== 'length') || force === 'value') { - value = !isNaN(value) ? Number(value) : value - return value >= minimum - } - if (typeof value === 'string' || (force === 'length')) { - value = !isNaN(value) ? value.toString() : value - return value.length >= minimum - } - return false - })()) + min ({ value }: ValidationContext, minimum: number | any = 1, force?: string): boolean { + if (Array.isArray(value)) { + minimum = !isNaN(minimum) ? Number(minimum) : minimum + return value.length >= minimum + } + + if ((!isNaN(value) && force !== 'length') || force === 'value') { + value = !isNaN(value) ? Number(value) : value + return value >= minimum + } + + if (typeof value === 'string' || (force === 'length')) { + value = !isNaN(value) ? value.toString() : value + return value.length >= minimum + } + + return false }, /** * Rule: Value is not in stack. */ - not ({ value }: { value: any }, ...stack: any[]): Promise { - return Promise.resolve(!stack.some(item => { - return typeof item === 'object' ? shallowEqualObjects(item, value) : item === value - })) + not ({ value }: ValidationContext, ...stack: any[]): boolean { + return !stack.some(item => typeof item === 'object' ? shallowEqualObjects(item, value) : item === value) }, /** * Rule: checks if the value is only alpha numeric */ - number ({ value }: { value: any }): Promise { - return Promise.resolve(String(value).length > 0 && !isNaN(Number(value))) + number ({ value }: ValidationContext): boolean { + return String(value).length > 0 && !isNaN(Number(value)) }, /** * Rule: must be a value */ - required ({ value }: { value: any }, isRequired: string|boolean = true): Promise { - return Promise.resolve(((): boolean => { - if (!isRequired || ['no', 'false'].includes(isRequired as string)) { - return true - } - if (Array.isArray(value)) { - return !!value.length - } - if (typeof value === 'string') { - return !!value - } - if (typeof value === 'object') { - return (!value) ? false : !!Object.keys(value).length - } + required ({ value }: ValidationContext, isRequired: string|boolean = true): boolean { + if (!isRequired || ['no', 'false'].includes(isRequired as string)) { return true - })()) + } + + if (Array.isArray(value)) { + return !!value.length + } + + if (typeof value === 'string') { + return !!value + } + + if (typeof value === 'object') { + return (!value) ? false : !!Object.keys(value).length + } + + return true }, /** * Rule: Value starts with one of the given Strings */ - startsWith ({ value }: { value: any }, ...stack: string[]): Promise { + startsWith ({ value }: ValidationContext, ...stack: string[]): boolean { if (!value) { - return Promise.resolve(true) + return true } if (typeof value === 'string') { - return Promise.resolve(stack.length === 0 || stack.some(str => value.startsWith(str))) + return stack.length === 0 || stack.some(str => value.startsWith(str)) } - return Promise.resolve(false) + return false }, /** * Rule: checks if a string is a valid url */ - url ({ value }: { value: string }): Promise { - return Promise.resolve(isUrl(value)) + url ({ value }: ValidationContext): boolean { + return isUrl(value) }, /** * Rule: not a true rule — more like a compiler flag. */ - bail (): Promise { - return Promise.resolve(true) - } + bail (): boolean { + return true + }, } diff --git a/test/unit/validation/rules.test.js b/test/unit/validation/rules.test.js index 7924702..76c6a0a 100644 --- a/test/unit/validation/rules.test.js +++ b/test/unit/validation/rules.test.js @@ -1,473 +1,428 @@ import rules from '@/validation/rules.ts' -/** - * Accepted rule - */ +const today = new Date() +const tomorrow = new Date() +const yesterday = new Date() + +tomorrow.setDate(today.getDate() + 1) +yesterday.setDate(today.getDate() - 1) + describe('accepted', () => { - it('passes with true', async () => expect(await rules.accepted({ value: 'yes' })).toBe(true)) + const validate = value => rules.accepted({ value, name: '', formValues: {} }) + const expectPass = value => expect(validate(value)).toBe(true) + const expectFail = value => expect(validate(value)).toBe(false) - it('passes with on', async () => expect(await rules.accepted({ value: 'on' })).toBe(true)) - - it('passes with 1', async () => expect(await rules.accepted({ value: '1' })).toBe(true)) - - it('passes with number 1', async () => expect(await rules.accepted({ value: 1 })).toBe(true)) - - it('passes with boolean true', async () => expect(await rules.accepted({ value: true })).toBe(true)) - - it('fail with boolean false', async () => expect(await rules.accepted({ value: false })).toBe(false)) - - it('fail with "false"', async () => expect(await rules.accepted({ value: 'false' })).toBe(false)) + it('passes with true', () => expectPass('yes')) + it('passes with on', () => expectPass('on')) + it('passes with 1', () => expectPass('1')) + it('passes with number 1', () => expectPass(1)) + it('passes with boolean true', () => expectPass(true)) + it('fail with boolean false', () => expectFail(false)) + it('fail with "false"', () => expectFail('false')) }) -/** - * Checks if a date is after another date - */ describe('after', () => { - const today = new Date() - const tomorrow = new Date() - const yesterday = new Date() - tomorrow.setDate(today.getDate() + 1) - yesterday.setDate(today.getDate() - 1) + const validate = (value, compare = false) => rules.after({ value, name: '', formValues: {} }, compare) + const expectPass = (value, compare = false) => expect(validate(value, compare)).toBe(true) + const expectFail = (value, compare = false) => expect(validate(value, compare)).toBe(false) - it('passes with tomorrow’s date object', async () => expect(await rules.after({ value: tomorrow })).toBe(true)) - - it('passes with future date', async () => expect(await rules.after({ value: 'January 15, 2999' })).toBe(true)) - - it('passes with long past date', async () => expect(await rules.after({ value: yesterday }, 'Jan 15, 2000')).toBe(true)) - - it('fails with yesterday’s date', async () => expect(await rules.after({ value: yesterday })).toBe(false)) - - it('fails with old date string', async () => expect(await rules.after({ value: 'January, 2000' })).toBe(false)) - - it('fails with invalid value', async () => expect(await rules.after({ value: '' })).toBe(false)) + it('passes with tomorrow’s date object', () => expectPass(tomorrow)) + it('passes with future date', () => expectPass('January 15, 2999')) + it('passes with long past date', () => expectPass(yesterday, 'Jan 15, 2000')) + it('fails with yesterday’s date', () => expectFail(yesterday)) + it('fails with old date string', () => expectFail('January, 2000')) + it('fails with invalid value', () => expectFail('')) }) -/** - * Checks if a date is after another date - */ describe('alpha', () => { - it('passes with simple string', async () => expect(await rules.alpha({ value: 'abc' })).toBe(true)) + const validate = (value, set = 'default') => rules.alpha({ value, name: '', formValues: {} }, set) - it('passes with long string', async () => expect(await rules.alpha({ value: 'lkashdflaosuihdfaisudgflakjsdbflasidufg' })).toBe(true)) + it('passes with simple string', () => { + expect(validate('abc')).toBe(true) + }) - it('passes with single character', async () => expect(await rules.alpha({ value: 'z' })).toBe(true)) + it('passes with long string', () => { + expect(validate('lkashdflaosuihdfaisudgflakjsdbflasidufg')).toBe(true) + }) - it('passes with accented character', async () => expect(await rules.alpha({ value: 'jüstin' })).toBe(true)) + it('passes with single character', () => { + expect(validate('z')).toBe(true) + }) - it('passes with lots of accented characters', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' })).toBe(true)) + it('passes with accented character', () => { + expect(validate('jüstin')).toBe(true) + }) - it('passes with lots of accented characters if invalid set', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'russian')).toBe(true)) + it('passes with lots of accented characters', () => { + expect(validate('àáâäïíôöÆ')).toBe(true) + }) - it('fails with lots of accented characters if latin', async () => expect(await rules.alpha({ value: 'àáâäïíôöÆ' }, 'latin')).toBe(false)) + it('passes with lots of accented characters if invalid set', () => { + expect(validate('àáâäïíôöÆ', 'russian')).toBe(true) + }) - it('fails with numbers', async () => expect(await rules.alpha({ value: 'justin83' })).toBe(false)) + it('fails with lots of accented characters if latin', () => { + expect(validate('àáâäïíôöÆ', 'latin')).toBe(false) + }) - it('fails with symbols', async () => expect(await rules.alpha({ value: '-justin' })).toBe(false)) + it('fails with numbers', () => { + expect(validate('justin83')).toBe(false) + }) + + it('fails with symbols', () => { + expect(validate('-justin')).toBe(false) + }) }) -/** - * Checks if a date alpha and numeric - */ describe('alphanumeric', () => { - it('passes with simple string', async () => expect(await rules.alphanumeric({ value: '567abc' })).toBe(true)) + const validate = (value, set = 'default') => rules.alphanumeric({ value, name: '', formValues: {} }, set) - it('passes with long string', async () => expect(await rules.alphanumeric({ value: 'lkashdfla234osuihdfaisudgflakjsdbfla567sidufg' })).toBe(true)) + it('passes with simple string', () => { + expect(validate('567abc')).toBe(true) + }) - it('passes with single character', async () => expect(await rules.alphanumeric({ value: 'z' })).toBe(true)) + it('passes with long string', () => { + expect(validate('lkashdfla234osuihdfaisudgflakjsdbfla567sidufg')).toBe(true) + }) - it('passes with accented character', async () => expect(await rules.alphanumeric({ value: 'jüst56in' })).toBe(true)) + it('passes with single character', () => { + expect(validate('z')).toBe(true) + }) - it('passes with lots of accented characters', async () => expect(await rules.alphanumeric({ value: 'àáâ7567567äïíôöÆ' })).toBe(true)) + it('passes with accented character', () => { + expect(validate('jüst56in')).toBe(true) + }) - it('passes with lots of accented characters if invalid set', async () => expect(await rules.alphanumeric({ value: '123123àáâäï67íôöÆ' }, 'russian')).toBe(true)) + it('passes with lots of accented characters', () => { + expect(validate('àáâ7567567äïíôöÆ')).toBe(true) + }) - it('fails with lots of accented characters if latin', async () => expect(await rules.alphanumeric({ value: 'àáâäï123123íôöÆ' }, 'latin')).toBe(false)) + it('passes with lots of accented characters if invalid set', () => { + expect(validate('123123àáâäï67íôöÆ', 'russian')).toBe(true) + }) - it('fails with decimals in', async () => expect(await rules.alphanumeric({ value: 'abcABC99.123' })).toBe(false)) + it('fails with lots of accented characters if latin', () => { + expect(validate('àáâäï123123íôöÆ', 'latin')).toBe(false) + }) + + it('fails with decimals in', () => { + expect(validate('abcABC99.123')).toBe(false) + }) }) -/** - * Checks if a date is after another date - */ describe('before', () => { - const today = new Date() - const tomorrow = new Date() - const yesterday = new Date() - tomorrow.setDate(today.getDate() + 1) - yesterday.setDate(today.getDate() - 1) + const validate = (value, compare = false) => rules.before({ value, name: '', formValues: {} }, compare) + const expectPass = (value, compare = false) => expect(validate(value, compare)).toBe(true) + const expectFail = (value, compare = false) => expect(validate(value, compare)).toBe(false) - it('fails with tomorrow’s date object', async () => expect(await rules.before({ value: tomorrow })).toBe(false)) - - it('fails with future date', async () => expect(await rules.before({ value: 'January 15, 2999' })).toBe(false)) - - it('fails with long past date', async () => expect(await rules.before({ value: yesterday }, 'Jan 15, 2000')).toBe(false)) - - it('passes with yesterday’s date', async () => expect(await rules.before({ value: yesterday })).toBe(true)) - - it('passes with old date string', async () => expect(await rules.before({ value: 'January, 2000' })).toBe(true)) - - it('fails with invalid value', async () => expect(await rules.after({ value: '' })).toBe(false)) + it('fails with tomorrow’s date object', () => expectFail(tomorrow)) + it('fails with future date', () => expectFail('January 15, 2999')) + it('fails with long past date', () => expectFail(yesterday, 'Jan 15, 2000')) + it('passes with yesterday’s date', () => expectPass(yesterday)) + it('passes with old date string', () => expectPass('January, 2000')) + it('fails with invalid value', () => expectFail('')) }) -/** - * Checks if between - */ describe('between', () => { - it('passes with simple number', async () => expect(await rules.between({ value: 5 }, 0, 10)).toBe(true)) + const validate = (value, from, to, force = undefined) => { + return rules.between({value, name: '', formValues: {}}, from, to, force) + } - it('passes with simple number string', async () => expect(await rules.between({ value: '5' }, '0', '10')).toBe(true)) + const expectPass = (value, from, to, force = undefined) => expect(validate(value, from, to, force)).toBe(true) + const expectFail = (value, from, to, force = undefined) => expect(validate(value, from, to, force)).toBe(false) - it('passes with decimal number string', async () => expect(await rules.between({ value: '0.5' }, '0', '1')).toBe(true)) - - it('passes with string length', async () => expect(await rules.between({ value: 'abc' }, 2, 4)).toBe(true)) - - it('fails with string length too long', async () => expect(await rules.between({ value: 'abcdef' }, 2, 4)).toBe(false)) - - it('fails with string length too short', async () => expect(await rules.between({ value: 'abc' }, 3, 10)).toBe(false)) - - it('fails with number to small', async () => expect(await rules.between({ value: 0 }, 3, 10)).toBe(false)) - - it('fails with number to large', async () => expect(await rules.between({ value: 15 }, 3, 10)).toBe(false)) - - it('passes when forced to value', async () => expect(await rules.between({ value: '4' }, 3, 10, 'value')).toBe(true)) - - it('fails when forced to value', async () => expect(await rules.between({ value: 442 }, 3, 10, 'value')).toBe(false)) - - it('passes when forced to length', async () => expect(await rules.between({ value: 7442 }, 3, 10, 'length')).toBe(true)) - - it('fails when forced to length', async () => expect(await rules.between({ value: 6 }, 3, 10, 'length')).toBe(false)) + it('passes with simple number', () => expectPass(5, 0, 10)) + it('passes with simple number string', () => expectPass('5', '0', '10')) + it('passes with decimal number string', () => expectPass('0.5', '0', '1')) + it('passes with string length', () => expectPass('abc', 2, 4)) + it('fails with string length too long', () => expectFail('abcdef', 2, 4)) + it('fails with string length too short', () => expectFail('abc', 3, 10)) + it('fails with number too small', () => expectFail(0, 3, 10)) + it('fails with number too large', () => expectFail(15, 3, 10)) + it('passes when forced to value', () => expectPass('4', 3, 10, 'value')) + it('fails when forced to value', () => expectFail(442, 3, 10, 'value')) + it('passes when forced to length', () => expectPass(7442, 3, 10, 'length')) + it('fails when forced to length', () => expectFail(6, 3, 10, 'length')) }) -/** - * Confirm - */ describe('confirm', () => { - it('Passes when the values are the same strings', async () => expect(await rules.confirm( - { value: 'abc', name: 'password', formValues: { password_confirm: 'abc' } } - )).toBe(true)) + const validate = (context, field = undefined) => rules.confirm(context, field) + const expectPass = (context, field = undefined) => expect(validate(context, field)).toBe(true) + const expectFail = (context, field = undefined) => expect(validate(context, field)).toBe(false) - it('Passes when the values are the same integers', async () => expect(await rules.confirm( - { value: 4422132, name: 'xyz', formValues: { xyz_confirm: 4422132 } } - )).toBe(true)) + it('Passes when the values are the same strings', () => expectPass({ + value: 'abc', + name: 'password', + formValues: { password_confirm: 'abc' } + })) - it('Passes when using a custom field', async () => expect(await rules.confirm( - { value: 4422132, name: 'name', formValues: { other_field: 4422132 } }, - 'other_field' - )).toBe(true)) + it('Passes when the values are the same integers', () => expectPass({ + value: 4422132, + name: 'xyz', + formValues: { xyz_confirm: 4422132 } + })) - it('Passes when using a field ends in _confirm', async () => expect(await rules.confirm( - { value: '$ecret', name: 'password_confirm', formValues: { password: '$ecret' } } - )).toBe(true)) + it('Passes when using a custom field', () => expectPass({ + value: 4422132, + name: 'name', + formValues: { other_field: 4422132 } + }, 'other_field')) - it('Fails when using different strings', async () => expect(await rules.confirm( - { value: 'Justin', name: 'name', formValues: { name_confirm: 'Daniel' } }, - )).toBe(false)) + it('Passes when using a field ends in _confirm', () => expectPass({ + value: '$ecret', + name: 'password_confirm', + formValues: { password: '$ecret' } + })) - it('Fails when the types are different', async () => expect(await rules.confirm( - { value: '1234', name: 'num', formValues: { num_confirm: 1234 } }, - )).toBe(false)) + it('Fails when using different strings', () => expectFail({ + value: 'Justin', + name: 'name', + formValues: { name_confirm: 'Daniel' } + })) + + it('Fails when the types are different', () => expectFail({ + value: '1234', + name: 'num', + formValues: { num_confirm: 1234 } + })) }) -/** - * Determines if the string is a date - */ describe('date', () => { - it('passes with month day year', async () => expect(await rules.date({ value: 'December 17, 2020' })).toBe(true)) - - it('passes with month day', async () => expect(await rules.date({ value: 'December 17' })).toBe(true)) - - it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17' })).toBe(true)) - - it('passes with short month day', async () => expect(await rules.date({ value: 'Dec 17 12:34:15' })).toBe(true)) - - it('passes with out of bounds number', async () => expect(await rules.date({ value: 'January 77' })).toBe(true)) - - it('passes with only month', async () => expect(await rules.date({ value: 'January' })).toBe(false)) - - it('passes with valid date format', async () => expect(await rules.date({ value: '12/17/1987' }, 'MM/DD/YYYY')).toBe(true)) - - it('fails with simple number and date format', async () => expect(await rules.date({ value: '1234' }, 'MM/DD/YYYY')).toBe(false)) - - it('fails with only day of week', async () => expect(await rules.date({ value: 'saturday' })).toBe(false)) - - it('fails with random string', async () => expect(await rules.date({ value: 'Pepsi 17' })).toBe(false)) - - it('fails with random number', async () => expect(await rules.date({ value: '1872301237' })).toBe(false)) + const validate = (value, format = false) => rules.date({ value, name: '', formValues: {} }, format) + const expectPass = (value, compare = false) => expect(validate(value, compare)).toBe(true) + const expectFail = (value, compare = false) => expect(validate(value, compare)).toBe(false) + it('passes with month day year', () => expectPass('December 17, 2020')) + it('passes with month day', () => expectPass('December 17')) + it('passes with short month day', () => expectPass('Dec 17')) + it('passes with short month day and time', () => expectPass('Dec 17 12:34:15')) + it('passes with out of bounds number', () => expectPass('January 77')) + it('fails with only month', () => expectFail('January')) + it('passes with valid date format', () => expectPass('12/17/1987', 'MM/DD/YYYY')) + it('fails with simple number and date format', () => expectFail('1234', 'MM/DD/YYYY')) + it('fails with only day of week', () => expectFail('saturday')) + it('fails with random string', () => expectFail('Pepsi 17')) + it('fails with random number', () => expectFail('1872301237')) }) /** - * Checks if email. - * * Note: testing is light, regular expression used is here: http://jsfiddle.net/ghvj4gy9/embedded/result,js/ */ describe('email', () => { - it('passes normal email', async () => expect(await rules.email({ value: 'dev+123@wearebraid.com' })).toBe(true)) + const validate = value => rules.email({ value, name: '', formValues: {} }) + const expectPass = value => expect(validate(value)).toBe(true) + const expectFail = value => expect(validate(value)).toBe(false) - it('passes numeric email', async () => expect(await rules.email({ value: '12345@google.com' })).toBe(true)) - - it('passes unicode email', async () => expect(await rules.email({ value: 'àlphä@❤️.ly' })).toBe(true)) - - it('passes numeric with new tld', async () => expect(await rules.email({ value: '12345@google.photography' })).toBe(true)) - - it('fails string without tld', async () => expect(await rules.email({ value: '12345@localhost' })).toBe(false)) - - it('fails string without invalid name', async () => expect(await rules.email({ value: '1*(123)2345@localhost' })).toBe(false)) + it('passes normal email', () => expectPass('dev+123@wearebraid.com')) + it('passes numeric email', () => expectPass('12345@google.com')) + it('passes unicode email', () => expectPass('àlphä@❤️.ly')) + it('passes numeric with new tld', () => expectPass('12345@google.photography')) + it('fails string without tld', () => expectFail('12345@localhost')) + it('fails string without invalid name', () => expectFail('1*(123)2345@localhost')) }) -/** - * Checks if value ends with a one of the specified Strings. - */ describe('endsWith', () => { - it('fails when value ending is not in stack of single value', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@gmail.com')).toBe(false) - }) + const validate = (value, ...stack) => rules.endsWith({ value, name: '', formValues: {} }, ...stack) + const expectPass = (value, ...stack) => expect(validate(value, ...stack)).toBe(true) + const expectFail = (value, ...stack) => expect(validate(value, ...stack)).toBe(false) - it('fails when value ending is not in stack of multiple values', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@gmail.com', '@yahoo.com')).toBe(false) - }) + it('fails when value ending is not in stack of single value', () => expectFail( + 'andrew@wearebraid.com', + '@gmail.com' + )) - 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 value ending is not in stack of multiple values', () => expectFail( + 'andrew@wearebraid.com', + '@gmail.com', '@yahoo.com' + )) - it('fails when passed value is not a string', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com'}, {value: '@wearebraid.com'})).toBe(false) - }) + it('fails when passed value is not a string', () => expectFail( + 'andrew@wearebraid.com', + ['@gmail.com', '@wearebraid.com'] + )) - 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('fails when passed value is not a string', () => expectFail( + 'andrew@wearebraid.com', + { value: '@wearebraid.com' } + )) - it('passes when stack consists of zero values', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com' })).toBe(true) - }) + it('passes when a string value is present and matched even if non-string values also exist as arguments', () => { + expectPass('andrew@wearebraid.com', { value: 'bad data' }, ['no bueno'], '@wearebraid.com') + }) - it('passes when value ending is in stack of single value', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@wearebraid.com')).toBe(true) - }) + it('passes when stack consists of zero values', () => expectPass('andrew@wearebraid.com')) - it('passes when value ending is in stack of multiple values', async () => { - expect(await rules.endsWith({ value: 'andrew@wearebraid.com' }, '@yahoo.com', '@wearebraid.com', '@gmail.com')).toBe(true) - }) + it('passes when value ending is in stack of single value', () => expectPass( + 'andrew@wearebraid.com', + '@wearebraid.com' + )) + + it('passes when value ending is in stack of multiple values', () => expectPass( + 'andrew@wearebraid.com', + '@yahoo.com', '@wearebraid.com', '@gmail.com' + )) }) -/** - * In rule - */ describe('in', () => { - it('fails when not in stack', async () => { - expect(await rules.in({ value: 'third' }, 'first', 'second')).toBe(false) - }) + const validate = (value, ...stack) => rules.in({ value, name: '', formValues: {} }, ...stack) + const expectPass = (value, ...stack) => expect(validate(value, ...stack)).toBe(true) + const expectFail = (value, ...stack) => expect(validate(value, ...stack)).toBe(false) - it('fails when case sensitive mismatch is in stack', async () => { - expect(await rules.in({ value: 'third' }, 'first', 'second', 'Third')).toBe(false) - }) - - it('fails comparing dissimilar objects', async () => { - expect(await rules.in({ value: {f: 'abc'} }, {a: 'cdf'}, {b: 'abc'})).toBe(false) - }) - - it('passes when case sensitive match is in stack', async () => { - expect(await rules.in({ value: 'third' }, 'first', 'second', 'third')).toBe(true) - }) - - it('passes a shallow array compare', async () => { - expect(await rules.in({ value: ['abc'] }, ['cdf'], ['abc'])).toBe(true) - }) - - it('passes a shallow object compare', async () => { - expect(await rules.in({ value: {f: 'abc'} }, {a: 'cdf'}, {f: 'abc'},)).toBe(true) - }) + it('fails when not in stack', () => expectFail('third', 'first', 'second')) + it('fails when case sensitive mismatch is in stack', () => expectFail( + 'third', + 'first', 'second', 'Third' + )) + it('fails comparing dissimilar objects', () => expectFail( + { f: 'abc' }, + { a: 'cdf' }, { b: 'abc' } + )) + it('passes when case sensitive match is in stack', () => expectPass( + 'third', + 'first', 'second', 'third' + )) + it('passes a shallow array compare', () => expectPass(['abc'], ['cdf'], ['abc'])) + it('passes a shallow object compare', () => expectPass( + { f: 'abc' }, + { a: 'cdf' }, { f: 'abc' } + )) }) -/** - * Matches rule - */ describe('matches', () => { - it('simple strings fail if they aren’t equal', async () => { - expect(await rules.matches({ value: 'third' }, 'first')).toBe(false) - }) + const validate = (value, ...stack) => rules.matches({ value, name: '', formValues: {} }, ...stack) + const expectPass = (value, ...stack) => expect(validate(value, ...stack)).toBe(true) + const expectFail = (value, ...stack) => expect(validate(value, ...stack)).toBe(false) - it('fails on non matching regex', async () => { - expect(await rules.matches({ value: 'third' }, /^thirds/)).toBe(false) - }) - - it('passes if simple strings match', async () => { - expect(await rules.matches({ value: 'second' }, 'third', 'second')).toBe(true) - }) - - it('passes on matching regex', async () => { - expect(await rules.matches({ value: 'third' }, /^third/)).toBe(true) - }) - - it('passes on matching mixed regex and string', async () => { - expect(await rules.matches({ value: 'first-fourth' }, 'second', /^third/, /fourth$/)).toBe(true) - }) - - it('fails on a regular expression encoded as a string', async () => { - expect(await rules.matches({ value: 'mypassword' }, '/[0-9]/')).toBe(false) - }) - - it('passes on a regular expression encoded as a string', async () => { - expect(await rules.matches({ value: 'mypa55word' }, '/[0-9]/')).toBe(true) - }) - - it('passes on a regular expression containing slashes', async () => { - expect(await rules.matches({ value: 'https://' }, '/https?:///')).toBe(true) - }) + it('simple strings fail if they aren’t equal', () => expectFail('third', 'first')) + it('fails on non matching regex', () => expectFail('third', /^thirds/)) + it('passes if simple strings match', () => expectPass('second', 'third', 'second')) + it('passes on matching regex', () => expectPass('third', /^third/)) + it('passes on matching mixed regex and string', () => expectPass( + 'first-fourth', + 'second', /^third/, /fourth$/ + )) + it('fails on a regular expression encoded as a string', () => expectFail('mypassword', '/[0-9]/')) + it('passes on a regular expression encoded as a string', () => expectPass('mypa55word', '/[0-9]/')) + it('passes on a regular expression containing slashes', () => expectPass( + 'https://', + '/https?:///' + )) }) -/** - * Minimum. - */ -describe('min', () => { - it('passes when a number string', async () => expect(await rules.min({ value: '5' }, '5')).toBe(true)) - - it('passes when a number', async () => expect(await rules.min({ value: 6 }, 5)).toBe(true)) - - it('passes when a string length', async () => expect(await rules.min({ value: 'foobar' }, '6')).toBe(true)) - - it('passes when a array length', async () => expect(await rules.min({ value: Array(6) }, '6')).toBe(true)) - - it('passes when string is forced to value', async () => expect(await rules.min({ value: 'bcd' }, 'aaa', 'value')).toBe(true)) - - it('fails when string is forced to lesser value', async () => expect(await rules.min({ value: 'a' }, 'b', 'value')).toBe(false)) - - it('passes when a number is forced to length', async () => expect(await rules.min({ value: '000' }, 3, 'length')).toBe(true)) - - it('fails when a number is forced to length', async () => expect(await rules.min({ value: '44' }, 3, 'length')).toBe(false)) - - it('fails when a array length', async () => expect(await rules.min({ value: Array(6) }, '7')).toBe(false)) - - it('fails when a string length', async () => expect(await rules.min({ value: 'bar' }, 4)).toBe(false)) - - it('fails when a number', async () => expect(await rules.min({ value: 3 }, '7')).toBe(false)) - -}) - -/** - * Maximum. - */ describe('max', () => { - it('passes when a number string', async () => expect(await rules.max({ value: '5' }, '5')).toBe(true)) + const validate = (value, max, force = undefined) => rules.max({value, name: '', formValues: {}}, max, force) + const expectPass = (v, max, force = undefined) => expect(validate(v, max, force)).toBe(true) + const expectFail = (v, max, force = undefined) => expect(validate(v, max, force)).toBe(false) - it('passes when a number', async () => expect(await rules.max({ value: 5 }, 6)).toBe(true)) - - it('passes when a string length', async () => expect(await rules.max({ value: 'foobar' }, '6')).toBe(true)) - - it('passes when a array length', async () => expect(await rules.max({ value: Array(6) }, '6')).toBe(true)) - - it('passes when forced to validate on length', async () => expect(await rules.max({ value: 10 }, 3, 'length')).toBe(true)) - - it('passes when forced to validate string on value', async () => expect(await rules.max({ value: 'b' }, 'e', 'value')).toBe(true)) - - it('fails when a array length', async () => expect(await rules.max({ value: Array(6) }, '5')).toBe(false)) - - it('fails when a string length', async () => expect(await rules.max({ value: 'bar' }, 2)).toBe(false)) - - it('fails when a number', async () => expect(await rules.max({ value: 10 }, '7')).toBe(false)) - - it('fails when a number', async () => expect(await rules.max({ value: 10 }, '7')).toBe(false)) - - it('fails when forced to validate on length', async () => expect(await rules.max({ value: -10 }, '1', 'length')).toBe(false)) + it('passes when a number string', () => expectPass('5', '5')) + it('passes when a number', () => expectPass(5, 6)) + it('passes when a string length', () => expectPass('foobar', '6')) + it('passes when a array length', () => expectPass(Array(6), '6')) + it('passes when forced to validate on length', () => expectPass(10, 3, 'length')) + it('passes when forced to validate string on value', () => expectPass('b', 'e', 'value')) + it('fails when a array length', () => expectFail(Array(6), '5')) + it('fails when a string length', () => expectFail('bar', 2)) + it('fails when a number', () => expectFail(10, '7')) + it('fails when forced to validate on length', () => expectFail(-10, '1', 'length')) +}) + +describe('min', () => { + const validate = (value, min, force = undefined) => rules.min({value, name: '', formValues: {}}, min, force) + const expectPass = (v, min, force = undefined) => expect(validate(v, min, force)).toBe(true) + const expectFail = (v, min, force = undefined) => expect(validate(v, min, force)).toBe(false) + + it('passes when a number string', () => expectPass('5', '5')) + it('passes when a number', () => expectPass(6, 5)) + it('passes when a string length', () => expectPass('foobar', '6')) + it('passes when a array length', () => expectPass(Array(6), '6')) + it('passes when string is forced to value', () => expectPass('bcd', 'aaa', 'value')) + it('fails when string is forced to lesser value', () => expectFail('a', 'b', 'value')) + it('passes when a number is forced to length', () => expectPass('000', 3, 'length')) + it('fails when a number is forced to length', () => expectFail('44', 3, 'length')) + it('fails when a array length', () => expectFail(Array(6), '7')) + it('fails when a string length', () => expectFail('bar', 4)) + it('fails when a number', () => expectFail(3, '7')) }) -/** - * Maximum. - */ describe('not', () => { - it('passes when a number string', async () => expect(await rules.not({ value: '5' }, '6')).toBe(true)) + const validate = (value, ...stack) => rules.not({ value, name: '', formValues: {} }, ...stack) + const expectPass = (value, ...stack) => expect(validate(value, ...stack)).toBe(true) + const expectFail = (value, ...stack) => expect(validate(value, ...stack)).toBe(false) - it('passes when a number', async () => expect(await rules.not({ value: 1 }, 30)).toBe(true)) - - it('passes when a string', async () => expect(await rules.not({ value: 'abc' }, 'def')).toBe(true)) - - it('fails when a shallow equal array', async () => expect(await rules.not({ value: ['abc'] }, ['abc'])).toBe(false)) - - it('fails when a shallow equal object', async () => expect(await rules.not({ value: {a: 'abc'} }, ['123'], {a: 'abc'})).toBe(false)) - - it('fails when string is in stack', async () => expect(await rules.not({ value: 'a' }, 'b', 'c', 'd', 'a', 'f')).toBe(false)) + it('passes when a number string', () => expectPass('5', '6')) + it('passes when a number', () => expectPass(1, 30)) + it('passes when a string', () => expectPass('abc', 'def')) + it('fails when a shallow equal array', () => expectFail(['abc'], ['abc'])) + it('fails when a shallow equal object', () => expectFail({a: 'abc'}, ['123'], {a: 'abc'})) + it('fails when string is in stack', () => expectFail('a', 'b', 'c', 'd', 'a', 'f')) }) -/** - * Checks if a date is after another date - */ describe('number', () => { - it('passes with simple number string', async () => expect(await rules.number({ value: '123' })).toBe(true)) + const validate = value => rules.number({ value, name: '', formValues: {} }) + const expectPass = value => expect(validate(value)).toBe(true) + const expectFail = value => expect(validate(value)).toBe(false) - it('passes with simple number', async () => expect(await rules.number({ value: 19832461234 })).toBe(true)) - - it('passes with float', async () => expect(await rules.number({ value: 198.32464 })).toBe(true)) - - it('passes with decimal in string', async () => expect(await rules.number({ value: '567.23' })).toBe(true)) - - it('fails with comma in number string', async () => expect(await rules.number({ value: '123,456' })).toBe(false)) - - it('fails with alpha', async () => expect(await rules.number({ value: '123sdf' })).toBe(false)) + it('passes with simple number string', () => expectPass('123')) + it('passes with simple number', () => expectPass(19832461234)) + it('passes with float', () => expectPass(198.32464)) + it('passes with decimal in string', () => expectPass('567.23')) + it('fails with comma in number string', () => expectFail('123,456')) + it('fails with alpha', () => expectFail('123sdf')) }) - -/** - * Required rule - */ describe('required', () => { - it('fails on empty string', async () => expect(await rules.required({ value: '' })).toBe(false)) + const validate = (value, isRequired = true) => rules.required({ value, name: '', formValues: {} }, isRequired) + const expectPass = (value, isRequired = true) => expect(validate(value, isRequired)).toBe(true) + const expectFail = (value, isRequired = true) => expect(validate(value, isRequired)).toBe(false) - it('fails on empty array', async () => expect(await rules.required({ value: [] })).toBe(false)) - - it('fails on empty object', async () => expect(await rules.required({ value: {} })).toBe(false)) - - it('fails on null', async () => expect(await rules.required({ value: null })).toBe(false)) - - it('passes with the number zero', async () => expect(await rules.required({ value: 0 })).toBe(true)) - - it('passes with the boolean false', async () => expect(await rules.required({ value: false })).toBe(true)) - - it('passes with a non empty array', async () => expect(await rules.required({ value: ['123'] })).toBe(true)) - - it('passes with a non empty object', async () => expect(await rules.required({ value: {a: 'b'} })).toBe(true)) - - it('passes with empty value if second argument is false', async () => expect(await rules.required({ value: '' }, false)).toBe(true)) - - it('passes with empty value if second argument is false string', async () => expect(await rules.required({ value: '' }, 'false')).toBe(true)) + it('fails on empty string', () => expectFail('')) + it('fails on empty array', () => expectFail([])) + it('fails on empty object', () => expectFail({})) + it('fails on null', () => expectFail(null)) + it('passes with the number zero', () => expectPass(0)) + it('passes with the boolean false', () => expectPass(false)) + it('passes with a non empty array', () => expectPass(['123'])) + it('passes with a non empty object', () => expectPass({ a: 'b' })) + it('passes with empty value if second argument is false', () => expectPass('', false)) + it('passes with empty value if second argument is false string', () => { + expectPass('', 'false') + }) }) -/** - * Checks if value starts with a one of the specified Strings. - */ describe('startsWith', () => { - it('fails when value starting is not in stack of single value', async () => { - expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza')).toBe(false) - }) + const validate = (value, ...args) => rules.startsWith({ value, name: '', formValues: {} }, ...args) - it('fails when value starting is not in stack of multiple values', async () => { - expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza', 'coffee')).toBe(false) - }) + it('fails when value starting is not in stack of single value', () => { + expect(validate('taco tuesday', 'pizza')).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 value starting is not in stack of multiple values', () => { + expect(validate('taco tuesday', 'pizza', 'coffee')).toBe(false) + }) - it('fails when passed value is not a string', async () => { - expect(await rules.startsWith({ value: 'taco tuesday'}, {value: 'taco'})).toBe(false) - }) + it('fails when passed value is not a string', () => { + expect(validate('taco tuesday', ['taco', 'pizza'])).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('fails when passed value is not a string', () => { + expect(validate('taco tuesday', {value: 'taco'})).toBe(false) + }) - it('passes when stack consists of zero values', async () => { - expect(await rules.startsWith({ value: 'taco tuesday' })).toBe(true) - }) + it('passes when a string value is present and matched even if non-string values also exist as arguments', () => { + expect(validate('taco tuesday', {value: 'taco'}, ['taco'], 'taco')).toBe(true) + }) - it('passes when value starting is in stack of single value', async () => { - expect(await rules.startsWith({ value: 'taco tuesday' }, 'taco')).toBe(true) - }) + it('passes when stack consists of zero values', () => { + expect(validate('taco tuesday')).toBe(true) + }) - it('passes when value starting is in stack of multiple values', async () => { - expect(await rules.startsWith({ value: 'taco tuesday' }, 'pizza', 'taco', 'coffee')).toBe(true) - }) + it('passes when value starting is in stack of single value', () => { + expect(validate('taco tuesday', 'taco')).toBe(true) + }) + + it('passes when value starting is in stack of multiple values', () => { + expect(validate('taco tuesday', 'pizza', 'taco', 'coffee')).toBe(true) + }) }) /** @@ -477,7 +432,11 @@ describe('startsWith', () => { * well tested: https://github.com/segmentio/is-url/blob/master/test/index.js */ describe('url', () => { - it('passes with http://google.com', async () => expect(await rules.url({ value: 'http://google.com' })).toBe(true)) + const validate = value => rules.url({ value, name: '', formValues: {} }) + const expectPass = value => expect(validate(value)).toBe(true) + const expectFail = value => expect(validate(value)).toBe(false) - it('fails with google.com', async () => expect(await rules.url({ value: 'google.com' })).toBe(false)) + it('passes with http://google.com', () => expectPass('http://google.com')) + it('passes with http://scholar.google.com', () => expectPass('http://scholar.google.com')) + it('fails with google.com', () => expectFail('google.com')) }) From 62e65687d1ac5b4a1176c848e75eb1f47d884baf Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Fri, 6 Nov 2020 20:44:04 +0300 Subject: [PATCH 07/14] chore: Types renames --- src/Formulario.ts | 13 ++++++----- src/FormularioInput.vue | 16 ++++++------- src/validation/messages.ts | 4 ++-- src/validation/validator.ts | 45 ++++++++++++++++++++----------------- 4 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index 401d2c4..abee46a 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -4,8 +4,9 @@ import validationMessages from '@/validation/messages' import { ValidationContext, - CheckRuleFn, - CreateMessageFn, + ValidationRuleFn, + ValidationMessageFn, + ValidationMessageI18NFn, } from '@/validation/validator' export interface FormularioOptions { @@ -18,7 +19,7 @@ export interface FormularioOptions { * The base formulario library. */ export default class Formulario { - public validationRules: Record = {} + public validationRules: Record = {} public validationMessages: Record = {} constructor (options?: FormularioOptions) { @@ -43,16 +44,16 @@ export default class Formulario { /** * Get validation rules by merging any passed in with global rules. */ - getRules (extendWith: Record = {}): Record { + getRules (extendWith: Record = {}): Record { return merge(this.validationRules, extendWith) } /** * Get validation messages by merging any passed in with global messages. */ - getMessages (vm: Vue, extendWith: Record): Record { + getMessages (vm: Vue, extendWith: Record): Record { const raw = merge(this.validationMessages || {}, extendWith) - const messages: Record = {} + const messages: Record = {} for (const name in raw) { messages[name] = (context: ValidationContext, ...args: any[]): string => { diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index bbfc003..a396a68 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -15,8 +15,8 @@ import { } from 'vue-property-decorator' import { arrayify, has, shallowEqualObjects, snakeToCamel } from './utils' import { - CheckRuleFn, - CreateMessageFn, + ValidationRuleFn, + ValidationMessageI18NFn, processConstraints, validate, Violation, @@ -47,8 +47,8 @@ export default class FormularioInput extends Vue { }) name!: string @Prop({ default: '' }) validation!: string|any[] - @Prop({ default: () => ({}) }) validationRules!: Record - @Prop({ default: () => ({}) }) validationMessages!: Record + @Prop({ default: () => ({}) }) validationRules!: Record + @Prop({ default: () => ({}) }) validationMessages!: Record @Prop({ default: VALIDATION_BEHAVIOR.DEMAND, validator: behavior => Object.values(VALIDATION_BEHAVIOR).includes(behavior) @@ -99,16 +99,16 @@ export default class FormularioInput extends Vue { }) } - get normalizedValidationRules (): Record { - const rules: Record = {} + get normalizedValidationRules (): Record { + const rules: Record = {} Object.keys(this.validationRules).forEach(key => { rules[snakeToCamel(key)] = this.validationRules[key] }) return rules } - get normalizedValidationMessages (): Record { - const messages: Record = {} + get normalizedValidationMessages (): Record { + const messages: Record = {} Object.keys(this.validationMessages).forEach((key) => { messages[snakeToCamel(key)] = this.validationMessages[key] }) diff --git a/src/validation/messages.ts b/src/validation/messages.ts index 902aea1..e42cd05 100644 --- a/src/validation/messages.ts +++ b/src/validation/messages.ts @@ -29,7 +29,7 @@ export default { /** * The value is not a letter. */ - alpha (vm: Vue, context: Record): string { + alpha (vm: Vue, context: ValidationContext): string { return vm.$t('validation.alpha', context) }, @@ -175,7 +175,7 @@ export default { /** * Value is not a url. */ - url (vm: Vue, context: Record): string { + url (vm: Vue, context: ValidationContext): string { return vm.$t('validation.url.default', context) } } diff --git a/src/validation/validator.ts b/src/validation/validator.ts index 225dc43..50b767a 100644 --- a/src/validation/validator.ts +++ b/src/validation/validator.ts @@ -11,14 +11,18 @@ export interface Violation { message: string; } -export interface CheckRuleFn { +export interface ValidationRuleFn { (context: ValidationContext, ...args: any[]): Promise|boolean; } -export interface CreateMessageFn { +export interface ValidationMessageFn { (context: ValidationContext, ...args: any[]): string; } +export interface ValidationMessageI18NFn { + (vm: Vue, context: ValidationContext, ...args: any[]): string; +} + export interface ValidationContext { // The value of the field (do not mutate!), value: any; @@ -34,21 +38,20 @@ export type ValidatorGroup = { } export function createValidator ( - ruleFn: CheckRuleFn, + ruleFn: ValidationRuleFn, ruleName: string|null, ruleArgs: any[], - messageFn: CreateMessageFn + messageFn: ValidationMessageFn ): Validator { return (context: ValidationContext): Promise => { - return Promise.resolve(ruleFn(context, ...ruleArgs)) - .then(valid => { - return !valid ? { - rule: ruleName, - args: ruleArgs, - context, - message: messageFn(context, ...ruleArgs), - } : null - }) + return Promise.resolve(ruleFn(context, ...ruleArgs)).then(valid => { + return !valid ? { + rule: ruleName, + args: ruleArgs, + context, + message: messageFn(context, ...ruleArgs), + } : null + }) } } @@ -61,8 +64,8 @@ export function parseModifier (ruleName: string): [string, string|null] { export function processSingleArrayConstraint ( constraint: any[], - rules: Record, - messages: Record + rules: Record, + messages: Record ): [Validator, string|null, string|null] { const args = constraint.slice() const first = args.shift() @@ -95,8 +98,8 @@ export function processSingleArrayConstraint ( export function processSingleStringConstraint ( constraint: string, - rules: Record, - messages: Record + rules: Record, + messages: Record ): [Validator, string|null, string|null] { const args = constraint.split(':') const [name, modifier] = parseModifier(args.shift() || '') @@ -119,8 +122,8 @@ export function processSingleStringConstraint ( export function processSingleConstraint ( constraint: string|Validator|[Validator|string, ...any[]], - rules: Record, - messages: Record + rules: Record, + messages: Record ): [Validator, string|null, string|null] { if (typeof constraint === 'function') { return [constraint, null, null] @@ -139,8 +142,8 @@ export function processSingleConstraint ( export function processConstraints ( constraints: string|any[], - rules: Record, - messages: Record + rules: Record, + messages: Record ): [Validator, string|null, string|null][] { if (typeof constraints === 'string') { return processConstraints(constraints.split('|').filter(f => f.length), rules, messages) From c82c8de5dba4d92698b3dda3d58930d706061011 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Fri, 6 Nov 2020 20:59:49 +0300 Subject: [PATCH 08/14] chore: Typehints, rewritten comments in @/validation/messages --- src/validation/messages.ts | 75 +++++++------------------------------ src/validation/rules.ts | 9 ++++- src/validation/validator.ts | 2 +- 3 files changed, 22 insertions(+), 64 deletions(-) diff --git a/src/validation/messages.ts b/src/validation/messages.ts index e42cd05..90bae34 100644 --- a/src/validation/messages.ts +++ b/src/validation/messages.ts @@ -1,23 +1,25 @@ -import { ValidationContext } from '@/validation/validator' +import { + ValidationContext, + ValidationMessageI18NFn, +} from '@/validation/validator' -export default { +/** + * Message builders, names match rules names, see @/validation/rules + */ +const messages: Record = { /** - * The default render method for error messages. + * Fallback for rules without message builder + * @param vm + * @param context */ default (vm: Vue, context: ValidationContext): string { return vm.$t('validation.default', context) }, - /** - * Valid accepted value. - */ accepted (vm: Vue, context: ValidationContext): string { return vm.$t('validation.accepted', context) }, - /** - * The date is not after. - */ after (vm: Vue, context: ValidationContext, compare: string | false = false): string { if (typeof compare === 'string' && compare.length) { return vm.$t('validation.after.compare', context) @@ -26,23 +28,14 @@ export default { return vm.$t('validation.after.default', context) }, - /** - * The value is not a letter. - */ alpha (vm: Vue, context: ValidationContext): string { return vm.$t('validation.alpha', context) }, - /** - * Rule: checks if the value is alpha numeric - */ alphanumeric (vm: Vue, context: ValidationContext): string { return vm.$t('validation.alphanumeric', context) }, - /** - * The date is not before. - */ before (vm: Vue, context: ValidationContext, compare: string|false = false): string { if (typeof compare === 'string' && compare.length) { return vm.$t('validation.before.compare', context) @@ -51,9 +44,6 @@ export default { return vm.$t('validation.before.default', context) }, - /** - * The value is not between two numbers or lengths - */ between (vm: Vue, context: ValidationContext, from: number|any = 0, to: number|any = 10, force?: string): string { const data = { ...context, from, to } @@ -64,16 +54,10 @@ export default { return vm.$t('validation.between.default', data) }, - /** - * The confirmation field does not match - */ confirm (vm: Vue, context: ValidationContext): string { return vm.$t('validation.confirm', context) }, - /** - * Is not a valid date. - */ date (vm: Vue, context: ValidationContext, format: string | false = false): string { if (typeof format === 'string' && format.length) { return vm.$t('validation.date.format', context) @@ -82,24 +66,15 @@ export default { return vm.$t('validation.date.default', context) }, - /** - * Is not a valid email address. - */ email (vm: Vue, context: ValidationContext): string { return vm.$t('validation.email.default', context) }, - /** - * Ends with specified value - */ endsWith (vm: Vue, context: ValidationContext): string { return vm.$t('validation.endsWith.default', context) }, - /** - * Value is an allowed value. - */ - in: function (vm: Vue, context: ValidationContext): string { + in (vm: Vue, context: ValidationContext): string { if (typeof context.value === 'string' && context.value) { return vm.$t('validation.in.string', context) } @@ -107,16 +82,10 @@ export default { return vm.$t('validation.in.default', context) }, - /** - * Value is not a match. - */ matches (vm: Vue, context: ValidationContext): string { return vm.$t('validation.matches.default', context) }, - /** - * The maximum value allowed. - */ max (vm: Vue, context: ValidationContext, maximum: string | number = 10, force?: string): string { if (Array.isArray(context.value)) { return vm.$tc('validation.max.array', maximum, context) @@ -129,9 +98,6 @@ export default { return vm.$tc('validation.max.default', maximum, context) }, - /** - * The maximum value allowed. - */ min (vm: Vue, context: ValidationContext, minimum: number | any = 1, force?: string): string { if (Array.isArray(context.value)) { return vm.$tc('validation.min.array', minimum, context) @@ -144,38 +110,25 @@ export default { return vm.$tc('validation.min.default', minimum, context) }, - /** - * The field is not an allowed value - */ not (vm: Vue, context: ValidationContext): string { return vm.$t('validation.not.default', context) }, - /** - * The field is not a number - */ number (vm: Vue, context: ValidationContext): string { return vm.$t('validation.number.default', context) }, - /** - * Required field. - */ required (vm: Vue, context: ValidationContext): string { return vm.$t('validation.required.default', context) }, - /** - * Starts with specified value - */ startsWith (vm: Vue, context: ValidationContext): string { return vm.$t('validation.startsWith.default', context) }, - /** - * Value is not a url. - */ url (vm: Vue, context: ValidationContext): string { return vm.$t('validation.url.default', context) } } + +export default messages diff --git a/src/validation/rules.ts b/src/validation/rules.ts index 4ecbfba..c88f86c 100644 --- a/src/validation/rules.ts +++ b/src/validation/rules.ts @@ -1,8 +1,11 @@ import isUrl from 'is-url' import { has, regexForFormat, shallowEqualObjects } from '@/utils' -import { ValidationContext } from '@/validation/validator' +import { + ValidationContext, + ValidationRuleFn, +} from '@/validation/validator' -export default { +const rules: Record = { /** * Rule: the value must be "yes", "on", "1", or true */ @@ -257,3 +260,5 @@ export default { return true }, } + +export default rules diff --git a/src/validation/validator.ts b/src/validation/validator.ts index 50b767a..2954a8e 100644 --- a/src/validation/validator.ts +++ b/src/validation/validator.ts @@ -121,7 +121,7 @@ export function processSingleStringConstraint ( } export function processSingleConstraint ( - constraint: string|Validator|[Validator|string, ...any[]], + constraint: Validator|string|[Validator|string, ...any[]], rules: Record, messages: Record ): [Validator, string|null, string|null] { From 0bca0873a610ec2a6452942bbb21d03b767083a3 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Mon, 9 Nov 2020 09:47:13 +0300 Subject: [PATCH 09/14] style: Code style --- src/FormularioInput.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index a396a68..1b993b3 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -109,7 +109,7 @@ export default class FormularioInput extends Vue { get normalizedValidationMessages (): Record { const messages: Record = {} - Object.keys(this.validationMessages).forEach((key) => { + Object.keys(this.validationMessages).forEach(key => { messages[snakeToCamel(key)] = this.validationMessages[key] }) return messages From b567c115c9ce49529e9c2bd47eb9566f9203dd3e Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Tue, 10 Nov 2020 09:46:04 +0300 Subject: [PATCH 10/14] chore: Removed unnecessary comment --- src/Formulario.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index abee46a..b6eac96 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -14,7 +14,6 @@ export interface FormularioOptions { validationMessages?: Record; } -// noinspection JSUnusedGlobalSymbols /** * The base formulario library. */ From 602af0a22dff971a90f8a82b332e699f317acf41 Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Tue, 10 Nov 2020 09:47:40 +0300 Subject: [PATCH 11/14] chore: Typehints fixes --- src/Formulario.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index b6eac96..8d37a82 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -19,7 +19,7 @@ export interface FormularioOptions { */ export default class Formulario { public validationRules: Record = {} - public validationMessages: Record = {} + public validationMessages: Record = {} constructor (options?: FormularioOptions) { this.validationRules = validationRules From 076e1404526fe164d841a869f3bb4b1bdeca34ef Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Tue, 10 Nov 2020 09:53:13 +0300 Subject: [PATCH 12/14] chore: Typehints fixes --- src/Formulario.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Formulario.ts b/src/Formulario.ts index 8d37a82..9c41268 100644 --- a/src/Formulario.ts +++ b/src/Formulario.ts @@ -10,8 +10,8 @@ import { } from '@/validation/validator' export interface FormularioOptions { - validationRules?: any; - validationMessages?: Record; + validationRules?: Record; + validationMessages?: Record; } /** From 13390b59c408b770dc28ba71972dfae31a4ff90d Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Tue, 10 Nov 2020 10:24:31 +0300 Subject: [PATCH 13/14] docs: Fixed README.md --- README.md | 130 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 67 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index ea4f6b3..ba3625c 100644 --- a/README.md +++ b/README.md @@ -1,86 +1,90 @@ ## What is Vue Formulario? -Vue Formulario is a library, based on Vue Formulate, that handles the core logic for working with forms and gives full control on the form presentation. +Vue Formulario is a library, based on Vue Formulate, that handles the core logic +for working with forms and gives full control on the form presentation. ## Examples -Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in v-slot props. Control should use `context.model` as v-model and `context.blurHandler` as handler for `blur` event (it is necessary for validation when property `errorBehavior` is `blur`). Errors object list for field can be accessed through `context.allErrors`. Each error is an object with fields message (translated message), rule (rule name) and context. +Every form control have to rendered inside FormularioInput component. This component provides `id` and `context` in +v-slot props. Control should use `context.model` as v-model and `context.runValidation` as handler for `blur` event +(it is necessary for validation when property `validationBehavior` is `demand`). Errors list for a field can be +accessed through `context.allErrors`. The example below creates the authorization form from data: ```json - { - "username": "", - "password": "", - "options": { - "anonym": false, - "tags": ["test"] - }, +{ + "username": "", + "password": "", + "options": { + "anonymous": false, + "tags": ["test"] } +} ``` ```html - + - - + -
- +
+
+ + + + + + + +
+ - {{ error.message }} - +
+
- + - - - - - -
- - -
-
-
- - - - -
+ + ``` ## License From 849738f0d0c6069e7b502c38aa9366dbdef5908b Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Tue, 10 Nov 2020 17:27:14 +0300 Subject: [PATCH 14/14] refactor: Logic optimization --- src/form/registry.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/form/registry.ts b/src/form/registry.ts index 98e917b..07e2ae7 100644 --- a/src/form/registry.ts +++ b/src/form/registry.ts @@ -28,24 +28,21 @@ export default class Registry { if (this.registry.has(field)) { return } + this.registry.set(field, component) + + // @ts-ignore + const value = getNested(this.ctx.initialValues, field) const hasModel = has(component.$options.propsData || {}, 'value') - if ( - !hasModel && - // @ts-ignore - this.ctx.hasInitialValue && - // @ts-ignore - getNested(this.ctx.initialValues, field) !== undefined - ) { + + // @ts-ignore + if (!hasModel && this.ctx.hasInitialValue && value !== undefined) { // In the case that the form is carrying an initial value and the // element is not, set it directly. // @ts-ignore - component.context.model = getNested(this.ctx.initialValues, field) - } else if ( - hasModel && + component.context.model = value // @ts-ignore - !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field)) - ) { + } else if (hasModel && !shallowEqualObjects(component.proxy, value)) { // In this case, the field is v-modeled or has an initial value and the // form has no value or a different value, so use the field value // @ts-ignore