Skip to content

Commit

Permalink
fix: field with pre-register schema errors should be validated on reg…
Browse files Browse the repository at this point in the history
…ister closes #3342
  • Loading branch information
logaretm committed Jun 5, 2021
1 parent 79d3779 commit 61c7359
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 26 deletions.
2 changes: 1 addition & 1 deletion packages/vee-validate/src/useField.ts
Expand Up @@ -417,7 +417,7 @@ export function useFieldMeta<TValue>(initialValue: MaybeRef<TValue>, currentValu
touched: false,
pending: false,
valid: true,
validated: false,
validated: !!unref(errors).length,
initialValue: computed(() => unref(initialValue) as TValue | undefined),
dirty: computed(() => {
return !isEqual(unref(currentValue), unref(initialValue));
Expand Down
66 changes: 41 additions & 25 deletions packages/vee-validate/src/useForm.ts
Expand Up @@ -103,6 +103,35 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}, {} as FormErrors<TValues>);
});

/**
* Holds a computed reference to all fields names and labels
*/
const fieldNames = computed(() => {
return keysOf(fieldsById.value).reduce((names, path) => {
const field = normalizeField(fieldsById.value[path]);
if (field) {
names[path as string] = unref(field.label || field.name) || '';
}

return names;
}, {} as Record<string, string>);
});

const fieldBailsMap = computed(() => {
return keysOf(fieldsById.value).reduce((map, path) => {
const field = normalizeField(fieldsById.value[path]);
if (field) {
map[path as string] = field.bails ?? true;
}

return map;
}, {} as Record<string, boolean>);
});

// mutable non-reactive reference to initial errors
// we need this to process initial errors then unset them
const initialErrors = { ...(opts?.initialErrors || {}) };

// initial form values
const { readonlyInitialValues, initialValues, setInitialValues } = useFormInitialValues<TValues>(
fieldsById,
Expand Down Expand Up @@ -284,6 +313,18 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}
);
}

// if field already had errors (initial errors) that's not user-set, validate it again to ensure state is correct
// the difference being that `initialErrors` will contain the error message while other errors (pre-validated schema) won't have them as initial errors
// #3342
const path = unref(field.name);
const initialErrorMessage = unref(field.errorMessage);
if (initialErrorMessage && initialErrors?.[path] !== initialErrorMessage) {
validateField(path);
}

// marks the initial error as "consumed" so it won't be matched later with same non-initial error
delete initialErrors[path];
}

function unregisterField(field: PrivateFieldComposite<unknown>) {
Expand Down Expand Up @@ -451,31 +492,6 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
setFieldInitialValue(path, value);
}

/**
* Holds a computed reference to all fields names and labels
*/
const fieldNames = computed(() => {
return keysOf(fieldsById.value).reduce((names, path) => {
const field = normalizeField(fieldsById.value[path]);
if (field) {
names[path as string] = unref(field.label || field.name) || '';
}

return names;
}, {} as Record<string, string>);
});

const fieldBailsMap = computed(() => {
return keysOf(fieldsById.value).reduce((map, path) => {
const field = normalizeField(fieldsById.value[path]);
if (field) {
map[path as string] = field.bails ?? true;
}

return map;
}, {} as Record<string, boolean>);
});

async function validateSchema(mode: SchemaValidationMode): Promise<FormValidationResult<TValues>> {
const schemaValue = unref(schema);
if (!schemaValue) {
Expand Down
33 changes: 33 additions & 0 deletions packages/vee-validate/tests/Form.spec.ts
Expand Up @@ -2082,4 +2082,37 @@ describe('<Form />', () => {
await flushPromises();
expect(document.querySelectorAll('.error')).toHaveLength(2);
});

// #3342
test('field with pre-register errors should be checked on register', async () => {
const isShown = ref(false);
const modelValue = ref('');
const wrapper = mountWithHoc({
setup() {
return {
isShown,
modelValue,
schema: {
fname: 'required',
},
};
},
template: `
<VForm :validation-schema="schema" v-slot="{ errors }">
<Field v-if="isShown" name="fname" v-model="modelValue" />
<span>{{ errors.fname }}</span>
</VForm>
`,
});

await flushPromises();
const span = wrapper.$el.querySelector('span');
expect(span.textContent).toBe(REQUIRED_MESSAGE);
modelValue.value = 'hello';
isShown.value = true;

await flushPromises();
// field was re-checked
expect(span.textContent).toBe('');
});
});

0 comments on commit 61c7359

Please sign in to comment.