vee-validate icon indicating copy to clipboard operation
vee-validate copied to clipboard

The best way to omit `useField`?

Open yaquawa opened this issue 3 years ago • 7 comments

I wish this code will work so we can omit writing the code for useField for each field one by one.

<template>
  <input v-model="formValues.email" placeholder="email" />
  <pre v-text="JSON.stringify(errors)"></pre>
</template>

<script>
import { useForm } from "vee-validate";
import { string, object } from "yup";
import { reactive } from "vue";

export default {
  name: "App",
  setup() {
    const validationSchema = object({
      email: string().required().email(),
      password: string().required(),
    });

    const formValues = reactive(getTheDataFromApi());

    const { errors } = useForm({
      validationSchema,
      initialValues: formValues,
    });

    return {
      errors,
      formValues,
    };
  },
};
</script>

This is a simple validation schema, imagine if you have tons of fields, that will be a nightmare to use useField for each field, things even goes more complex when the schema is a nested one.

Sure I could do this by using the Field component with the v-model, for example <Field type="email" v-model="formValues.email" />, but I think it'll be more convenient if I could do this with the composition API.

If the initialValues accepts reactive value, why not just use it as the returned value of useField? I think this could be done by just watching the initialValues and validate the form when it changes. What do you think?

yaquawa avatar Jan 28 '22 09:01 yaquawa

useField does more than just holding the value, it informs the vee-validate form what kind of field it is and the path it occupies. In your example mutating the form values makes a lot of sense since it is a simple flat object. But consider a form with complex nested paths like arays:

{
  name: '',
  array: [{ test: '' }]
}

In the previous example, it is hard to tell which part of the array is the actual field, Is it the array as a whole (field emits an array value) or is it a single property on one of its objects, or is it an element of that array (field emits an object value). Telling the difference doesn't matter in terms of validation but it is critical in terms of meta flags.

The problem with direct mutations is that it is impossible to figure out the user intent from a simple set operation. Let's say you do something like this:

const { values } = useForm();

function reset() {
  values.name = '';
  values.array = [];
}

function populate() {
  values.name = 'something';
  values.array = [...]
}

There is no inherent difference between the two functions here, and vee-validate cannot tell what kind of mutation it was to optimize the other aspects like errors, meta flags, and more. Such an open mutable API is dangerous to allow, a good example of this was in v3 of vee-validate where it piggybacked on the v-model you already have and to reset the form you always had to wait for a tick or two after changing your field values.

If the initialValues accepts reactive value, why not just use it as the returned value of useField?

Because the initial value may as well be a read-only value so it cannot be mutated and there is no way to tell that a Vue reactive or ref is read-only. Reactive here means it could be either: reactive proxy, ref, computed ref, shallowRef, readonly proxy, and customRef. The computed/customRef refs are impossible to tell if it is immutable or not so vee-validate cannot really assume that it is.

imagine if you have tons of fields

How do you plan to organize such fields? are they components that you created (if so then calling useField isn't really a deal-breaker here)? If they are 3rd party then I agree it would be hard to wrap each and every one of them with useField.


Having said all of this, I'm not against having a simple mutable form value here. I will try to think of a way to allow v-model to work on form values without compromising the existing features.

This actually works well in array fields in the v4.6 in #3618. So doing the same thing for form values could be tricky, but could be exposed under a different API that doesn't work with useField and mainly focuses on the model aspect. Like useFormModel or useFastForm as some libraries do that kind of variations for this use-case.

logaretm avatar Jan 29 '22 15:01 logaretm

Thanks @logaretm , I really appreciate that you invented such an awesome plugin for vue 3. In the past few months I really enjoyed it with using the components like <Filed> or <Form>, but when I recently started a new project, which use a third party UI framework comparing with where previous project was using just the vanilla form elements, I had a hard time to wrap the input components from third party UI framework into tons of <Field> elements.

In my usecase, I have a layer of model, which like a ORM, so for example:

const userProfle = reactive(new UserProfleModel(await getDataFromApi()))

const onSubmit = async ()=> {
  await userProfle.save()
  alert('user profile saved!')
}
<Form @submit="onSubmit" :validation-schema="schema">
  <Field v-model="userProfile.email" name="email" />
  <Field v-model="userProfile.firstName" name="firstName" />
  <Field v-model="userProfile.lastName" name="lastName" />
  <Field v-model="userProfile.company.name" name="company.name" />
  <Field v-model="userProfile.company.address" name="company.address" />
</Form>

as you can see I'm not even using the form values which passed over by vee-validate through the onSubmit, to me, the reactive two way binding is more easier to understand and far more less code to write and manage instead of using the useField and useForm.

But this time, I'm using a third party UI framework, so I have to wrap the form elements into the <Field> like

<Form @submit="onSubmit" :validation-schema="schema">
  <Field v-model="userProfile.email" name="userProfile.email">
    <MyInput v-model="userProfile.email"/>
  </Field>
</Form>

which looks horrible and hard to understand. I really wish I could just do something like this with the composition API.

<Form @submit="onSubmit" :validation-schema="schema">
  <MyInput v-model="userProfile.email"/>
</Form>

so I could just send the form values to the server as simple as this:

const onSubmit = async ()=> {
  await userProfle.save()
  alert('user profile saved!')
}

as you said, new API like useFormModel is a good idea I think, that way the initialValues should be ignored, and the input of useFormModel should be a reactive object, the return value is void or just the passed reactive object itself(in terms of naming maybe instead of useFormModel, bindFormModel is a better name?). so the pervious example could be written like this which is way more easier to work with.

useForm({
  validationSchema,
});

const userProfile = reactive(getTheDataFromApi())
useFormModel(userProfile)

by the way, if the field of model is type of array, I use a simple useArrayField composable to get the add and remove functions.

const { add, remove } = useArrayField(userProfile, 'friends')

Anyway, I'm really looking forward to have this kind of new API 😀

yaquawa avatar Jan 30 '22 07:01 yaquawa

@yaquawa have you already tried the following example? I can create my own input component with whatever the syntax/code I would like to embed but using useField just to parse the initial values and capture any errors.

https://codesandbox.io/s/custom-text-input-with-vee-validate-v4-8k9gy?from-embed=&file=/src/components/TextInput.vue

jimbatamang avatar Feb 06 '22 23:02 jimbatamang

@jimbatamang I guess you just missed my point, the point is that how to make the most out of vee-validate in terms of it's composition APIs. Currently, it's too verbose to use the composition APIs.

yaquawa avatar Feb 11 '22 14:02 yaquawa

This took a while but I merged a recent new feature that is similar to what is discussed here in https://github.com/logaretm/vee-validate/commit/26c828e21495c485d489ea1319575d9b5c271801

It mainly addresses this:

Currently, it's too verbose to use the composition APIs.

This is valid if you are only using useField as a model creator, which while is fine is not encouraged like I mentioned. Still it would be nice to have something that caters to the model users specific case.

So this is what the API looks like on main branch:

const { useFieldModel } = useForm(...);

// single field
const email = useFieldModel('email');
// multiple fields
const [email, name, password] = useFieldModel(['email', 'name', 'password']);

It supports nested fields as well but you should be careful when setting paths as you can easily create a model that changes an entire path like an array of objects.

The downside of this API is you lose any information related to meta for the individual fields, but you can still rely on the form's version of it. Also, validation will become aggressive since it has no field instances created with useField.

This will be tagged in 4.6 once I implement a few other changes that tie in nicely with this one.

logaretm avatar Jun 08 '22 21:06 logaretm

Hi @logaretm , nice work! Thank you for your effort for this enhancement, I'lll take a look at it later on.

yaquawa avatar Jun 10 '22 11:06 yaquawa

This took a while but I merged a recent new feature that is similar to what is discussed here in 26c828e

It mainly addresses this:

Currently, it's too verbose to use the composition APIs.

This is valid if you are only using useField as a model creator, which while is fine is not encouraged like I mentioned. Still it would be nice to have something that caters to the model users specific case.

So this is what the API looks like on main branch:

const { useFieldModel } = useForm(...);

// single field
const email = useFieldModel('email');
// multiple fields
const [email, name, password] = useFieldModel(['email', 'name', 'password']);

It supports nested fields as well but you should be careful when setting paths as you can easily create a model that changes an entire path like an array of objects.

The downside of this API is you lose any information related to meta for the individual fields, but you can still rely on the form's version of it. Also, validation will become aggressive since it has no field instances created with useField.

This will be tagged in 4.6 once I implement a few other changes that tie in nicely with this one.

I just started using vee-fleet v4 and was precisely looking for a way to avoid the validation to happen on my fields created with useFieldModel, would like to be able to either on focus or on blur to have those messages shown. So I guess Ill take a look for 4.6.

Thanks for the hard work.

carlos-at avatar Sep 02 '22 15:09 carlos-at

@logaretm Can't we keep a dirty state parallely ourselves for useFieldModel and only mark fields dirty if the user has mutated value ?

lallenfrancisl avatar Nov 15 '22 07:11 lallenfrancisl

4.9 introduced a new helper relevant to this.

With that, you can avoid wrapping fields with Field or useField especially with 3rd party components. However, it still means you need to call defineComponentBinds for every field you plan to use. This is a better alternative than useFieldModel because it also creates and maintains meta object.

With this, I don't think there is anything else to add to fully fix this. As always suggestions are welcome.

logaretm avatar May 07 '23 17:05 logaretm