From 3d31c461e63e9cca0f96c417c51f26506568ee8e Mon Sep 17 00:00:00 2001 From: Zaytsev Kirill Date: Sun, 25 Oct 2020 14:28:14 +0300 Subject: [PATCH] feat!: formularioValue prop removed, prop value now used as model --- src/FormularioInput.vue | 175 ++++++++++++--------------- src/form/registry.ts | 5 +- test/unit/FormularioForm.test.js | 29 +++-- test/unit/FormularioGrouping.test.js | 2 +- test/unit/FormularioInput.test.js | 39 +++--- 5 files changed, 121 insertions(+), 129 deletions(-) diff --git a/src/FormularioInput.vue b/src/FormularioInput.vue index 4f09160..ed714b3 100644 --- a/src/FormularioInput.vue +++ b/src/FormularioInput.vue @@ -1,10 +1,6 @@ @@ -33,6 +29,7 @@ import { const ERROR_BEHAVIOR = { BLUR: 'blur', LIVE: 'live', + NONE: 'none', SUBMIT: 'submit', } @@ -47,50 +44,81 @@ export default class FormularioInput extends Vue { @Inject({ default: undefined }) removeErrorObserver!: Function|undefined @Inject({ default: '' }) path!: string - @Model('input', { default: '' }) formularioValue: any + @Model('input', { default: '' }) value!: any + + @Prop({ + required: true, + validator: (name: any): boolean => typeof name === 'string' && name.length > 0, + }) name!: string - @Prop({ default: null }) id!: string|number|null - @Prop({ required: true }) name!: string - @Prop({ default: false }) value!: any @Prop({ default: '' }) validation!: string|any[] @Prop({ default: () => ({}) }) validationRules!: Record @Prop({ default: () => ({}) }) validationMessages!: Record @Prop({ default: () => [] }) errors!: string[] @Prop({ default: ERROR_BEHAVIOR.BLUR, - validator: behavior => [ERROR_BEHAVIOR.BLUR, ERROR_BEHAVIOR.LIVE, ERROR_BEHAVIOR.SUBMIT].includes(behavior) + validator: behavior => [ + ERROR_BEHAVIOR.BLUR, + ERROR_BEHAVIOR.LIVE, + ERROR_BEHAVIOR.NONE, + ERROR_BEHAVIOR.SUBMIT, + ].includes(behavior) }) errorBehavior!: string - @Prop({ default: false }) disableErrors!: boolean + @Prop({ default: false }) errorsDisabled!: boolean - defaultId: string = this.$formulario.nextId(this) - proxy: Record = this.getInitialValue() + proxy: any = this.getInitialValue() localErrors: string[] = [] - validationErrors: ValidationError[] = [] + violations: ValidationError[] = [] pendingValidation: Promise = Promise.resolve() + get model (): any { + const model = this.hasModel ? 'value' : 'proxy' + if (this[model] === undefined) { + return '' + } + return this[model] + } + + set model (value: any) { + if (!shallowEqualObjects(value, this.proxy)) { + this.proxy = value + } + + this.$emit('input', value) + + if (typeof this.formularioSetter === 'function') { + this.formularioSetter(this.context.name, value) + } + } + get context (): Record { return Object.defineProperty({ - id: this.id || this.defaultId, - name: this.nameOrFallback, + name: this.fullQualifiedName, + validate: this.performValidation.bind(this), + violations: this.violations, + errors: this.mergedErrors, + // @TODO: Deprecated + allErrors: [ + ...this.mergedErrors.map(message => ({ message })), + ...arrayify(this.violations) + ], blurHandler: this.blurHandler.bind(this), - errors: this.explicitErrors, - allErrors: this.allErrors, performValidation: this.performValidation.bind(this), - validationErrors: this.validationErrors, - value: this.value, }, 'model', { - get: this.modelGetter.bind(this), - set: this.modelSetter.bind(this), + get: () => this.model, + set: (value: any) => { + this.model = value + }, }) } get parsedValidationRules (): Record { - const parsedValidationRules: Record = {} + const rules: Record = {} Object.keys(this.validationRules).forEach(key => { - parsedValidationRules[snakeToCamel(key)] = this.validationRules[key] + rules[snakeToCamel(key)] = this.validationRules[key] }) - return parsedValidationRules + return rules } get messages (): Record { @@ -104,32 +132,14 @@ export default class FormularioInput extends Vue { /** * Return the element’s name, or select a fallback. */ - get nameOrFallback (): string { + get fullQualifiedName (): string { return this.path !== '' ? `${this.path}.${this.name}` : this.name } - /** - * Does this computed property have errors - */ - get hasErrors (): boolean { - return this.allErrors.length > 0 - } - - /** - * The merged errors computed property. - * Each error is an object with fields message (translated message), rule (rule name) and context - */ - get allErrors (): ValidationError[] { - return [ - ...this.explicitErrors.map(message => ({ message })), - ...arrayify(this.validationErrors) - ] - } - /** * These are errors we that have been explicitly passed to us. */ - get explicitErrors (): string[] { + get mergedErrors (): string[] { return [...arrayify(this.errors), ...this.localErrors] } @@ -137,35 +147,35 @@ export default class FormularioInput extends Vue { * Determines if this formulario element is v-modeled or not. */ get hasModel (): boolean { - return has(this.$options.propsData || {}, 'formularioValue') + return has(this.$options.propsData || {}, 'value') } @Watch('proxy') - onProxyChanged (newValue: Record, oldValue: Record): void { - if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) { - this.performValidation() - } else { - this.validationErrors = [] - } + onProxyChanged (newValue: any, oldValue: any): void { if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) { this.context.model = newValue } + if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) { + this.performValidation() + } else { + this.violations = [] + } } - @Watch('formularioValue') - onFormularioValueChanged (newValue: Record, oldValue: Record): void { + @Watch('value') + onValueChanged (newValue: any, oldValue: any): void { if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) { this.context.model = newValue } } created (): void { - this.applyInitialValue() - if (this.formularioRegister && typeof this.formularioRegister === 'function') { - this.formularioRegister(this.nameOrFallback, this) + this.initProxy() + if (typeof this.formularioRegister === 'function') { + this.formularioRegister(this.fullQualifiedName, this) } - if (!this.disableErrors && typeof this.addErrorObserver === 'function') { - this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback }) + if (typeof this.addErrorObserver === 'function' && !this.errorsDisabled) { + this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.fullQualifiedName }) } if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) { this.performValidation() @@ -174,35 +184,11 @@ export default class FormularioInput extends Vue { // noinspection JSUnusedGlobalSymbols beforeDestroy (): void { - if (!this.disableErrors && typeof this.removeErrorObserver === 'function') { + if (!this.errorsDisabled && typeof this.removeErrorObserver === 'function') { this.removeErrorObserver(this.setErrors) } if (typeof this.formularioDeregister === 'function') { - this.formularioDeregister(this.nameOrFallback) - } - } - - /** - * Get the value from a model. - */ - modelGetter (): any { - const model = this.hasModel ? 'formularioValue' : 'proxy' - if (this[model] === undefined) { - return '' - } - return this[model] - } - - /** - * Set the value from a model. - */ - modelSetter (value: any): void { - if (!shallowEqualObjects(value, this.proxy)) { - this.proxy = value - } - this.$emit('input', value) - if (this.context.name && typeof this.formularioSetter === 'function') { - this.formularioSetter(this.context.name, value) + this.formularioDeregister(this.fullQualifiedName) } } @@ -217,15 +203,10 @@ export default class FormularioInput extends Vue { } getInitialValue (): any { - if (has(this.$options.propsData as Record, 'value')) { - return this.value - } else if (has(this.$options.propsData as Record, 'formularioValue')) { - return this.formularioValue - } - return '' + return has(this.$options.propsData || {}, 'value') ? this.value : '' } - applyInitialValue (): void { + initProxy (): void { // This should only be run immediately on created and ensures that the // proxy and the model are both the same before any additional registration. if (!shallowEqualObjects(this.context.model, this.proxy)) { @@ -277,12 +258,12 @@ export default class FormularioInput extends Vue { } didValidate (violations: ValidationError[]): void { - const validationChanged = !shallowEqualObjects(violations, this.validationErrors) - this.validationErrors = violations + const validationChanged = !shallowEqualObjects(violations, this.violations) + this.violations = violations if (validationChanged) { const errorBag = { name: this.context.name, - errors: this.validationErrors, + errors: this.violations, } this.$emit('validation', errorBag) if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') { @@ -324,7 +305,7 @@ export default class FormularioInput extends Vue { hasValidationErrors (): Promise { return new Promise(resolve => { this.$nextTick(() => { - this.pendingValidation.then(() => resolve(!!this.validationErrors.length)) + this.pendingValidation.then(() => resolve(this.violations.length > 0)) }) }) } @@ -335,7 +316,7 @@ export default class FormularioInput extends Vue { resetValidation (): void { this.localErrors = [] - this.validationErrors = [] + this.violations = [] } } diff --git a/src/form/registry.ts b/src/form/registry.ts index c79e7dd..43a7663 100644 --- a/src/form/registry.ts +++ b/src/form/registry.ts @@ -115,8 +115,7 @@ export default class Registry { return } this.registry.set(field, component) - const hasModel = has(component.$options.propsData || {}, 'formularioValue') - const hasValue = has(component.$options.propsData || {}, 'value') + const hasModel = has(component.$options.propsData || {}, 'value') if ( !hasModel && // @ts-ignore @@ -129,7 +128,7 @@ export default class Registry { // @ts-ignore component.context.model = getNested(this.ctx.initialValues, field) } else if ( - (hasModel || hasValue) && + hasModel && // @ts-ignore !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field)) ) { diff --git a/test/unit/FormularioForm.test.js b/test/unit/FormularioForm.test.js index 1411448..e151545 100644 --- a/test/unit/FormularioForm.test.js +++ b/test/unit/FormularioForm.test.js @@ -88,7 +88,7 @@ describe('FormularioForm', () => { propsData: { formularioValue: { test: 'has initial value' } }, slots: { default: ` - + ` @@ -164,30 +164,39 @@ describe('FormularioForm', () => { it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => { const wrapper = mount(FormularioForm, { propsData: { - formularioValue: { test: '123' } + formularioValue: { test: 'Initial' } }, slots: { - default: '' - } + default: '' + }, }) - expect(wrapper.emitted().input[wrapper.emitted().input.length - 1]).toEqual([{ test: 'override-data' }]) + + const emitted = wrapper.emitted('input') + + expect(emitted).toBeTruthy() + expect(emitted[emitted.length - 1]).toEqual([{ test: 'Overrides' }]) }) it('updates an inputs value when the form v-model is modified', async () => { const wrapper = mount({ - data: () => ({ formValues: { test: 'abcd' } }), + data: () => ({ values: { test: 'abcd' } }), template: ` - + ` }) + + wrapper.vm.values = { test: '1234' } + await flushPromises() - wrapper.vm.formValues = { test: '1234' } - await flushPromises() - expect(wrapper.find('input[type="text"]').element['value']).toBe('1234') + + const input = wrapper.find('input[type="text"]') + + expect(input).toBeTruthy() + expect(input.element['value']).toBe('1234') }) it('Resolves hasValidationErrors to true', async () => { diff --git a/test/unit/FormularioGrouping.test.js b/test/unit/FormularioGrouping.test.js index 40448c7..7cb2818 100644 --- a/test/unit/FormularioGrouping.test.js +++ b/test/unit/FormularioGrouping.test.js @@ -53,7 +53,7 @@ describe('FormularioGrouping', () => { ` } }) - expect(wrapper.find('input[type="text"]').element.value).toBe('Group text') + expect(wrapper.find('input[type="text"]').element['value']).toBe('Group text') }) it('Data reactive with grouped fields', async () => { diff --git a/test/unit/FormularioInput.test.js b/test/unit/FormularioInput.test.js index 8964f8b..3e18150 100644 --- a/test/unit/FormularioInput.test.js +++ b/test/unit/FormularioInput.test.js @@ -19,17 +19,17 @@ Vue.use(Formulario, { }) describe('FormularioInput', () => { - it('allows custom field-rule level validation strings', async () => { + it('Allows custom field-rule level validation strings', async () => { const wrapper = mount(FormularioInput, { propsData: { name: 'test', validation: 'required|in:abcdef', - validationMessages: {in: 'the value was different than expected'}, + validationMessages: { in: 'the value was different than expected' }, errorBehavior: 'live', - value: 'other value' + value: 'other value', }, scopedSlots: { - default: `
{{ error.message }}
` + default: `
{{ violation.message }}
` }, }) await flushPromises() @@ -52,30 +52,33 @@ describe('FormularioInput', () => { expect(wrapper.find('span').exists()).toBe(false) }) - it('no validation on value change when errorBehavior is not live', async () => { + it('No validation on value change when errorBehavior is not live', async () => { const wrapper = mount(FormularioInput, { propsData: { name: 'test', validation: 'required|in:abcdef', validationMessages: {in: 'the value was different than expected'}, errorBehavior: 'submit', - value: 'other value' + value: 'Initial' }, scopedSlots: { default: `
- {{ error.message }} + {{ error.message }}
` } }) + await flushPromises() + expect(wrapper.find('span').exists()).toBe(false) - const input = wrapper.find('input[type="text"]') - input.element.value = 'test' - input.trigger('input') + wrapper.find('input[type="text"]').element['value'] = 'Test' + wrapper.find('input[type="text"]').trigger('change') + await flushPromises() - expect(wrapper.find('input[type="text"]').element.value).toBe('test') + + expect(wrapper.find('input[type="text"]').element['value']).toBe('Test') expect(wrapper.find('span').exists()).toBe(false) }) @@ -156,7 +159,7 @@ describe('FormularioInput', () => { expect(wrapper.find('span').text()).toBe('failed the foobar check') }) - it('uses global custom validation rules', async () => { + it('Uses global custom validation rules', async () => { const wrapper = mount(FormularioInput, { propsData: { name: 'test', @@ -169,7 +172,7 @@ describe('FormularioInput', () => { expect(globalRule.mock.calls.length).toBe(1) }) - it('emits correct validation event', async () => { + it('Emits correct validation event', async () => { const wrapper = mount(FormularioInput, { propsData: { validation: 'required', @@ -195,7 +198,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.validationErrors.length).toBe(1); + expect(wrapper.vm.context.violations.length).toBe(1); }) it('can show multiple validation errors if they occur before the bail rule', async () => { @@ -203,7 +206,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.validationErrors.length).toBe(2); + expect(wrapper.vm.context.violations.length).toBe(2); }) it('can avoid bail behavior by using modifier', async () => { @@ -211,7 +214,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' } }) await flushPromises(); - expect(wrapper.vm.context.validationErrors.length).toBe(2); + expect(wrapper.vm.context.violations.length).toBe(2); }) it('prevents later error messages when modified rule fails', async () => { @@ -219,7 +222,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.validationErrors.length).toBe(1); + expect(wrapper.vm.context.violations.length).toBe(1); }) it('can bail in the middle of the rule set with a modifier', async () => { @@ -227,7 +230,7 @@ describe('FormularioInput', () => { propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' } }) await flushPromises(); - expect(wrapper.vm.context.validationErrors.length).toBe(2); + expect(wrapper.vm.context.violations.length).toBe(2); }) it('does not show errors on blur when set error-behavior is submit', async () => {