Skip to content

Commit

Permalink
feat: allow custom models for defineComponentBinds
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Jun 22, 2023
1 parent 2cf0eec commit bfd6b00
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-bananas-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vee-validate': minor
---

"feat: allow custom models for defineComponentBinds"
6 changes: 3 additions & 3 deletions packages/vee-validate/src/types/forms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,7 @@ export interface PrivateFormContext<TValues extends GenericObject = GenericObjec
markForUnmount(path: string): void;
}

export interface BaseComponentBinds<TValue = unknown> {
modelValue: TValue | undefined;
'onUpdate:modelValue': (value: TValue) => void;
export interface BaseComponentBinds<TValue = unknown, TModel = 'modelValue'> {
onBlur: () => void;
}

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

export type LazyComponentBindsConfig<TValue = unknown, TExtraProps extends GenericObject = GenericObject> = (
Expand All @@ -271,6 +270,7 @@ export type LazyComponentBindsConfig<TValue = unknown, TExtraProps extends Gener
props: TExtraProps;
validateOnBlur: boolean;
validateOnModelUpdate: boolean;
model: string;
}>;

export interface BaseInputBinds<TValue = unknown> {
Expand Down
22 changes: 14 additions & 8 deletions packages/vee-validate/src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,21 +880,27 @@ export function useForm<
}

const props = computed(() => {
const base: BaseComponentBinds<TValue> = {
modelValue: pathState.value,
'onUpdate:modelValue': onUpdateModelValue,
onBlur,
};

if (isCallable(config)) {
const configVal = config(pathState);
const model = configVal.model || 'modelValue';

return {
...base,
...(config(pathState).props || {}),
onBlur,
[model]: pathState.value,
[`onUpdate:${model}`]: onUpdateModelValue,
...(configVal.props || {}),
} as BaseComponentBinds<TValue> & TExtras;
}

const model = config?.model || 'modelValue';
const base = {
[model]: pathState.value,
[`onUpdate:${model}`]: onUpdateModelValue,
};

if (config?.mapProps) {
return {
onBlur,
...base,
...config.mapProps(omit(pathState, PRIVATE_PATH_STATE_KEYS)),
} as BaseComponentBinds<TValue> & TExtras;
Expand Down
4 changes: 2 additions & 2 deletions packages/vee-validate/tests/Field.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { defineRule, configure, useForm } from '@/vee-validate';
import { defineRule, configure } from '@/vee-validate';
import { mountWithHoc, setValue, dispatchEvent, setChecked, flushPromises, dispatchFileEvent } from './helpers';
import * as yup from 'yup';
import { computed, reactive, ref, Ref } from 'vue';
import ModelComp from './helpers/ModelComp';
import { ModelComp } from './helpers/ModelComp';

vi.useFakeTimers();

Expand Down
10 changes: 9 additions & 1 deletion packages/vee-validate/tests/helpers/ModelComp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export default {
export const ModelComp = {
props: ['modelValue', 'name', 'test'],
emits: ['blur', 'update:modelValue'],
inheritAttrs: false,
template: `<input type="text" :name="name" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" @blur="$emit('blur')">
<div v-if="test">{{ test }}</div>`,
};

export const CustomModelComp = {
props: ['value', 'name', 'test'],
emits: ['blur', 'update:value'],
inheritAttrs: false,
template: `<input type="text" :name="name" :value="value" @input="$emit('update:value', $event.target.value)" @blur="$emit('blur')">
<div v-if="test">{{ test }}</div>`,
};
76 changes: 75 additions & 1 deletion packages/vee-validate/tests/useForm.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FieldMeta, FormContext, FormMeta, useField, useForm } from '@/vee-valid
import { mountWithHoc, setValue, flushPromises, runInSetup, dispatchEvent } from './helpers';
import * as yup from 'yup';
import { onMounted, Ref } from 'vue';
import ModelComp from './helpers/ModelComp';
import { ModelComp, CustomModelComp } from './helpers/ModelComp';

describe('useForm()', () => {
const REQUIRED_MESSAGE = 'Field is required';
Expand Down Expand Up @@ -928,6 +928,80 @@ describe('useForm()', () => {
await flushPromises();
expect(document.body.innerHTML).toContain('valid');
});

test('can have custom model', async () => {
mountWithHoc({
components: {
CustomModelComp,
},
setup() {
const { defineComponentBinds, values, errors } = useForm({
validationSchema: yup.object({
name: yup.string().required(),
}),
});

const field = defineComponentBinds('name', { model: 'value' });

return { field, values, errors };
},
template: `
<CustomModelComp v-bind="field" />
<span id="errors">{{ errors.name }}</span>
<span id="values">{{ values.name }}</span>
`,
});

await flushPromises();
const errorEl = document.getElementById('errors');
const valuesEl = document.getElementById('values');
setValue(document.querySelector('input') as any, '');
dispatchEvent(document.querySelector('input') as any, 'blur');
await flushPromises();
expect(errorEl?.textContent).toBe('name is a required field');
setValue(document.querySelector('input') as any, '123');
dispatchEvent(document.querySelector('input') as any, 'blur');
await flushPromises();
expect(errorEl?.textContent).toBe('');
expect(valuesEl?.textContent).toBe('123');
});

test('can have lazy custom model', async () => {
mountWithHoc({
components: {
CustomModelComp,
},
setup() {
const { defineComponentBinds, values, errors } = useForm({
validationSchema: yup.object({
name: yup.string().required(),
}),
});

const field = defineComponentBinds('name', () => ({ model: 'value' }));

return { field, values, errors };
},
template: `
<CustomModelComp v-bind="field" />
<span id="errors">{{ errors.name }}</span>
<span id="values">{{ values.name }}</span>
`,
});

await flushPromises();
const errorEl = document.getElementById('errors');
const valuesEl = document.getElementById('values');
setValue(document.querySelector('input') as any, '');
dispatchEvent(document.querySelector('input') as any, 'blur');
await flushPromises();
expect(errorEl?.textContent).toBe('name is a required field');
setValue(document.querySelector('input') as any, '123');
dispatchEvent(document.querySelector('input') as any, 'blur');
await flushPromises();
expect(errorEl?.textContent).toBe('');
expect(valuesEl?.textContent).toBe('123');
});
});

describe('defineInputBinds', () => {
Expand Down

0 comments on commit bfd6b00

Please sign in to comment.