Home  >  Q&A  >  body text

Vue 3 reusable error handling and reusable handleSubmit functions in "useForm" using the composition API

In recent web applications we have many forms with the same submission structure:

  1. Disable form and submit button based on isSubmitting variable
  2. Validate input fields (we use Yup)
  3. If validation fails: Set isSubmitting back to the false setting and display validationErrors
  4. on the input field
  5. If verification is successful: send post request with form data to api
  6. If the api is closed or returns an error, display a general error

I have tried doing some operations using the composition api in vue 3.

Login.vue

<template>
    <div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
        <div class="sm:mx-auto sm:w-full sm:max-w-md">
            <h1 class="text-3xl text-center text-gray-900">{{ t('sign_in_account', 1) }}</h1>
        </div>

        <div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
            <div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
                <form @submit.prevent="handleSubmit">
                    <fieldset :disabled="isSubmitting" class="space-y-6">
                        <MessageBox v-if="errors.general" :title="errors.general" :messages="errors.messages" />
                        <Input :label="t('email', 1)" type="text" id="email" v-model="user.email" :error="errors.email" />
                        <Password :label="t('password', 1)" type="password" id="password" v-model="user.password" :error="errors.password" />

                        <div class="text-sm text-right">
                            <router-link class="font-medium text-indigo-600 hover:text-indigo-500" :to="forgotPassword">{{ t('forgot_password', 1) }}</router-link>
                        </div>

                        <SubmitButton class="w-full" :label="t('sign_in', 1)" :submittingLabel="t('sign_in_loader', 1)" :isSubmitting="isSubmitting" />
                    </fieldset>
                </form>
            </div>
        </div>
    </div>
</template>

<script>
    import { ref } from 'vue';
    import { useStore } from 'vuex';
    import { useI18n } from 'vue-i18n';

    import useForm from '@/use/useForm';
    import { validateEmail, LoginValidationSchema } from '@/utils/validators';

    export default {
        setup() {
            const store = useStore();
            const { t } = useI18n({ useScope: 'global' });

            const user = ref({
                email: '',
                password: '',
            });

            const { handleSubmit, isSubmitting, errors } = useForm(user, LoginValidationSchema, handleLogin);

            async function handleLogin(values) {
                try {
                    return await store.dispatch('auth/login', values);
                } catch (error) {
                    if (error.response) {
                        console.log(error.reponse);
                        if (error.response.status == 422) {
                            errors.value = {
                                general: `${t('unable_to_login', 1)}<br /> ${t('fix_and_retry', 1)}`,
                                messages: Object.values(error.response.data.errors).flat(),
                            };
                        } else if (error.response.data.message) {
                            errors.value = {
                                general: error.response.data.message,
                            };
                        } else {
                            errors.value = {
                                general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
                            };
                        }
                    } else if (error.request) {
                        console.log(error.request);
                        errors.value = {
                            general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
                        };
                    } else {
                        errors.value = {
                            general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
                        };
                    }

                    return;
                }
            }

            return { t, user, handleSubmit, isSubmitting, errors };
        },
        computed: {
            forgotPassword() {
                return validateEmail(this.user.email) ? { name: 'forgotPassword', query: { email: this.user.email } } : { name: 'forgotPassword' };
            },
        },
    };
</script>

useForm.js

import { ref, watch } from 'vue';

export default function useForm(initialValues, validationSchema, callback) {
    let values = ref(initialValues);
    let isSubmitting = ref(false);
    let errors = ref({});

    async function handleSubmit() {
        try {
            errors.value = {};
            await validationSchema.validate(values.value, { abortEarly: false });
            isSubmitting.value = true;
        } catch (err) {
            console.log('In the catch');
            isSubmitting.value = false;

            err.inner.forEach((error) => {
                errors.value = { ...errors.value, [error.path]: error.message };
            });
        }
    }

    watch(isSubmitting, () => {
        if (Object.keys(errors.value).length === 0 && isSubmitting.value) {
            callback(values);
            isSubmitting.value = false;
        } else {
            isSubmitting.value = false;
        }
    });

    return { handleSubmit, isSubmitting, errors };
}

This works to some extent, but I'm missing two things. In useForm, I want to wait until the callback completes (success or failure) before setting isSubmitting back to false. Is commitment a good way to do this? Is there a better way? Secondly, I want a reusable way to handle errors in Login.vue. Any suggestions on how to deal with this issue?

P粉020556231P粉020556231205 days ago382

reply all(1)I'll reply

  • P粉301523298

    P粉3015232982024-03-28 11:01:09

    Regarding your first question - the try..catch statement has a third statement named finally which is always inside the try statement Execute after the block is completed.

    To answer your second question - Promises are a good way to handle asynchronous logic, including situations where the API you're sending the request to returns an error response, and then you can decide how to handle the user experience in that case.

    I'm not quite sure what you mean by handling errors in Login.vue in a reusable way, but maybe you could simply pass an empty array prop called formErrors of useForm and have your useForm.js emit an update:modelValue event to get two-way binding.

    reply
    0
  • Cancelreply