Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Validation not working when using model less validation with a default value. #2355

Merged
merged 2 commits into from Sep 20, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/components/common.ts
Expand Up @@ -2,19 +2,19 @@ import { VNodeDirective, VNode } from 'vue';
import { isCallable, debounce, identity } from '../utils';
import { modes, InteractionModeFactory } from '../modes';
import { ValidationResult, ValidationFlags, KnownKeys, ProviderInstance } from '../types';
import { findModel, getInputEventName, addVNodeListener } from '../utils/vnode';
import { findModel, getInputEventName, addVNodeListener, findValue } from '../utils/vnode';

/**
* Determines if a provider needs to run validation.
*/
function shouldValidate(ctx: ProviderInstance, model: VNodeDirective) {
function shouldValidate(ctx: ProviderInstance, value: string) {
// when an immediate/initial validation is needed and wasn't done before.
if (!ctx._ignoreImmediate && ctx.immediate) {
return true;
}

// when the value changes for whatever reason.
if (ctx.value !== model.value && ctx.normalizedEvents.length) {
if (ctx.value !== value && ctx.normalizedEvents.length) {
return true;
}

Expand All @@ -24,7 +24,7 @@ function shouldValidate(ctx: ProviderInstance, model: VNodeDirective) {
}

// when the initial value is undefined and the field wasn't rendered yet.
if (!ctx.initialized && model.value === undefined) {
if (!ctx.initialized && value === undefined) {
return true;
}

Expand Down Expand Up @@ -69,18 +69,18 @@ export function createValidationCtx(ctx: ProviderInstance): ValidationContext {
};
}

export function onRenderUpdate(vm: ProviderInstance, model: VNodeDirective | undefined) {
if (!model) {
export function onRenderUpdate(vm: ProviderInstance, value: any | undefined) {
if (value === undefined) {
return;
}

if (!vm.initialized) {
vm.initialValue = model.value;
vm.initialValue = value;
}

const validateNow = shouldValidate(vm, model);
const validateNow = shouldValidate(vm, value);
vm._needsValidation = false;
vm.value = model.value;
vm.value = value;
vm._ignoreImmediate = true;

if (!validateNow) {
Expand Down Expand Up @@ -150,11 +150,10 @@ export function createCommonHandlers(vm: ProviderInstance) {

// Adds all plugin listeners to the vnode.
export function addListeners(vm: ProviderInstance, node: VNode) {
const model = findModel(node);
const value = findValue(node);
// cache the input eventName.
vm._inputEventName = vm._inputEventName || getInputEventName(node, model);

onRenderUpdate(vm, model);
vm._inputEventName = vm._inputEventName || getInputEventName(node, findModel(node));
onRenderUpdate(vm, value);

const { onInput, onBlur, onValidate } = createCommonHandlers(vm);
addVNodeListener(node, vm._inputEventName, onInput);
Expand Down
12 changes: 10 additions & 2 deletions src/components/withValidation.ts
@@ -1,6 +1,13 @@
import { ValidationProvider } from './Provider';
import { identity } from '../utils';
import { findModel, findModelConfig, mergeVNodeListeners, getInputEventName, normalizeSlots } from '../utils/vnode';
import {
findModel,
findModelConfig,
mergeVNodeListeners,
getInputEventName,
normalizeSlots,
findValue
} from '../utils/vnode';
import { CreateElement, Component } from 'vue';
import { createValidationCtx, onRenderUpdate, createCommonHandlers, ValidationContext } from './common';

Expand Down Expand Up @@ -29,7 +36,8 @@ export function withValidation(component: ComponentLike, mapProps: ValidationCon

const model = findModel(this.$vnode);
this._inputEventName = this._inputEventName || getInputEventName(this.$vnode, model);
onRenderUpdate(this, model);
const value = findValue(this.$vnode);
onRenderUpdate(this, value);

const { onInput, onBlur, onValidate } = createCommonHandlers(this);

Expand Down
20 changes: 19 additions & 1 deletion src/utils/vnode.ts
Expand Up @@ -44,6 +44,24 @@ export function findModel(vnode: VNode): VNodeDirective | undefined {
return find(vnode.data.directives, d => d.name === 'model');
}

export function findValue(vnode: VNode): any | undefined {
const model = findModel(vnode);
if (model) {
return model.value;
}

if (vnode.componentOptions && vnode.componentOptions.propsData && 'value' in vnode.componentOptions.propsData) {
const propsDataWithValue = vnode.componentOptions.propsData as any;
return propsDataWithValue.value;
}

if (vnode.data && vnode.data.domProps && 'value' in vnode.data.domProps) {
return vnode.data.domProps.value;
}

return undefined;
}

function extractChildren(vnode: VNode | VNode[]): VNode[] {
if (Array.isArray(vnode)) {
return vnode;
Expand All @@ -62,7 +80,7 @@ function extractChildren(vnode: VNode | VNode[]): VNode[] {
}

export function extractVNodes(vnode: VNode | VNode[]): VNode[] {
if (!Array.isArray(vnode) && findModel(vnode)) {
if (!Array.isArray(vnode) && findValue(vnode) !== undefined) {
return [vnode];
}

Expand Down
4 changes: 4 additions & 0 deletions tests/helpers/ModelComp.js
@@ -0,0 +1,4 @@
export default {
props: ['value'],
template: `<input type="text" :value="value" @input="emit('input', $event)">`
};
77 changes: 52 additions & 25 deletions tests/providers/provider.js
Expand Up @@ -4,6 +4,7 @@ import flushPromises from 'flush-promises';
import { ValidationProvider, ValidationObserver, extend, withValidation, configure } from '@/index.full';
import InputWithoutValidation from './components/Input';
import SelectWithoutValidation from './components/Select';
import ModelComp from './../helpers/ModelComp';

const Vue = createLocalVue();
Vue.component('ValidationProvider', ValidationProvider);
Expand Down Expand Up @@ -781,6 +782,55 @@ test('validates manually using the validate event handler', async () => {
expect(error.text()).toBeFalsy();
});

test('validates manually with a initial value using the validate event handler on native comp', async () => {
const wrapper = mount(
{
data: () => ({
myValue: 'initial value'
}),
template: `
<ValidationObserver ref="obs">
<ValidationProvider rules="required" v-slot="{ validate, errors }">
<input type="text" :value="myValue" @input="validate">
<p id="error">{{ errors[0] }}</p>
</ValidationProvider>
</ValidationObserver>
`
},
{ localVue: Vue, sync: false }
);

await wrapper.vm.$refs.obs.validate();

const error = wrapper.find('#error');
expect(error.text()).toBe('');
});

test('validates manually with a initial value using the validate event handler on vue comp', async () => {
Vue.component('ModelComp', ModelComp);
const wrapper = mount(
{
data: () => ({
myValue: 'initial value'
}),
template: `
<ValidationObserver ref="obs">
<ValidationProvider rules="required" v-slot="{ validate, errors }">
<ModelComp :value="myValue" @input="validate" />
<p id="error">{{ errors[0] }}</p>
</ValidationProvider>
</ValidationObserver>
`
},
{ localVue: Vue, sync: false }
);

await wrapper.vm.$refs.obs.validate();

const error = wrapper.find('#error');
expect(error.text()).toBe('');
});

test('resets validation state using reset method in slot scope data', async () => {
const wrapper = mount(
{
Expand Down Expand Up @@ -1021,7 +1071,7 @@ test('array param collecting in the last parameter', async () => {
expect(wrapper.find('#error').text()).toBe('');
});

test('should throw if rule does not exist', () => {
test('should throw if rule does not exist', async () => {
const wrapper = mount(
{
data: () => ({ val: '123' }),
Expand All @@ -1033,30 +1083,7 @@ test('should throw if rule does not exist', () => {
},
{ localVue: Vue, sync: false }
);
expect(wrapper.vm.$refs.pro.validate()).rejects.toThrow();
});

test('should throw if required rule does not return an object', () => {
extend('faultyRequired', {
computesRequired: true,
validate() {
return false;
}
});

const wrapper = mount(
{
data: () => ({ val: '' }),
template: `
<ValidationProvider rules="faultyRequired" v-slot="ctx" ref="pro">
<input v-model="val" type="text">
</ValidationProvider>
`
},
{ localVue: Vue, sync: false }
);

expect(wrapper.vm.$refs.pro.validate()).rejects.toThrow();
await expect(wrapper.vm.$refs.pro.validate()).rejects.toThrow();
});

test('returns custom error messages passed in the customMessages prop', async () => {
Expand Down