jaredpalmer / formik

Build forms in React, without the tears 😭
https://formik.org
Apache License 2.0
34.02k stars 2.79k forks source link

Can't validate an input through "fetch" #418

Closed andreasciamanna closed 6 years ago

andreasciamanna commented 6 years ago

Issue initially reported at https://github.com/jquense/yup/issues/174

Copy/pasting with some edits here, as the problem seems related to Formik.

I've configured a validation schema which deals with a text box and a flag.

Both are values of the Formik form.

The text box is a required field of type string. So far so good.

However, when the flag is set to true, I need to add an additional validation, which uses fetch to validate its content.

In concrete words:

I've currently tried using when, as it gives me access to the checkbox field and compares its value.

This is what I have:

domains[`languageDomains_${key}`] = Yup.string()
    .ensure()
    .required('Please enter a domain')
    .when(`domainsValidation_${key}`, {
        is:   true,
        then: Yup.string().test({
                                                            name:    'validate-domain',
                                                            test:    (value) => validateDomain(value).catch(false).then(true),
                                                            message: 'Unable to validate this domain',
                                                        }),
    });

validateDomain returns a promise from fetch.

This seems to cause an Uncaught (in promise) TypeError: Cannot read property 'length' of undefined at yupToFormErrors error in the console.

As I'm fairly new with Yup, React, Formik and JS6 development in general, I'm fairly sure I'm doing something wrong.

But I'm not new in code development and I've tried to use the most logical (to me) approaches, as well as trying to read the documentation, doing some research, but I could not find how to solve this.

The stack trace might be showing something useful, which however I can't see:

formik.es6.js:3237 Uncaught (in promise) TypeError: Cannot read property 'length' of undefined
    at yupToFormErrors (formik.es6.js:3237)
    at formik.es6.js:2972
    at <anonymous>

As you can see, there's nothing mentioning the app's scripts: only references to Formik's code.

Since I noticed the issue happens as soon as fetch is called, I think it may help to also include this part of the code, which can be very well wrong, as promises are still something new to me (together with all the rest :) ):

const validateDomain = (domain) => {
    return fetch('http://wpml-development.test/wp-json/wpml/v1/core/validate', {
        method:  "POST",
        headers: {
            'Content-Type': 'application/json',
            'X-WP-Nonce':   wpApiSettings.nonce
        },
        body:    JSON.stringify({
                                                            domain
                                                        }),
    })
        .then(response => {
            console.log('validateDomain: response', response);
            return response.json();
        })
        .then(json => {
            console.log('validateDomain: json', json);
            return json;
        })
        .catch(error => {
            console.log('validateDomain: error', error);
            return error;
        });
};

The console error appears a fraction of a second after the fetch call is made and before console.log('validateDomain: response', response); is called, meaning that it's happening sometime during the actual request/response.

Any help?

andreasciamanna commented 6 years ago

This seems a consistent issue.

I have tried to use the same logic in a different form:

A validateRootHTMLFile function, used to validate a file:

const validateRootHTMLFile = ({siteUrl, value}) => {
    const url = value && value.includes('/') ? value : `${siteUrl}/${value}`;

    return fetch(url, {
        method:  "GET",
        headers: {
            'Content-Type': 'application/json',
        },
    })
        .then(function (response) {
            if (!response.ok) {
                throw Error(response.statusText);
            }
            return response;
        }).then(function (response) {
            console.log('response', response);
            return response;
        }).catch(function (error) {
            console.log('error', error.message);
            return error;
        });
};

The validation schema:

const validationSchema = ({siteUrl}) => {
 return Yup
  .object()
  .shape({
       root_html_file_path: Yup.string().when([
                           'language_negotiation_type',
                           'directory_for_default_language',
                           'show_on_root'
                          ], {
                           is:        (language_negotiation_type, directory_for_default_language, show_on_root) => {
                            return language_negotiation_type === 1 && directory_for_default_language && show_on_root === 'html_file';
                           },
                           then:      Yup
                                  .string()
                                  .test({
                                      name:    'validate_html_page',
                                      test:    (value) => {
                                       return validateRootHTMLFile({
                                                      siteUrl,
                                                      value
                                                     });
                                      },
                                      message: '???',

                                      exclusive: true,
                                     }),
                           otherwise: Yup.string(),
                          }),
      });
};

Then I have the field:

<Field
    name="root_html_file_path"
    id="root_html_file_path"
    type="text"
/>
{errors.root_html_file_path && (<Notice
    type="error"
>
    {errors.root_html_file_path}
</Notice>)}

Whenever I edit and exit the field, I get the Cannot read property 'length' of undefined error.

The behaviour is the same, no matter how I set validateOnBlur or validateOnChange.

The same validateDomain function, when used in an onChange or onBlur event of the field, works just fine.

Finally, with the console error, I would expect the validation to fail anyway, but it seems to not be happening, as root_html_file_path.error is never true and yet, I the field gets blocked by Formik until I remove validationSchema.

jaredpalmer commented 6 years ago

How are you passing your validate function to Formik? With async validate, you need to throw an error object and not simply return it as stated in the documentation.

andreasciamanna commented 6 years ago

The above validationSchema function is passed to withFormik:

const formikForm = withFormik({
    form:               'wpmlSettingsLanguagesURLFormat',
    enableReinitialize: true,
    mapPropsToValues,
    handleSubmit,
    validationSchema,
})(LanguagesURLFormat);

With async validate, you need to throw an error object and not simply return it as stated in the documentation.

Where I'm supposed to throw an error? Not in the catch: I've tried and in addition to the above error, I get, as expected, a Uncaught (in promise) Error: Error: Not Found.

Sorry if the question is silly, but I'm still trying to grok promises :)

andreasciamanna commented 6 years ago

Ok, I see that following the example "Asynchronous and return a Promise that's error in an errors object" and using the validate property instead of Yup, I could manage to make it work.

The problem is that, unless I can use both validate and validationSchema, I can't take advantage of Yup.

jaredpalmer commented 6 years ago

Correct.