Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Fields To Access Their Requiredness #1241

Open
Undistraction opened this issue Jan 15, 2019 · 26 comments
Open

Allow Fields To Access Their Requiredness #1241

Undistraction opened this issue Jan 15, 2019 · 26 comments

Comments

@Undistraction
Copy link

Undistraction commented Jan 15, 2019

🚀 Feature request

If I define a Yup schema, I would expect to be able to use the information I define there to render my fields. For example if I mark a field as 'Required' in the schema I would expect to be able to find that field's requiredness within the field / input components

Current Behavior

Currently this information is only available by pulling the information from the schema manually, either doing this for all fields at the form level, or by passing the schema down and doing it on a per-component level.

Desired Behavior

This information should be made available to form and fields in an easy-to-use form.

Suggested Solution

Add another prop object which is passed down to the form rendered by Formik:

{
  name: {
    tests: ['required']
  },
  email: {
    tests: ['required', 'validEmail']
  }
}

Who does this impact? Who is this for?

This would help anyone wanting to style or add elements to a field / field group based on the schema.

Describe alternatives you've considered

There is a manual alternative outlined in #712 but it really feels like this should be provided by Formik.

Another alternative would be to pass the schema down to the rendered form.

@adaam2
Copy link

adaam2 commented Jan 23, 2019

Just in case anyone else comes here and needs a practical example, as @Undistraction describes above, you can read straight from the Yup.js schema to determine if a field has been marked as .required(). Something like this would work, although admittedly it requires that you can access your validationSchema:

validationSchema
  .describe()
  .fields[name]
  .tests
  .find(testName => testName === "required")

@Undistraction
Copy link
Author

@adaam2 Yep. That's what I don't like about this approach. You need to pass the schema around which feels clumsy. I've ended up parsing all the fields in the schema and passing down a meta object that looks like:

{
   name: { required: true }
   email: { required: true }
   age: { required: false }
   ...
}

@jaredpalmer
Copy link
Owner

Related to #343

@jaredpalmer
Copy link
Owner

Not sure I love coupling formik to Yup even more tbh or having different behavior with/without Yup. Also FWIW you can implement this on your own if you build your own <Field> with connect().

@adaam2
Copy link

adaam2 commented Feb 6, 2019

That brings up the question about validationSchema.. aside from yup.js, what other libraries of a similiar ilk expose a comparable interface in terms of schema?

If none, then I would think that the coupling of yup and formik should be removed completely or extracted to a separate package.

@jaredpalmer
Copy link
Owner

I agree with this 100%. We can get there in v3 and warn when using validationSchema in v2. However part of Formik’s success is the convenience of Yup so we def need a separate package with all of not even more yup related enhancements.

@fulvio-m
Copy link

Just in case anyone else comes here and needs a practical example, as @Undistraction describes above, you can read straight from the Yup.js schema to determine if a field has been marked as .required(). Something like this would work, although admittedly it requires that you can access your validationSchema:

validationSchema
  .describe()
  .fields[name]
  .tests
  .find(testName => testName === "required")

While this is a decent solution, it doesn't work for me when using custom validation logic such as

name: Yup.string().when('someOtherField', {
                is: 'myValue',
                then: Yup.string().required(),
                otherwise: Yup.string()
            }).max(16)

The name field never shows the required validation test, only max

@shotokai
Copy link

shotokai commented Mar 18, 2019

Not sure I love coupling formik to Yup even more tbh or having different behavior with/without Yup. Also FWIW you can implement this on your own if you build your own <Field> with connect().

Building on what @jaredpalmer suggests, here is a <Field> with connect() that will give you required on your input. Use it in place of import { Field } from 'formik';

import React from 'react';
import { connect, getIn, Field } from 'formik';

const EnhancedField = props => {
  // All FormikProps available on props.formik!
  const validationSchema = props.formik.validationSchema;
  const fieldValidationSchema = validationSchema ? getIn(validationSchema.describe().fields, props.name) : false;
  const tests = fieldValidationSchema ? fieldValidationSchema.tests : false;
  const isRequired = tests ? !!tests.find(test => test.name === 'required') : false;
  const enhancedProps = { ...props };
  return (
    <Field required={isRequired} { ...enhancedProps} />
  );
};

export default connect(EnhancedField);

@joefiorini
Copy link

I tried @shotokai's solution, unfortunately it didn't work with schemas using when. There was a solution in jquense/yup#205, but a recent breaking change invalidated that approach. After some playing around I came up with this:

const EnhancedField = (
  props: { formik: FormikConfig<any> & FormikProps<any> } & { name: string },
) => {
  const { formik, name } = props;
  const validationSchema = formik.validationSchema;
  const fieldValidationSchema = validationSchema
    ? reach(validationSchema, name, formik.values, formik.values)
    : false;
  const resolvedSchema = fieldValidationSchema
    ? fieldValidationSchema.resolve({ value: formik.values })
    : false;
  const tests = resolvedSchema ? resolvedSchema.describe().tests : false;
  const isRequired = tests ? !!tests.find((test: any) => test.name === 'required') : false;
  const enhancedProps = { ...props };
  return <FormikField required={isRequired} {...enhancedProps} />;
};

@stale stale bot added the stale label May 27, 2019
@kylemh
Copy link
Contributor

kylemh commented Sep 6, 2019

@joefiorini what's reach() in the above example?

@stale stale bot removed the stale label Sep 6, 2019
@johnrom
Copy link
Collaborator

johnrom commented Sep 7, 2019

Just going to throw my $0.00 in, I think ideally we'd be able to standardize the use of schemas, but not be bound to any specific framework. Then we could do formik-ajv and formik-yup without being completely bound to any given schema processor. A schema processor like the hypothetical formik-yup could then read its schema and add properties to the field object. I have no clue how that could be typed in Typescript.

Some other projects involving schemas:

Schema based forms: https://react-jsonschema-form.readthedocs.io/en/latest/
Schema validation: https://github.com/epoberezkin/ajv
Schema to Yup: https://github.com/kristianmandrup/schema-to-yup

@atligudl
Copy link

@joefiorini what's reach() in the above example?

It's a function exported from Yup

import { reach } from 'yup';

@samueldepooter
Copy link

I tried @shotokai's solution, unfortunately it didn't work with schemas using when. There was a solution in jquense/yup#205, but a recent breaking change invalidated that approach. After some playing around I came up with this:

const EnhancedField = (
  props: { formik: FormikConfig<any> & FormikProps<any> } & { name: string },
) => {
  const { formik, name } = props;
  const validationSchema = formik.validationSchema;
  const fieldValidationSchema = validationSchema
    ? reach(validationSchema, name, formik.values, formik.values)
    : false;
  const resolvedSchema = fieldValidationSchema
    ? fieldValidationSchema.resolve({ value: formik.values })
    : false;
  const tests = resolvedSchema ? resolvedSchema.describe().tests : false;
  const isRequired = tests ? !!tests.find((test: any) => test.name === 'required') : false;
  const enhancedProps = { ...props };
  return <FormikField required={isRequired} {...enhancedProps} />;
};

Any other way to implement this? Seems like this doesn't work anymore (if using when) 😞

@Dashue
Copy link

Dashue commented May 21, 2020

Is it just me or is validationSchema no longer available on formik prop when connect()?

@jaredpalmer I understand the feel to not couple to yup, it would be great to have a simple approach we can implement ourselves that is realiable and doesn't hack internals/uses things it shouldn't

Would love everyone's thoughts now as a bit of time has passed

@stale stale bot removed the stale label May 21, 2020
@Dashue
Copy link

Dashue commented May 21, 2020

May have come up with a good pattern:


export const ValidationSchemaProvider = (props: { schema: any, children: ReactNode }) => {
    const { schema, children } = props;

    return (
        <Context.Provider value={{ schema }} >
            {children}
        </Context.Provider>
    );
};

const Context = createContext({ schema: undefined });

export const useValidationSchemaContext = () => {

    const { schema } = useContext(Context);

    const IsRequired = (name: string) => {
        const fieldValidationSchema = schema ? getIn(schema.describe().fields, name) : false;
        const tests = fieldValidationSchema ? fieldValidationSchema.tests : false;
        const isRequired = tests ? !!tests.find((test) => test.name === 'required') : false;

        return isRequired;
    };

    return {
        IsRequired,
    };
};
<Formik
                initialValues={initialValues}
                onSubmit={registerUser}
                validationSchema={validation}
            >
                {(props) => {
                    const { handleSubmit } = props;

                    return (
                        <Form onSubmit={handleSubmit}>
                            <ValidationSchemaProvider schema={validation}>
                                <h2>Account Details</h2>
                                <p>All fields are required when entering your account details.</p>
                                <TextField name='username' title='Username' />
                                <PasswordField name='password' title='Password' help={['Password must contain a number, a lowercase letter, an uppercase letter and a special character']} />
                                <TextField name='name' title='Name' />
                                <EmailField name='email' title='E-mail address' help={['We will send you a confirmation e-mail shortly.']} />

                                <SubmitButton text='Register' />
                            </ValidationSchemaProvider>
                        </Form>
                    );
                }}
            </Formik>

And in your field

 const { IsRequired } = useValidationSchemaContext();
 const required = IsRequired(name);

@johnrom
Copy link
Collaborator

johnrom commented May 21, 2020

That's a good pattern! My current thoughts are of a similar pattern, but where Formik manages the validation context. Based loosely off of this, we'd abstract the connection between Formik and Yup with a connector.

const MyForm = () => {
  const validator = useYupFormikValidator(yupSchema);

  return (
    <Formik validator={validator} validate={() => { /** additional validation **/ }}>
      <MyField name="firstName" />
    </Formik>
  );
}

const MyField = () => {
  const yup = useYupFormikContext();

  return null;
}

One comment with the above code @Dashue, you should wrap isRequired in a useCallback so it is memoized.

    const { schema } = useContext(Context);
    const isRequired = React.useCallback((name: string) => {
        // etc

        return fieldIsRequired;
    }, [schema]);

@Dashue
Copy link

Dashue commented May 25, 2020

@johnrom Thanks!
Just now saw your comment after changing the solution to pre-compute requiredfields. Not got much experience with memoization yet.

Updated code


const RequiredFieldsContext: Context<{ requiredFields: { [key: string]: boolean } }> = createContext({ requiredFields: undefined });
export const ValidationSchemaProvider = (props: { schema: any, children: ReactNode }) => {
    const { schema, children } = props;

    const requiredFields = {} as any;
    const fields = schema.describe().fields;
    for (const key in fields) {
        if (fields.hasOwnProperty(key)) {
            const fieldValidationSchema = fields ? getIn(fields, key) : false;
            const tests = fieldValidationSchema ? fieldValidationSchema.tests : false;
            requiredFields[key] = tests ? !!tests.find((test) => test.name === 'required') : false;
        }
    }

    return (
        <RequiredFieldsContext.Provider value={{ requiredFields }} >
            {children}
        </RequiredFieldsContext.Provider>
    );
};

const useRequiredFields = () => {

    const { requiredFields } = useContext(RequiredFieldsContext);

    return {
        requiredFields,
    };
};

@todor-a
Copy link

todor-a commented Aug 14, 2020

Hey, @Dashue thanks for sharing your solution. I have one question though:

schema.js:

export default {
  Type1: [step1Schema, step2Schema],
  Type2: [step1Schema]
};

Form.jsx:

 return (
    <Formik
      initialValues={getInitialFormValues()}
      validationSchema={validationSchema[type][step]}
    >
      {() => (
          <Form>
            <ValidationSchemaProvider
              schema={validationSchema[type][step]}>
                 // ... the form steps
            </ValidationSchemaProvider>
          </Form>
      )}
    </Formik>

When I call schema.describe() I get an error because schema has no function describe(). When I debug it, it first has the method but somewhere down the line the schema changes its type. Any idea?

@Dashue
Copy link

Dashue commented Aug 14, 2020

@tdranv
My guess is that you're switching the schema.
Anytime I've wanted to switch the value of a provider in other pieces of code, I have had to introduce state and a set mechanism as part of the context.

Without knowing your full case, my approach to multistep would be to have different forms per step, or introduce conditional validation logic with yup.

I'm assuming form validation schema wasn't built to handle being switched like that

@todor-a
Copy link

todor-a commented Aug 14, 2020

There are a few different schemas, exported as an array and on every step, I am switching the schema. The weird part is that it renders 3 times, 2 of which it has describe() and the 3rd it doesn't.

What did you mean by set mechanism as part of the context. :?

Thanks for answering :)

@Dashue
Copy link

Dashue commented Aug 14, 2020

In this case I'm not sure if it's possible.
In other cases it involves passing a set function to the provider which would allow changing the schema inside the provider rather than from the outside like you do now.

This link kind of shows some examples of doing it. A good way of explaining the functionality is "how do I set the global theme from a child" the theme provider sits at the top of the app and takes a toggleTheme function, this toggleTheme is then provided to the child, either through useContext, or by passing it down as props. The child calls toggleTheme and the theme changes within the provider.

https://stackoverflow.com/questions/50502664/how-to-update-the-context-value-in-provider-from-the-consumer

So you want the ValidationSchemaProvider to take schema and setSchema, but I've never done this with yup or formik, it's a simpler scenario when changing the theme or logged in user etc.

Perhaps another angle to work is figuring out why it renders 3 times, if that's not expected?
Figuring that out and preventing that may be the better fix?

@todor-a
Copy link

todor-a commented Aug 14, 2020

False alarm... I had a key in the schema that was just key: "" and had forgotten to define validation for it. For some reason that was causing the whole thing to freak out and think the object is not a schema. Thanks, @Dashue :)

@Dashue
Copy link

Dashue commented Aug 14, 2020

Good you found the issue!

@maximilianoforlenza
Copy link

@samueldepooter I have the same issue. Did you find any solution for this?

@TechWilk
Copy link

+1 Completely agree this should be a feature & that the workaround feel unclean

@ntoporcov
Copy link

ntoporcov commented Aug 10, 2023

Inspired by @todor-a, this was my solution

import { Formik, FormikValues } from "formik";
import { createContext, useContext } from "react";
import { ObjectSchema } from "yup";

const SchemaContext = createContext<ObjectSchema<any> | undefined>(undefined);

export function FormikWithSchema<T extends FormikValues = FormikValues>(
  props: Parameters<typeof Formik<T>>[0],
) {
  return (
    <SchemaContext.Provider value={props.validationSchema}>
      <Formik {...props} />
    </SchemaContext.Provider>
  );
}

export const useSchemaContext = () => useContext(SchemaContext);

Then in my field components

function FormikTextField<F extends object, K extends keyof F = keyof F>(props: FormikTextFieldProps<F, K>) {
  const formik = useFormikContext<F>();
  const key = props.field as string;
  const fieldMeta = formik.getFieldMeta(key);
  const schema = useSchemaContext();

  // @ts-ignore
  const schemaIsNumber = schema?.fields?.[key]?.type === "number";
  // @ts-ignore
  const schemaIsRequired = schema?.fields?.[key]?.spec.optional === false;
  // @ts-ignore
  const schemaLabel = schema?.fields?.[key]?.spec.label;
  const safeLabel = label || schemaLabel || "Missing Label";

  const FormikProps = formik.getFieldProps(key);

  return (...)

Haven't fully looked into how to type the stuff coming from yup

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests