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>
<div class="formulario-input">
<slot
:id="id"
:context="context"
:violations="validationErrors"
/>
<slot :context="context" />
</div>
</template>
@ -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<string, ValidationRule>
@Prop({ default: () => ({}) }) validationMessages!: Record<string, any>
@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<string, any> = this.getInitialValue()
proxy: any = this.getInitialValue()
localErrors: string[] = []
validationErrors: ValidationError[] = []
violations: ValidationError[] = []
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> {
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<string, ValidationRule> {
const parsedValidationRules: Record<string, ValidationRule> = {}
const rules: Record<string, ValidationRule> = {}
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> {
@ -104,32 +132,14 @@ export default class FormularioInput extends Vue {
/**
* Return the elements 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<string, any>, oldValue: Record<string, any>): 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<string, any>, oldValue: Record<string, any>): 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<string, any>, 'value')) {
return this.value
} else if (has(this.$options.propsData as Record<string, any>, '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<boolean> {
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 = []
}
}
</script>

View File

@ -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))
) {

View File

@ -88,7 +88,7 @@ describe('FormularioForm', () => {
propsData: { formularioValue: { test: 'has initial value' } },
slots: {
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">
</FormularioInput>
`
@ -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: '<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 () => {
const wrapper = mount({
data: () => ({ formValues: { test: 'abcd' } }),
data: () => ({ values: { test: 'abcd' } }),
template: `
<FormularioForm v-model="formValues">
<FormularioForm v-model="values">
<FormularioInput v-slot="{ context }" name="test" >
<input v-model="context.model" type="text">
</FormularioInput>
</FormularioForm>
`
})
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 () => {

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 () => {

View File

@ -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: `<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()
@ -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: `<div>
<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>`
}
})
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 () => {