1
0
mirror of synced 2024-11-25 14:56:03 +03:00

feat!: formularioValue prop removed, prop value now used as model

This commit is contained in:
Zaytsev Kirill 2020-10-25 14:28:14 +03:00
parent 357d493899
commit 3d31c461e6
5 changed files with 121 additions and 129 deletions

View File

@ -1,10 +1,6 @@
<template> <template>
<div class="formulario-input"> <div class="formulario-input">
<slot <slot :context="context" />
:id="id"
:context="context"
:violations="validationErrors"
/>
</div> </div>
</template> </template>
@ -33,6 +29,7 @@ import {
const ERROR_BEHAVIOR = { const ERROR_BEHAVIOR = {
BLUR: 'blur', BLUR: 'blur',
LIVE: 'live', LIVE: 'live',
NONE: 'none',
SUBMIT: 'submit', SUBMIT: 'submit',
} }
@ -47,50 +44,81 @@ export default class FormularioInput extends Vue {
@Inject({ default: undefined }) removeErrorObserver!: Function|undefined @Inject({ default: undefined }) removeErrorObserver!: Function|undefined
@Inject({ default: '' }) path!: string @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: '' }) validation!: string|any[]
@Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRule> @Prop({ default: () => ({}) }) validationRules!: Record<string, ValidationRule>
@Prop({ default: () => ({}) }) validationMessages!: Record<string, any> @Prop({ default: () => ({}) }) validationMessages!: Record<string, any>
@Prop({ default: () => [] }) errors!: string[] @Prop({ default: () => [] }) errors!: string[]
@Prop({ @Prop({
default: ERROR_BEHAVIOR.BLUR, 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 }) errorBehavior!: string
@Prop({ default: false }) disableErrors!: boolean @Prop({ default: false }) errorsDisabled!: boolean
defaultId: string = this.$formulario.nextId(this) proxy: any = this.getInitialValue()
proxy: Record<string, any> = this.getInitialValue()
localErrors: string[] = [] localErrors: string[] = []
validationErrors: ValidationError[] = [] violations: ValidationError[] = []
pendingValidation: Promise<any> = Promise.resolve() pendingValidation: Promise<any> = 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<string, any> { get context (): Record<string, any> {
return Object.defineProperty({ return Object.defineProperty({
id: this.id || this.defaultId, name: this.fullQualifiedName,
name: this.nameOrFallback, 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), blurHandler: this.blurHandler.bind(this),
errors: this.explicitErrors,
allErrors: this.allErrors,
performValidation: this.performValidation.bind(this), performValidation: this.performValidation.bind(this),
validationErrors: this.validationErrors,
value: this.value,
}, 'model', { }, 'model', {
get: this.modelGetter.bind(this), get: () => this.model,
set: this.modelSetter.bind(this), set: (value: any) => {
this.model = value
},
}) })
} }
get parsedValidationRules (): Record<string, ValidationRule> { get parsedValidationRules (): Record<string, ValidationRule> {
const parsedValidationRules: Record<string, ValidationRule> = {} const rules: Record<string, ValidationRule> = {}
Object.keys(this.validationRules).forEach(key => { 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<string, any> { get messages (): Record<string, any> {
@ -104,32 +132,14 @@ export default class FormularioInput extends Vue {
/** /**
* Return the elements name, or select a fallback. * Return the elements name, or select a fallback.
*/ */
get nameOrFallback (): string { get fullQualifiedName (): string {
return this.path !== '' ? `${this.path}.${this.name}` : this.name 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. * These are errors we that have been explicitly passed to us.
*/ */
get explicitErrors (): string[] { get mergedErrors (): string[] {
return [...arrayify(this.errors), ...this.localErrors] 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. * Determines if this formulario element is v-modeled or not.
*/ */
get hasModel (): boolean { get hasModel (): boolean {
return has(this.$options.propsData || {}, 'formularioValue') return has(this.$options.propsData || {}, 'value')
} }
@Watch('proxy') @Watch('proxy')
onProxyChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void { onProxyChanged (newValue: any, oldValue: any): void {
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
this.performValidation()
} else {
this.validationErrors = []
}
if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) { if (!this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue this.context.model = newValue
} }
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
this.performValidation()
} else {
this.violations = []
}
} }
@Watch('formularioValue') @Watch('value')
onFormularioValueChanged (newValue: Record<string, any>, oldValue: Record<string, any>): void { onValueChanged (newValue: any, oldValue: any): void {
if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) { if (this.hasModel && !shallowEqualObjects(newValue, oldValue)) {
this.context.model = newValue this.context.model = newValue
} }
} }
created (): void { created (): void {
this.applyInitialValue() this.initProxy()
if (this.formularioRegister && typeof this.formularioRegister === 'function') { if (typeof this.formularioRegister === 'function') {
this.formularioRegister(this.nameOrFallback, this) this.formularioRegister(this.fullQualifiedName, this)
} }
if (!this.disableErrors && typeof this.addErrorObserver === 'function') { if (typeof this.addErrorObserver === 'function' && !this.errorsDisabled) {
this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.nameOrFallback }) this.addErrorObserver({ callback: this.setErrors, type: 'input', field: this.fullQualifiedName })
} }
if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) { if (this.errorBehavior === ERROR_BEHAVIOR.LIVE) {
this.performValidation() this.performValidation()
@ -174,35 +184,11 @@ export default class FormularioInput extends Vue {
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
beforeDestroy (): void { beforeDestroy (): void {
if (!this.disableErrors && typeof this.removeErrorObserver === 'function') { if (!this.errorsDisabled && typeof this.removeErrorObserver === 'function') {
this.removeErrorObserver(this.setErrors) this.removeErrorObserver(this.setErrors)
} }
if (typeof this.formularioDeregister === 'function') { if (typeof this.formularioDeregister === 'function') {
this.formularioDeregister(this.nameOrFallback) this.formularioDeregister(this.fullQualifiedName)
}
}
/**
* 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)
} }
} }
@ -217,15 +203,10 @@ export default class FormularioInput extends Vue {
} }
getInitialValue (): any { getInitialValue (): any {
if (has(this.$options.propsData as Record<string, any>, 'value')) { return has(this.$options.propsData || {}, 'value') ? this.value : ''
return this.value
} else if (has(this.$options.propsData as Record<string, any>, 'formularioValue')) {
return this.formularioValue
}
return ''
} }
applyInitialValue (): void { initProxy (): void {
// This should only be run immediately on created and ensures that the // This should only be run immediately on created and ensures that the
// proxy and the model are both the same before any additional registration. // proxy and the model are both the same before any additional registration.
if (!shallowEqualObjects(this.context.model, this.proxy)) { if (!shallowEqualObjects(this.context.model, this.proxy)) {
@ -277,12 +258,12 @@ export default class FormularioInput extends Vue {
} }
didValidate (violations: ValidationError[]): void { didValidate (violations: ValidationError[]): void {
const validationChanged = !shallowEqualObjects(violations, this.validationErrors) const validationChanged = !shallowEqualObjects(violations, this.violations)
this.validationErrors = violations this.violations = violations
if (validationChanged) { if (validationChanged) {
const errorBag = { const errorBag = {
name: this.context.name, name: this.context.name,
errors: this.validationErrors, errors: this.violations,
} }
this.$emit('validation', errorBag) this.$emit('validation', errorBag)
if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') { if (this.onFormularioFieldValidation && typeof this.onFormularioFieldValidation === 'function') {
@ -324,7 +305,7 @@ export default class FormularioInput extends Vue {
hasValidationErrors (): Promise<boolean> { hasValidationErrors (): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
this.$nextTick(() => { 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 { resetValidation (): void {
this.localErrors = [] this.localErrors = []
this.validationErrors = [] this.violations = []
} }
} }
</script> </script>

View File

@ -115,8 +115,7 @@ export default class Registry {
return return
} }
this.registry.set(field, component) this.registry.set(field, component)
const hasModel = has(component.$options.propsData || {}, 'formularioValue') const hasModel = has(component.$options.propsData || {}, 'value')
const hasValue = has(component.$options.propsData || {}, 'value')
if ( if (
!hasModel && !hasModel &&
// @ts-ignore // @ts-ignore
@ -129,7 +128,7 @@ export default class Registry {
// @ts-ignore // @ts-ignore
component.context.model = getNested(this.ctx.initialValues, field) component.context.model = getNested(this.ctx.initialValues, field)
} else if ( } else if (
(hasModel || hasValue) && hasModel &&
// @ts-ignore // @ts-ignore
!shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field)) !shallowEqualObjects(component.proxy, getNested(this.ctx.initialValues, field))
) { ) {

View File

@ -88,7 +88,7 @@ describe('FormularioForm', () => {
propsData: { formularioValue: { test: 'has initial value' } }, propsData: { formularioValue: { test: 'has initial value' } },
slots: { slots: {
default: ` default: `
<FormularioInput v-slot="{ context }" formulario-value="123" name="test" > <FormularioInput v-slot="{ context }" name="test" value="123">
<input v-model="context.model" type="text"> <input v-model="context.model" type="text">
</FormularioInput> </FormularioInput>
` `
@ -164,30 +164,39 @@ describe('FormularioForm', () => {
it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => { it('Updates calls setFieldValue on form when a field contains a populated v-model on registration', () => {
const wrapper = mount(FormularioForm, { const wrapper = mount(FormularioForm, {
propsData: { propsData: {
formularioValue: { test: '123' } formularioValue: { test: 'Initial' }
}, },
slots: { slots: {
default: '<FormularioInput name="test" formulario-value="override-data" />' default: '<FormularioInput name="test" value="Overrides" />'
} },
}) })
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 () => { it('updates an inputs value when the form v-model is modified', async () => {
const wrapper = mount({ const wrapper = mount({
data: () => ({ formValues: { test: 'abcd' } }), data: () => ({ values: { test: 'abcd' } }),
template: ` template: `
<FormularioForm v-model="formValues"> <FormularioForm v-model="values">
<FormularioInput v-slot="{ context }" name="test" > <FormularioInput v-slot="{ context }" name="test" >
<input v-model="context.model" type="text"> <input v-model="context.model" type="text">
</FormularioInput> </FormularioInput>
</FormularioForm> </FormularioForm>
` `
}) })
wrapper.vm.values = { test: '1234' }
await flushPromises() await flushPromises()
wrapper.vm.formValues = { test: '1234' }
await flushPromises() const input = wrapper.find('input[type="text"]')
expect(wrapper.find('input[type="text"]').element['value']).toBe('1234')
expect(input).toBeTruthy()
expect(input.element['value']).toBe('1234')
}) })
it('Resolves hasValidationErrors to true', async () => { it('Resolves hasValidationErrors to true', async () => {

View File

@ -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 () => { it('Data reactive with grouped fields', async () => {

View File

@ -19,17 +19,17 @@ Vue.use(Formulario, {
}) })
describe('FormularioInput', () => { describe('FormularioInput', () => {
it('allows custom field-rule level validation strings', async () => { it('Allows custom field-rule level validation strings', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|in:abcdef', validation: 'required|in:abcdef',
validationMessages: {in: 'the value was different than expected'}, validationMessages: { in: 'the value was different than expected' },
errorBehavior: 'live', errorBehavior: 'live',
value: 'other value' value: 'other value',
}, },
scopedSlots: { scopedSlots: {
default: `<div><span v-for="error in props.context.allErrors">{{ error.message }}</span></div>` default: `<div><span v-for="violation in props.context.violations">{{ violation.message }}</span></div>`
}, },
}) })
await flushPromises() await flushPromises()
@ -52,30 +52,33 @@ describe('FormularioInput', () => {
expect(wrapper.find('span').exists()).toBe(false) 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, { const wrapper = mount(FormularioInput, {
propsData: { propsData: {
name: 'test', name: 'test',
validation: 'required|in:abcdef', validation: 'required|in:abcdef',
validationMessages: {in: 'the value was different than expected'}, validationMessages: {in: 'the value was different than expected'},
errorBehavior: 'submit', errorBehavior: 'submit',
value: 'other value' value: 'Initial'
}, },
scopedSlots: { scopedSlots: {
default: `<div> default: `<div>
<input type="text" v-model="props.context.model"> <input type="text" v-model="props.context.model">
<span v-for="error in props.context.allErrors">{{ error.message }}</span> <span v-for="error in props.context.violations">{{ error.message }}</span>
</div>` </div>`
} }
}) })
await flushPromises() await flushPromises()
expect(wrapper.find('span').exists()).toBe(false) expect(wrapper.find('span').exists()).toBe(false)
const input = wrapper.find('input[type="text"]') wrapper.find('input[type="text"]').element['value'] = 'Test'
input.element.value = 'test' wrapper.find('input[type="text"]').trigger('change')
input.trigger('input')
await flushPromises() 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) expect(wrapper.find('span').exists()).toBe(false)
}) })
@ -156,7 +159,7 @@ describe('FormularioInput', () => {
expect(wrapper.find('span').text()).toBe('failed the foobar check') 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, { const wrapper = mount(FormularioInput, {
propsData: { propsData: {
name: 'test', name: 'test',
@ -169,7 +172,7 @@ describe('FormularioInput', () => {
expect(globalRule.mock.calls.length).toBe(1) expect(globalRule.mock.calls.length).toBe(1)
}) })
it('emits correct validation event', async () => { it('Emits correct validation event', async () => {
const wrapper = mount(FormularioInput, { const wrapper = mount(FormularioInput, {
propsData: { propsData: {
validation: 'required', validation: 'required',
@ -195,7 +198,7 @@ describe('FormularioInput', () => {
propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' } propsData: { name: 'test', validation: 'bail|required|in:xyz', errorBehavior: 'live' }
}) })
await flushPromises(); 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 () => { 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' } propsData: { name: 'test', validation: 'required|in:xyz|bail', errorBehavior: 'live' }
}) })
await flushPromises(); 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 () => { 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' } propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live', value: '123' }
}) })
await flushPromises(); 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 () => { 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' } propsData: { name: 'test', validation: '^required|in:xyz|min:10,length', errorBehavior: 'live' }
}) })
await flushPromises(); 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 () => { 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' } propsData: { name: 'test', validation: 'required|^in:xyz|min:10,length', errorBehavior: 'live' }
}) })
await flushPromises(); 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 () => { it('does not show errors on blur when set error-behavior is submit', async () => {