logaretm / vee-validate

✅ Painless Vue forms
https://vee-validate.logaretm.com/v4
MIT License
10.77k stars 1.26k forks source link

Expose useFormContext composable #4490

Open kendallroth opened 1 year ago

kendallroth commented 1 year ago

Is your feature request related to a problem? Please describe.

In cases where multiple form context elements are required, such as utility functions to manage state of reset/submit buttons depending on several factors within form context, it would be nice to have access to the form context via a composable.

For example, consider a sample function that checks whether a cancel button should be disabled or not. It checks multiple aspects of form context, which would be annoying to both get and pass in individually every time this is used 🤷. Instead, passing in FormContext allows calling the function and directly passing form context for ease of use.

/**
 * Disable cancel buttons when form is submitting or empty/unsubmitted
 *
 * @param form           - Form state (potentially from injected reference)
 * @param allowWhenEmpty - Allow cancelling (reset) when empty/unsubmitted (useful in dialogs, etc)
 */
export const shouldDisableCancel = (form: FormContext<FieldValues>, allowWhenEmpty = false) => {
  const isSubmittingOrValidating = form.isSubmitting.value || form.isValidating.value;

  // Some locations may allow cancelling when empty/unsubmitted (ie. dialogs)
  if (allowWhenEmpty) return isSubmittingOrValidating;

  // Must include submit count to allow resetting field after invalid submission attempt
  return isSubmittingOrValidating || (!form.meta.value.dirty && !form.submitCount.value);
};

Describe the solution you'd like

Expose a useFormContext hook to provide access to a wider array of values from within form context.

Describe alternatives you've considered

This can certainly be done by injecting the form context; however, this is not documented (perhaps intentially) except a brief reference under testing documentation. Additionally, it would be nice to expose this in the same manner as other similar composables (such as useFormValues, etc.

const formContext = inject<FormContext | undefined>(FormContextKey);
iamandrewluca commented 3 months ago

A workaround would be something like this. The problem is that the provided values by useForm is marked as readonly. If the values field (internally) from formCtx (PrivateFormContext) would be named differently, this would be possible.

This is possible because vee-validate shrinks only the type from PrivateFormContext to FormContext in reality it spreads entire private form context into the form context

https://github.com/logaretm/vee-validate/blob/926170d07bb201dbb220d0276cfc123ab2c4fbd7/packages/vee-validate/src/useForm.ts#L1200-L1205

<script setup>
const form = useForm();
const onSubmit = form.handleSubmit((values) => {});
</script>

<template>
    <FormContextProvider :form="form">
        <form @submit="onSubmit">
            <!-- ... -->
            <Field />
        </form>
    </FormContextProvider>
</template>

form-context.ts

export function usePrivateFormContext(form: FormContext): PrivateFormContext {
    const { handleReset, submitForm, ...formCtx } = form;
    return form as PrivateFormContext;
}

export const FormContextProvider = defineComponent({
    props: {
        form: {
            type: Object as PropType<FormContext>,
            required: true,
        },
    },
    setup: (props, ctx) => {
        const privateForm = usePrivateFormContext(props.form);
        provide(FormContextKey, privateForm);
        return () => ctx.slots.default?.();
    },
});
genu commented 1 month ago

A workaround would be something like this. The problem is that the provided values by useForm is marked as readonly. If the values field (internally) from formCtx (PrivateFormContext) would be named differently, this would be possible.

This is possible because vee-validate shrinks only the type from PrivateFormContext to FormContext in reality it spreads entire private form context into the form context

I like this approach, seems like it would be pretty clean.

The current limitation for me is that we can not have multiple forms at the same time.

A way to manage context would make everything so much more flexible.

@logaretm Any thoughts on move this along?

genu commented 1 month ago

Would it make sense to introduce a name or key to the current composables that would server as the injection key?

That way we can associate fields and forms respectively?

for example, this isn't possible at the moment as far as I know:

  const form1 = useForm()
  const form2 = useForm()

  const field1 = useField("sample field")
  const field2 = useField("sample field")

Which form do field1 and field2 belong to here?

From looking at the source code, it seems that they both belong to a form somewhere up component tree. I can't associate them to any of the forms I defined.