Skip to content

Commit 26c828e

Browse files
committed
feat: added useFieldModel to useForm API
1 parent 8aad333 commit 26c828e

5 files changed

Lines changed: 111 additions & 8 deletions

File tree

packages/vee-validate/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export type Locator = { __locatorRef: string } & ((values: GenericFormValues) =>
1515

1616
export type MaybeRef<T> = Ref<T> | T;
1717

18+
export type MaybeArray<T> = T | T[];
19+
1820
export interface FieldMeta<TValue> {
1921
touched: boolean;
2022
dirty: boolean;
@@ -159,6 +161,14 @@ export type FieldPathLookup<TValues extends Record<string, any> = Record<string,
159161
Record<keyof TValues, PrivateFieldContext | PrivateFieldContext[]>
160162
>;
161163

164+
export type MapValues<T, TValues extends Record<string, any>> = {
165+
[K in keyof T]: T[K] extends MaybeRef<infer TKey>
166+
? TKey extends keyof TValues
167+
? Ref<TValues[TKey]>
168+
: Ref<unknown>
169+
: Ref<unknown>;
170+
};
171+
162172
export interface PrivateFormContext<TValues extends Record<string, any> = Record<string, any>>
163173
extends FormActions<TValues> {
164174
formId: number;
@@ -184,6 +194,8 @@ export interface PrivateFormContext<TValues extends Record<string, any> = Record
184194
onSubmitValidationErrorCb?: InvalidSubmissionHandler<TValues>
185195
): (e?: Event) => Promise<TReturn | undefined>;
186196
setFieldInitialValue(path: string, value: unknown): void;
197+
useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
198+
useFieldModel<TPath extends keyof TValues>(paths: [...MaybeRef<TPath>[]]): MapValues<typeof paths, TValues>;
187199
}
188200

189201
export interface FormContext<TValues extends Record<string, any> = Record<string, any>>

packages/vee-validate/src/useFieldState.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { FormContextKey } from './symbols';
44
import { FieldMeta, FieldState, MaybeRef } from './types';
55
import { getFromPath, injectWithSelf } from './utils';
66

7-
interface StateSetterInit<TValue = unknown> extends FieldState<TValue> {
7+
export interface StateSetterInit<TValue = unknown> extends FieldState<TValue> {
88
initialValue: TValue;
99
}
1010

11-
interface FieldStateComposable<TValue = unknown> {
11+
export interface FieldStateComposable<TValue = unknown> {
1212
id: number;
1313
path: MaybeRef<string>;
1414
meta: FieldMeta<TValue>;
@@ -19,7 +19,7 @@ interface FieldStateComposable<TValue = unknown> {
1919
setState(state: Partial<StateSetterInit<TValue>>): void;
2020
}
2121

22-
interface StateInit<TValue = unknown> {
22+
export interface StateInit<TValue = unknown> {
2323
modelValue: MaybeRef<TValue>;
2424
standalone: boolean;
2525
}
@@ -74,12 +74,12 @@ interface FieldValueComposable<TValue = unknown> {
7474
/**
7575
* Creates the field value and resolves the initial value
7676
*/
77-
function _useFieldValue<TValue = unknown>(
77+
export function _useFieldValue<TValue = unknown>(
7878
path: MaybeRef<string>,
79-
modelValue: MaybeRef<TValue> | undefined,
80-
shouldInjectForm: boolean
79+
modelValue?: MaybeRef<TValue>,
80+
shouldInjectForm = true
8181
): FieldValueComposable<TValue> {
82-
const form = shouldInjectForm ? injectWithSelf(FormContextKey, undefined) : undefined;
82+
const form = shouldInjectForm === true ? injectWithSelf(FormContextKey, undefined) : undefined;
8383
const modelRef = ref(unref(modelValue)) as Ref<TValue>;
8484

8585
function resolveInitialValue() {

packages/vee-validate/src/useForm.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
FieldPathLookup,
3636
PrivateFieldArrayContext,
3737
InvalidSubmissionHandler,
38+
MaybeArray,
39+
MapValues,
3840
} from './types';
3941
import {
4042
getFromPath,
@@ -49,6 +51,7 @@ import {
4951
import { FormContextKey } from './symbols';
5052
import { validateYupSchema, validateObjectSchema } from './validate';
5153
import { refreshInspector, registerFormWithDevTools } from './devtools';
54+
import { _useFieldValue } from './useFieldState';
5255

5356
interface FormOptions<TValues extends Record<string, any>> {
5457
validationSchema?: MaybeRef<
@@ -179,6 +182,7 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
179182
stageInitialValue,
180183
unsetInitialValue,
181184
setFieldInitialValue,
185+
useFieldModel,
182186
};
183187

184188
function isFieldGroup(
@@ -282,6 +286,23 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
282286
Object.values(fieldArraysLookup).forEach(f => f && f.reset());
283287
}
284288

289+
function createModel<TPath extends keyof TValues>(path: MaybeRef<TPath>) {
290+
const { value } = _useFieldValue<TValues[TPath]>(path as string);
291+
watch(value, () => validate({ mode: 'validated-only' }));
292+
293+
return value;
294+
}
295+
296+
function useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath>): Ref<TValues[TPath]>;
297+
function useFieldModel<TPath extends keyof TValues>(paths: [...TPath[]]): MapValues<typeof paths, TValues>;
298+
function useFieldModel<TPath extends keyof TValues>(path: MaybeRef<TPath> | [...MaybeRef<TPath>[]]) {
299+
if (!Array.isArray(path)) {
300+
return createModel<TPath>(path);
301+
}
302+
303+
return path.map(createModel) as MapValues<typeof path, TValues>;
304+
}
305+
285306
/**
286307
* Sets the touched meta state on a field
287308
*/
@@ -722,6 +743,7 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
722743
setValues,
723744
setFieldTouched,
724745
setTouched,
746+
useFieldModel,
725747
};
726748
}
727749

packages/vee-validate/tests/helpers/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,14 @@ export async function dispatchFileEvent(input: HTMLInputElement, name: string |
9797
input.dispatchEvent(event);
9898
await flushPromises();
9999
}
100+
101+
export async function runInSetup(cb: () => any) {
102+
mountWithHoc({
103+
setup() {
104+
cb();
105+
},
106+
template: `
107+
<div></div>
108+
`,
109+
});
110+
}

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

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { FormContext, useField, useForm } from '@/vee-validate';
2-
import { mountWithHoc, setValue, flushPromises } from './helpers';
2+
import { mountWithHoc, setValue, flushPromises, runInSetup } from './helpers';
33
import * as yup from 'yup';
44
import { Ref } from 'vue';
55

@@ -411,4 +411,62 @@ describe('useForm()', () => {
411411
expect(meta[1]?.textContent).toBe('false');
412412
expect(meta[2]?.textContent).toBe('false');
413413
});
414+
415+
test('Creates a writeable model for a field path', async () => {
416+
await runInSetup(() => {
417+
const { values, useFieldModel } = useForm({
418+
initialValues: {
419+
greet: 'hey',
420+
age: 1,
421+
},
422+
});
423+
const greet = useFieldModel('greet');
424+
const age = useFieldModel('age');
425+
426+
expect(values.greet).toBe('hey');
427+
expect(values.age).toBe(1);
428+
greet.value = 'hello';
429+
age.value = 2;
430+
expect(values.greet).toBe('hello');
431+
expect(values.age).toBe(2);
432+
});
433+
});
434+
435+
test('Creates multiple writeable models for given field paths', async () => {
436+
await runInSetup(() => {
437+
const { values, useFieldModel } = useForm({
438+
initialValues: {
439+
greet: 'hey',
440+
age: 1,
441+
},
442+
});
443+
444+
const [age, greet] = useFieldModel(['age', 'greet']);
445+
446+
expect(values.greet).toBe('hey');
447+
expect(values.age).toBe(1);
448+
greet.value = 'hello';
449+
age.value = 2;
450+
expect(values.greet).toBe('hello');
451+
expect(values.age).toBe(2);
452+
});
453+
});
454+
455+
test('Validates the field model immediately and when it changes', async () => {
456+
await runInSetup(async () => {
457+
const { errors, useFieldModel } = useForm();
458+
const model = useFieldModel('test');
459+
460+
await flushPromises();
461+
expect(errors.value.test).toBe(REQUIRED_MESSAGE);
462+
463+
model.value = 'hello';
464+
await flushPromises();
465+
expect(errors.value.test).toBe('');
466+
467+
model.value = '';
468+
await flushPromises();
469+
expect(errors.value.test).toBe(REQUIRED_MESSAGE);
470+
});
471+
});
414472
});

0 commit comments

Comments
 (0)