feat!: formularioValue prop removed, prop value now used as model
This commit is contained in:
parent
357d493899
commit
3d31c461e6
@ -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 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<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>
|
||||
|
@ -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))
|
||||
) {
|
||||
|
@ -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 () => {
|
||||
|
@ -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 () => {
|
||||
|
@ -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 () => {
|
||||
|
Loading…
Reference in New Issue
Block a user