Skip to content

Commit bfd6b00

Browse files
committed
feat: allow custom models for defineComponentBinds
1 parent 2cf0eec commit bfd6b00

File tree

6 files changed

+108
-15
lines changed

6 files changed

+108
-15
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'vee-validate': minor
3+
---
4+
5+
"feat: allow custom models for defineComponentBinds"

packages/vee-validate/src/types/forms.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,7 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
248248
markForUnmount(path: string): void;
249249
}
250250

251-
export interface BaseComponentBinds<TValue = unknown> {
252-
modelValue: TValue | undefined;
253-
'onUpdate:modelValue': (value: TValue) => void;
251+
export interface BaseComponentBinds<TValue = unknown, TModel = 'modelValue'> {
254252
onBlur: () => void;
255253
}
256254

@@ -263,6 +261,7 @@ export interface ComponentBindsConfig<TValue = unknown, TExtraProps extends Gene
263261
mapProps: (state: PublicPathState<TValue>) => TExtraProps;
264262
validateOnBlur: boolean;
265263
validateOnModelUpdate: boolean;
264+
model: string;
266265
}
267266

268267
export type LazyComponentBindsConfig<TValue = unknown, TExtraProps extends GenericObject = GenericObject> = (
@@ -271,6 +270,7 @@ export type LazyComponentBindsConfig<TValue = unknown, TExtraProps extends Gener
271270
props: TExtraProps;
272271
validateOnBlur: boolean;
273272
validateOnModelUpdate: boolean;
273+
model: string;
274274
}>;
275275

276276
export interface BaseInputBinds<TValue = unknown> {

packages/vee-validate/src/useForm.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -880,21 +880,27 @@ export function useForm<
880880
}
881881

882882
const props = computed(() => {
883-
const base: BaseComponentBinds<TValue> = {
884-
modelValue: pathState.value,
885-
'onUpdate:modelValue': onUpdateModelValue,
886-
onBlur,
887-
};
888-
889883
if (isCallable(config)) {
884+
const configVal = config(pathState);
885+
const model = configVal.model || 'modelValue';
886+
890887
return {
891-
...base,
892-
...(config(pathState).props || {}),
888+
onBlur,
889+
[model]: pathState.value,
890+
[`onUpdate:${model}`]: onUpdateModelValue,
891+
...(configVal.props || {}),
893892
} as BaseComponentBinds<TValue> & TExtras;
894893
}
895894

895+
const model = config?.model || 'modelValue';
896+
const base = {
897+
[model]: pathState.value,
898+
[`onUpdate:${model}`]: onUpdateModelValue,
899+
};
900+
896901
if (config?.mapProps) {
897902
return {
903+
onBlur,
898904
...base,
899905
...config.mapProps(omit(pathState, PRIVATE_PATH_STATE_KEYS)),
900906
} as BaseComponentBinds<TValue> & TExtras;

packages/vee-validate/tests/Field.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { defineRule, configure, useForm } from '@/vee-validate';
1+
import { defineRule, configure } from '@/vee-validate';
22
import { mountWithHoc, setValue, dispatchEvent, setChecked, flushPromises, dispatchFileEvent } from './helpers';
33
import * as yup from 'yup';
44
import { computed, reactive, ref, Ref } from 'vue';
5-
import ModelComp from './helpers/ModelComp';
5+
import { ModelComp } from './helpers/ModelComp';
66

77
vi.useFakeTimers();
88

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1-
export default {
1+
export const ModelComp = {
22
props: ['modelValue', 'name', 'test'],
33
emits: ['blur', 'update:modelValue'],
44
inheritAttrs: false,
55
template: `<input type="text" :name="name" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @blur="$emit('blur')">
66
<div v-if="test">{{ test }}</div>`,
77
};
8+
9+
export const CustomModelComp = {
10+
props: ['value', 'name', 'test'],
11+
emits: ['blur', 'update:value'],
12+
inheritAttrs: false,
13+
template: `<input type="text" :name="name" :value="value" @input="$emit('update:value', $event.target.value)" @blur="$emit('blur')">
14+
<div v-if="test">{{ test }}</div>`,
15+
};

packages/vee-validate/tests/useForm.spec.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FieldMeta, FormContext, FormMeta, useField, useForm } from '@/vee-valid
22
import { mountWithHoc, setValue, flushPromises, runInSetup, dispatchEvent } from './helpers';
33
import * as yup from 'yup';
44
import { onMounted, Ref } from 'vue';
5-
import ModelComp from './helpers/ModelComp';
5+
import { ModelComp, CustomModelComp } from './helpers/ModelComp';
66

77
describe('useForm()', () => {
88
const REQUIRED_MESSAGE = 'Field is required';
@@ -928,6 +928,80 @@ describe('useForm()', () => {
928928
await flushPromises();
929929
expect(document.body.innerHTML).toContain('valid');
930930
});
931+
932+
test('can have custom model', async () => {
933+
mountWithHoc({
934+
components: {
935+
CustomModelComp,
936+
},
937+
setup() {
938+
const { defineComponentBinds, values, errors } = useForm({
939+
validationSchema: yup.object({
940+
name: yup.string().required(),
941+
}),
942+
});
943+
944+
const field = defineComponentBinds('name', { model: 'value' });
945+
946+
return { field, values, errors };
947+
},
948+
template: `
949+
<CustomModelComp v-bind="field" />
950+
<span id="errors">{{ errors.name }}</span>
951+
<span id="values">{{ values.name }}</span>
952+
`,
953+
});
954+
955+
await flushPromises();
956+
const errorEl = document.getElementById('errors');
957+
const valuesEl = document.getElementById('values');
958+
setValue(document.querySelector('input') as any, '');
959+
dispatchEvent(document.querySelector('input') as any, 'blur');
960+
await flushPromises();
961+
expect(errorEl?.textContent).toBe('name is a required field');
962+
setValue(document.querySelector('input') as any, '123');
963+
dispatchEvent(document.querySelector('input') as any, 'blur');
964+
await flushPromises();
965+
expect(errorEl?.textContent).toBe('');
966+
expect(valuesEl?.textContent).toBe('123');
967+
});
968+
969+
test('can have lazy custom model', async () => {
970+
mountWithHoc({
971+
components: {
972+
CustomModelComp,
973+
},
974+
setup() {
975+
const { defineComponentBinds, values, errors } = useForm({
976+
validationSchema: yup.object({
977+
name: yup.string().required(),
978+
}),
979+
});
980+
981+
const field = defineComponentBinds('name', () => ({ model: 'value' }));
982+
983+
return { field, values, errors };
984+
},
985+
template: `
986+
<CustomModelComp v-bind="field" />
987+
<span id="errors">{{ errors.name }}</span>
988+
<span id="values">{{ values.name }}</span>
989+
`,
990+
});
991+
992+
await flushPromises();
993+
const errorEl = document.getElementById('errors');
994+
const valuesEl = document.getElementById('values');
995+
setValue(document.querySelector('input') as any, '');
996+
dispatchEvent(document.querySelector('input') as any, 'blur');
997+
await flushPromises();
998+
expect(errorEl?.textContent).toBe('name is a required field');
999+
setValue(document.querySelector('input') as any, '123');
1000+
dispatchEvent(document.querySelector('input') as any, 'blur');
1001+
await flushPromises();
1002+
expect(errorEl?.textContent).toBe('');
1003+
expect(valuesEl?.textContent).toBe('123');
1004+
});
9311005
});
9321006

9331007
describe('defineInputBinds', () => {

0 commit comments

Comments
 (0)