diff --git a/src/FormularioField.vue b/src/FormularioField.vue index fd2f71e..2fb174b 100644 --- a/src/FormularioField.vue +++ b/src/FormularioField.vue @@ -94,16 +94,16 @@ export default class FormularioField extends Vue { /** * Determines if this formulario element is v-modeled or not. */ - get hasModel (): boolean { + public get hasModel (): boolean { return has(this.$options.propsData || {}, 'value') } - private get model (): unknown { + public get model (): unknown { const model = this.hasModel ? 'value' : 'proxy' return this.modelGetConverter(this[model]) } - private set model (value: unknown) { + public set model (value: unknown) { value = this.modelSetConverter(value, this.proxy) if (!shallowEquals(value, this.proxy)) { @@ -113,7 +113,7 @@ export default class FormularioField extends Vue { this.$emit('input', value) if (typeof this.__FormularioForm_set === 'function') { - this.__FormularioForm_set(this.context.name, value) + this.__FormularioForm_set(this.fullPath, value) } } @@ -241,7 +241,7 @@ export default class FormularioField extends Vue { */ setErrors (errors: string[]): void { if (!this.errorsDisabled) { - this.localErrors = arrayify(errors) + this.localErrors = arrayify(errors) as string[] } } diff --git a/src/FormularioForm.vue b/src/FormularioForm.vue index a969f53..8d11d23 100644 --- a/src/FormularioForm.vue +++ b/src/FormularioForm.vue @@ -1,6 +1,6 @@ @@ -38,9 +38,9 @@ export default class FormularioForm extends Vue { @Model('input', { default: () => ({}) }) public readonly state!: Record - // Errors record, describing state validation errors of whole form - @Prop({ default: () => ({}) }) readonly errors!: Record - // Form errors only used on FormularioForm default slot + // Describes validation errors of whole form + @Prop({ default: () => ({}) }) readonly fieldsErrors!: Record + // Only used on FormularioForm default slot @Prop({ default: () => ([]) }) readonly formErrors!: string[] public proxy: Record = {} @@ -48,58 +48,57 @@ export default class FormularioForm extends Vue { private registry: PathRegistry = new PathRegistry() // Local error messages are temporal, they wiped each resetValidation call + private localFieldsErrors: Record = {} private localFormErrors: string[] = [] - private localFieldErrors: Record = {} - get initialValues (): Record { + private get hasModel (): boolean { + return has(this.$options.propsData || {}, 'state') + } + + private get modelIsDefined (): boolean { + return this.state && typeof this.state === 'object' + } + + private get modelCopy (): Record { if (this.hasModel && typeof this.state === 'object') { - // If there is a v-model on the form/group, use those values as first priority return { ...this.state } // @todo - use a deep clone to detach reference types } return {} } - get mergedFormErrors (): string[] { + private get fieldsErrorsComputed (): Record { + return merge(this.fieldsErrors || {}, this.localFieldsErrors) + } + + private get formErrorsComputed (): string[] { return [...this.formErrors, ...this.localFormErrors] } - get mergedFieldErrors (): Record { - return merge(this.errors || {}, this.localFieldErrors) - } - - get hasModel (): boolean { - return has(this.$options.propsData || {}, 'state') - } - - get hasInitialValue (): boolean { - return this.state && typeof this.state === 'object' - } - @Watch('state', { deep: true }) - onStateChange (values: Record): void { + private onStateChange (values: Record): void { if (this.hasModel && values && typeof values === 'object') { this.setValues(values) } } - @Watch('mergedFieldErrors', { deep: true, immediate: true }) - onMergedFieldErrorsChange (errors: Record): void { + @Watch('fieldsErrorsComputed', { deep: true, immediate: true }) + private onFieldsErrorsChange (fieldsErrors: Record): void { this.registry.forEach((field, path) => { - field.setErrors(errors[path] || []) + field.setErrors(fieldsErrors[path] || []) }) } + @Provide('__FormularioForm_getValue') + private getValue (): Record { + return this.proxy + } + created (): void { this.syncProxy() } - @Provide('__FormularioForm_getValue') - getFormValues (): Record { - return this.proxy - } - - onFormSubmit (): Promise { + private onSubmit (): Promise { return this.hasValidationErrors() .then(hasErrors => hasErrors ? undefined : clone(this.proxy)) .then(data => { @@ -111,30 +110,24 @@ export default class FormularioForm extends Vue { }) } - @Provide('__FormularioForm_emitValidation') - private emitValidation (payload: ValidationEventPayload): void { - this.$emit('validation', payload) - } - @Provide('__FormularioForm_register') private register (path: string, field: FormularioField): void { this.registry.add(path, field) - const value = getNested(this.initialValues, path) + const value = getNested(this.modelCopy, path) - if (!field.hasModel && this.hasInitialValue && value !== undefined) { + if (!field.hasModel && this.modelIsDefined && value !== undefined) { // In the case that the form is carrying an initial value and the // element is not, set it directly. - // @ts-ignore - field.context.model = value + field.model = value } else if (field.hasModel && !shallowEquals(field.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 this.setFieldValueAndEmit(path, field.proxy) } - if (has(this.mergedFieldErrors, path)) { - field.setErrors(this.mergedFieldErrors[path] || []) + if (has(this.fieldsErrorsComputed, path)) { + field.setErrors(this.fieldsErrorsComputed[path] || []) } } @@ -148,9 +141,14 @@ export default class FormularioForm extends Vue { } } - syncProxy (): void { - if (this.hasInitialValue) { - this.proxy = this.initialValues + @Provide('__FormularioForm_emitValidation') + private emitValidation (payload: ValidationEventPayload): void { + this.$emit('validation', payload) + } + + private syncProxy (): void { + if (this.modelIsDefined) { + this.proxy = this.modelCopy } } @@ -177,7 +175,7 @@ export default class FormularioForm extends Vue { } if (!shallowEquals(newValue, field.proxy)) { - field.context.model = newValue + field.model = newValue } }) }) @@ -205,9 +203,9 @@ export default class FormularioForm extends Vue { this.$emit('input', { ...this.proxy }) } - setErrors ({ formErrors, inputErrors }: { formErrors?: string[]; inputErrors?: Record }): void { + setErrors ({ fieldsErrors, formErrors }: { fieldsErrors?: Record; formErrors?: string[] }): void { + this.localFieldsErrors = fieldsErrors || {} this.localFormErrors = formErrors || [] - this.localFieldErrors = inputErrors || {} } hasValidationErrors (): Promise { @@ -218,8 +216,8 @@ export default class FormularioForm extends Vue { } resetValidation (): void { + this.localFieldsErrors = {} this.localFormErrors = [] - this.localFieldErrors = {} this.registry.forEach((field: FormularioField) => { field.resetValidation() }) diff --git a/test/unit/FormularioFieldGroup.test.js b/test/unit/FormularioFieldGroup.test.js index 7f3d0d9..0dd842f 100644 --- a/test/unit/FormularioFieldGroup.test.js +++ b/test/unit/FormularioFieldGroup.test.js @@ -10,7 +10,7 @@ import FormularioForm from '@/FormularioForm.vue' Vue.use(Formulario) describe('FormularioFieldGroup', () => { - it('Grouped fields to be set', async () => { + test('grouped fields to be set', async () => { const wrapper = mount(FormularioForm, { slots: { default: ` @@ -36,7 +36,7 @@ describe('FormularioFieldGroup', () => { expect(emitted['submit']).toEqual([[{ group: { text: 'test' } }]]) }) - it('Grouped fields to be got', async () => { + test('grouped fields to be got', async () => { const wrapper = mount(FormularioForm, { propsData: { state: { @@ -57,11 +57,11 @@ describe('FormularioFieldGroup', () => { expect(wrapper.find('input[type="text"]').element['value']).toBe('Group text') }) - it('Data reactive with grouped fields', async () => { + test('data reactive with grouped fields', async () => { const wrapper = mount({ data: () => ({ values: {} }), template: ` - + @@ -71,22 +71,23 @@ describe('FormularioFieldGroup', () => { ` }) + expect(wrapper.find('span').text()).toBe('') + wrapper.find('input[type="text"]').setValue('test') + await flushPromises() + expect(wrapper.find('span').text()).toBe('test') }) - it('Errors are set for grouped fields', async () => { + test('errors are set for grouped fields', async () => { const wrapper = mount(FormularioForm, { - propsData: { - state: {}, - errors: { 'group.text': 'Test error' }, - }, + propsData: { fieldsErrors: { 'address.street': 'Test error' } }, slots: { default: ` - - + + {{ error }} diff --git a/test/unit/FormularioForm.test.js b/test/unit/FormularioForm.test.js index f834b23..08f34ee 100644 --- a/test/unit/FormularioForm.test.js +++ b/test/unit/FormularioForm.test.js @@ -1,6 +1,8 @@ import Vue from 'vue' + import { mount } from '@vue/test-utils' import flushPromises from 'flush-promises' + import Formulario from '@/index.ts' import FormularioForm from '@/FormularioForm.vue' @@ -13,32 +15,22 @@ Vue.use(Formulario, { }) describe('FormularioForm', () => { - it('render a form DOM element', () => { + test('renders a form DOM element', () => { const wrapper = mount(FormularioForm) expect(wrapper.find('form').exists()).toBe(true) }) - it('accepts a default slot', () => { + test('accepts a default slot', () => { const wrapper = mount(FormularioForm, { slots: { - default: '
' - } + default: '
' + }, }) - expect(wrapper.find('form div.default-slot-item').exists()).toBe(true) + + expect(wrapper.find('form [data-default]').exists()).toBe(true) }) - it('Intercepts submit event', () => { - const wrapper = mount(FormularioForm, { - slots: { - default: '