Closed tobiasdiez closed 1 year ago
why not form.email.useField
or smth like that?
That's a nice suggestion, or even shorter const { value: email, errorMessage: emailError } = form.email
without the useField
at all.
Howdy all;
I also experienced some friction with Vue 3 compositional API and TypeScript experience around the current APIs. There seemed to be a fair bit of boilerplate and I didn't like the type casting via generics on useField
. I also was getting invalid types on the callback handler for handleSubmit
.
I ended up writing a userland utility to help with this.
import { toFormValidator } from "@vee-validate/zod";
import { SubmissionContext, useField, useForm } from "vee-validate";
import { reactive, Ref } from "vue";
import * as zod from "zod";
type MaybeRef<T> = Ref<T> | T;
// I had to reimpliment this interface as it is not exported by vee-validate
interface FieldOptions<TValue = unknown> {
initialValue?: MaybeRef<TValue>;
validateOnValueUpdate: boolean;
validateOnMount?: boolean;
bails?: boolean;
type?: string;
valueProp?: MaybeRef<TValue>;
checkedValue?: MaybeRef<TValue>;
uncheckedValue?: MaybeRef<TValue>;
label?: MaybeRef<string | undefined>;
standalone?: boolean;
}
// I had to reimpliment this interface as it is not exported by vee-validate
interface FormOptions<TValues extends Record<string, any>> {
initialValues?: MaybeRef<TValues>;
initialErrors?: Record<keyof TValues, string | undefined>;
initialTouched?: Record<keyof TValues, boolean>;
validateOnMount?: boolean;
}
export const useZodForm = <
Schema extends zod.ZodObject<any> = any,
Values extends zod.infer<Schema> = any,
>(
schema: Schema,
options: Omit<FormOptions<Values>, "validationSchema"> = {},
) => {
const validationSchema = toFormValidator(schema);
const form = useForm<Values>({
...options,
validationSchema: validationSchema as any,
});
return {
...form,
useField: <Field extends keyof Values, FieldType extends Values[Field]>(
field: Field,
opts?: FieldOptions<FieldType>,
) => reactive(useField<FieldType>(field as string, undefined, opts)),
handleSubmit: (
cb: (values: Values, ctx: SubmissionContext<Values>) => unknown,
) => form.handleSubmit(cb),
};
};
Note how I return a wrapped and typed version of useField
, and also a wrapped and typed version of handleSubmit
. I additionally wrap the useField
result with reactive
so I can just return out the entire value rather than having to destructure and rename value
and errorMessage
for each field.
You can also still pass in all the options to useField
and get all the other properties expected from the result of useField
and useForm
.
Usage with just useZodForm
export default defineComponent({
name: "RegisterForm",
setup() {
const { handleSubmit, errors, values } = useZodForm(
zod.object({
displayName: zod
.string()
.nonempty({ message: "Display name is required" }),
email: zod
.string()
.email({ message: "Must be a valid email" })
.min(1, "Email name is required"),
password: zod.string().min(1, "Password is required"),
}),
);
const onSubmit = handleSubmit(async (values) => {
console.log(values);
});
return {
errors,
values,
onSubmit,
};
},
});
Usage with useZodForm and useField
export default defineComponent({
name: "RegisterForm",
setup() {
const { handleSubmit, useField } = useZodForm(
zod.object({
email: zod
.string()
.min(1)
.email({ message: "Must be a valid email" }),
password: zod.string().min(1),
}),
);
const email = useField("email");
const password = useField("password");
const onSubmit = handleSubmit(async (values) => {
console.log(values);
});
return {
email,
onSubmit,
password,
};
},
});
Fields:
Handle submit callback:
Render function data:
Or you can use my variant of zod:
export const useZodForm = <Schema extends zod.ZodObject<AnyType>, Values extends zod.infer<Schema>>(
schema: Schema,
initialValues: Values
) => {
const form = useForm({
initialValues,
validationSchema: toFormValidator(schema),
});
const keys = Object.keys(form.values) as Array<keyof typeof form.values>;
const getField = <Field extends keyof typeof form.values>(field: Field) =>
computed({
get() {
return form.values[field];
},
set(value: typeof form.values[Field]) {
form.setFieldValue(field, value);
},
});
const fields = keys.reduce(
(acc, key) => ({
...acc,
[key]: getField(key),
}),
{} as { [key in keyof typeof form.values]: WritableComputedRef<typeof form.values[key]> }
);
return { form, fields };
};
and use it like this:
<template>
<input v-model="fields.firstName" />
<input v-model="fields.lastName" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import * as z from 'zod';
import { useZodForm } from '@/shared/hooks';
export default defineComponent({
setup() {
const { form, fields } = useZodForm(
z.object({
firstName: z.string(),
lastName: z.string(),
}),
{ firstName: '', lastName: '' }
);
form.handleSubmit(values => {
console.log(values);
});
return { fields };
},
});
</script>
Connection between form and useField method is more than welcome.
For yup I'm using this hook:
(need to upgrade yup to beta version)
import { Ref } from 'vue';
import { useField, useForm } from 'vee-validate';
import { ObjectSchema, InferType } from 'yup';
interface Form<Schema, Values> {
validationSchema: Schema;
initialValues: Values;
}
type Fields<F> = { [K in keyof F]: Ref<F[K]> };
export const useYupForm = <Schema extends ObjectSchema<any>, Values extends InferType<Schema>>({
initialValues,
validationSchema,
}: Form<Schema, Values>) => {
const form = useForm({
initialValues,
validationSchema,
});
const keys = Object.keys(form.values) as (keyof Values)[];
const fields = keys.reduce(
(acc, key) => ({
...acc,
[key]: useField(key).value,
}),
{} as Fields<Values>
);
return { form, fields };
};
Up. I really don't understand why this lib has support for zod if it's to get no types out of validation out of the box. Are the above workarounds the only solutions as of now?
For yup I'm using this hook:
(need to upgrade yup to beta version)
import { Ref } from 'vue'; import { useField, useForm } from 'vee-validate'; import { ObjectSchema, InferType } from 'yup'; interface Form<Schema, Values> { validationSchema: Schema; initialValues: Values; } type Fields<F> = { [K in keyof F]: Ref<F[K]> }; export const useYupForm = <Schema extends ObjectSchema<any>, Values extends InferType<Schema>>({ initialValues, validationSchema, }: Form<Schema, Values>) => { const form = useForm({ initialValues, validationSchema, }); const keys = Object.keys(form.values) as (keyof Values)[]; const fields = keys.reduce( (acc, key) => ({ ...acc, [key]: useField(key).value, }), {} as Fields<Values> ); return { form, fields }; };
How would you return other field attributes instead of value only? For example I'd need field meta too. Better yet, get typed useField method as return value instead of fields.
I really don't understand why this lib has support for zod if it's to get no types out of validation out of the box
Previously, vee-validate was able to infer the form values type from the validation schema however it wasn't perfect because:
yup
or zod
even if they don't need them. Remember that those aren't the only ways to write validation rules, so I need to find a better compromise here.If everybody was using yup
or zod
then maybe. But that's just not the case. I'm trying to find the time to re-explore this in a way that works for both type of users.
For yup I'm using this hook:
(need to upgrade yup to beta version)
import { Ref } from 'vue'; import { useField, useForm } from 'vee-validate'; import { ObjectSchema, InferType } from 'yup'; interface Form<Schema, Values> { validationSchema: Schema; initialValues: Values; } type Fields<F> = { [K in keyof F]: Ref<F[K]> }; export const useYupForm = <Schema extends ObjectSchema<any>, Values extends InferType<Schema>>({ initialValues, validationSchema, }: Form<Schema, Values>) => { const form = useForm({ initialValues, validationSchema, }); const keys = Object.keys(form.values) as (keyof Values)[]; const fields = keys.reduce( (acc, key) => ({ ...acc, [key]: useField(key).value, }), {} as Fields<Values> ); return { form, fields }; };
This is nice, but doesnt seem to work with nested objects inside your schema :/
4.8
is released with automatic schema inference for input/output type with either yup
or zod
. Check this for more information
This works very well, thanks!
Looks like what I use with react-hook-form but what about nested field autocompletion ?
So far in react, I'm able to do:
const { register, control, handleSubmit, control, formState: { dirtyFields, errors } } = useFormWithSchemaBuilder(
(yup) =>
yup.object({
firstname: yup.string().required("some message"),
})
);
// Render with register
<CustomInput
register={register("firstname")}
inputType="text"
placeholder={t("contact_form.firstname_placeholder")}
containLabel={{ label: t("contact_form.firstname_placeholder"), labelTransition: true }}
/>
// Render with controls (for controlled components)
<InputText
control={control}
name="firstname" // I get auto-completion on "firstName"
textContentType="emailAddress"
keyboardType="email-address"
autoComplete="email"
error={errors.firstname?.message}
autoCorrect={false}
autoCapitalize="none"
/>
import { AppText } from "@modules/ui/components/Text";
import React, { RefObject } from "react";
import { Control, Controller, Path } from "react-hook-form";
import { TextInput, View, TextInputProps } from "react-native";
import { useTailwind } from "tailwind-rn";
interface BaseInputProps {
label?: string;
rightIcon?: React.ReactNode;
error?: string;
}
interface ActiveInputProps<T> extends BaseInputProps {
control: Control<T>;
name: Path<T>;
inputRef?: RefObject<TextInput>;
}
interface DisabeldInputProps extends BaseInputProps {
value: string;
}
type Props<T> = ActiveInputProps<T> | DisabeldInputProps;
export function InputText<T>(
props: Props<T> &
Pick<
TextInputProps,
| "autoComplete"
| "keyboardAppearance"
| "keyboardType"
| "textContentType"
| "secureTextEntry"
| "passwordRules"
| "autoCapitalize"
| "autoCorrect"
| "placeholder"
| "onSubmitEditing"
| "returnKeyType"
>,
) {
const tw = useTailwind();
const { label, rightIcon, error } = props;
// Readonly field
if ("value" in props) {
return (
<>
{label && (
<AppText style={tw("text-text-strong mb-2")}>{label}</AppText>
)}
<View
style={tw(
"flex flex-row items-center rounded-lg border border-surface-strong bg-surface-base",
)}
>
<TextInput
style={tw("h-12 flex-grow px-4 py-2.5 text-text-soft font-Medium")}
value={props.value}
editable={false}
/>
{rightIcon && <View style={tw("mx-4")}>{rightIcon}</View>}
</View>
{error && (
<AppText size="sm" style={tw("text-danger-base mt-1")}>
{error}
</AppText>
)}
</>
);
}
const { placeholder, control, name, inputRef, ...rest } = props;
return (
<>
{label && <AppText style={tw("text-text-strong mb-2")}>{label}</AppText>}
<View
style={tw(
"flex flex-row items-center rounded-lg border border-surface-strong bg-surface-base",
)}
>
<Controller
control={control}
render={({ field: { onChange, onBlur, value } }) => (
<TextInput
style={tw("h-12 flex-grow px-4 py-2.5 text-text-soft")}
onBlur={onBlur}
onChangeText={onChange}
value={value as string}
placeholder={placeholder}
ref={inputRef}
{...rest}
/>
)}
name={name}
/>
{rightIcon && <View style={tw("mx-4")}>{rightIcon}</View>}
</View>
{error && (
<AppText size="sm" style={tw("text-danger-base mt-1")}>
{error}
</AppText>
)}
</>
);
}
This is possible because we have
control: Control<T>;
name: Path<T>;
types exported, how to achieve same with vee ?
Is your feature request related to a problem? Please describe.
Currently, there is no connection between the schema and the
useField
methods. Consider for example, the following code snippet taken from the documentationIf one renames
email
in the schema but forget to do this in theuseField
call, this is only noticed on runtime when theuseField
method is invoked. Moreover, in order to get proper type support for theemail
variable, one needs to useuseField<string>('email')
if I'm not mistaken. So the problem is that there is essentially no connection between the schema and theuseField
method.Describe the solution you'd like
Add a
useField
method toPublicFormContext
so that the example above can be written asNow, the
useForm
method can get enough typing information and pass this toform.useField
. The upshot is that one gets type errors if the parameter ofuseField
is not defined in the schema. At the same time, the type of the variableemail
could also automatically be inferred to be string, since this is its type in the schema.