jaredpalmer / formik

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

Nested Forms #826

Closed ericbiewener closed 5 years ago

ericbiewener commented 6 years ago

Current Behavior

There doesn't appear to be official support for nested forms since the documentation doesn't mention this possibility. However, it does work; you just need your nested form to render a <div> rather than <form>, and ideally watch for the right events (e.g. Enter pressed in an input field) to autosubmit the sub form. Ultimately, the fields below the sub form get their state from the sub form's formik context rather than the parent form. Everything works great. But I'm hesitant to use this approach without official support because of the possibility of a change to Formik that breaks this nested form functionality.

Suggested Solution

Primarily, I just want the documentation to officially recognize this as a supported feature. A nice step beyond that would be to include a component that handles listening to the right kinds of events to simulate standard browser form submission.

Describe alternatives you've considered

FieldArrays can be used instead, but they require more work on the part of the user. For example, lets assume you want to submit an array of { firstName, lastName } objects to the server. Using FieldArrays, your Formik state will end up with { firstName: string[], lastName: string[] }, rather than simply { name: { firstName: string, lastName: string}[] }. Therefore, the user needs to implement some custom onSubmit logic to transform the formik values into the desired data structure.

Additionally, using FieldArrays instead makes your sub form less reusable. One could imagine an address subform that sometimes gets used as the main form for an entity that only has a single address, while other times gets used within a field array for an entity that might have multiple address values.

ericbiewener commented 6 years ago

I have discovered that I can namespace my field names further like address.0.address_line_1, resulting in the desired formik state ({ address: [{ address_line_1, ...}] }). However, I have to do a lot more wiring to make sure errors from the API propagate correctly. In fact, I ultimately found that I can't use form.errors because those are getting cleared out. Instead, I have to bake my own solution using form.status. It's workable, but it takes substantially more code on my end, and it relies heavily on somewhat opaque field name namespacing. It also doesn't let me leverage a lot of the custom stuff we have already built on top of Formik for our base form functionality (e.g. submit button loading states, disabled states, etc), nor does it offer the kind of reusability I mentioned in the last paragraph of my original post.

dadambickford commented 6 years ago

@ericbiewener would you mind sharing your solution for propagating those errors down to nested fields? Currently running into the same issue.

+1 for including error handling support with the namespace fields feature

jaredpalmer commented 6 years ago

What is a sub form? Can you give an example?

FabianSellmann commented 6 years ago

Ideally the developer would be able to nest formik forms as such:

<Formik>
 (formik) => (
    <Formik name="subform">(subformik) => (
        <input name="username" {...<props>}/>)
    </Formik>)
</Formik>

Which would result in something like this:

values = { 
    "subform": { 
        "username": ""
    }
}

Which is much more modular then the naming convention and would allow to reuse form parts. Problem is how to handle submit.

ericbiewener commented 5 years ago

@YoungFaa this isn't what I meant, and it's actually already possible with formik. Just give your fields names like myNamespace.field1, myNamespace.field2

@jaredpalmer I'm referring to nesting <Formik> components inside of other <Formik> components. For example: https://codesandbox.io/s/42xxjrpz19

zeflq commented 5 years ago

@ericbiewener there is an error in the nested form: Warning: validateDOMNesting(...): <form> cannot appear as a descendant of <form>

ericbiewener commented 5 years ago

@zeflq right, that's why I noted in my original post that you need to render a div rather than a form for your nested form, and suggested that the ideal solution would "include a component that handles listening to the right kinds of events to simulate standard browser form submission."

jaredpalmer commented 5 years ago

@mellis481 I am very proud of the fact that I don't know things and that I don't have an ego about asking questions, even if they make me look silly or uninformed. I am actually familiar with the concept of nesting, just had not heard the terminology of "sub form."

However... 1) It's invalid HTML to nest <form> elements. 2) <Formik> uses React context and plain render props. To avoid name space conflicts with context with nesting you can just use props directly and access relevant variables within their scopes like so.

<Formik>
 {(formik) => (
    <form onSubmit={formik.handleSubmit}>
    <Formik>
       {(subformik) => (
          <form >
              <input name="username" onChange={subformik.handleChange} value={subformik.values.username} />
               {/** i also have access to formik here too */}
              <button onClick={() => subformik.submitForm()}>Submit Inner</button>
              <button onClick={() => formik.submitForm()}>Submit Outer</button>
          </form>
        )}
    </Formik>
    </form>
  )}
</Formik>
ericbiewener commented 5 years ago

Sorry, I should have stuck with the terminology of "nested forms" -- that would have kept things clearer.

@jaredpalmer Right, what I described in my original post already works. That's why my suggested solution was really just about documenting that Formik could be used in this way. My concern was that nesting forms like this wasn't officially supported functionality and could therefore break in the future without warning. However, I can understand if you think this is an unnecessary thing for the docs to specify.

In regards to nesting <form> tags resulting in invalid HTML, that's why I also suggested that some kind of NestedForm component could be a nice addition to Formik. It would essentially render a div and have event handlers that replicate standard form submission functionality. Here's what I've written for my project:

class NestedForm extends Component<{ onSubmit: (SyntheticEvent<>) => void }> {
  onKeyDownCapture = (e: SyntheticKeyboardEvent<HTMLInputElement | HTMLButtonElement>) => {
    // Not bothering to handle `<input type="submit" />`
    if (e.key === "Enter") {
      if (
        e.target instanceof HTMLInputElement ||
        e.target instanceof HTMLSelectElement ||
        (e.target instanceof HTMLButtonElement && e.target.type !== "button")
      ) {
        e.preventDefault(); // Prevents outer form from submitting
        this.props.onSubmit(e);
      }
    } else if (e.key === "Space") {
      this.checkForSubmitButton(e);
    }
  };

  checkForSubmitButton = (e: SyntheticEvent<>) => {
    if (e.target instanceof HTMLButtonElement && e.target.type !== "button") {
      this.props.onSubmit(e);
    }
  };

  render(): Node {
    const { onSubmit, ...props } = this.props;

    return (
      <div onKeyDownCapture={this.onKeyDownCapture} onClick={this.checkForSubmitButton} role="form" {...props} />
    );
  }
}
jaredpalmer commented 5 years ago

@ericbiewener As you've just shown, this seems like it can be implemented in user land as its implementation is very specific to the desired UX. Would be happy to add the published package to the resources tab of the docs once it's out.

ericbiewener commented 5 years ago

Yup, that sounds like the right approach rather than bloating your library :)

gastonmorixe commented 5 years ago

I see the issue here being Formik use of Context API, we might improve and maybe even namespace it.

Imagine you have one big form and then a popup appears which has another form, that is completely unrelated and submits to different API.

Since the popup could be implemented by using React.Portals which shares the context, or the popup could be just a position fixed div with high z-index, the form in the popup will trigger the formik in the first level.

One solution would be for every Formik to override the previous context and create a new.

ericbiewener commented 5 years ago

It does override the context of the outer Formik form already. As I mentioned in the OP, the nested form approach already works fine.

On Mon, Jan 21, 2019, 5:36 AM Gaston Morixe <notifications@github.com wrote:

I see the issue here being Formik use of Context API, we might improve and maybe even namespace it.

Imagine you have one big form and then a popup appears which has another form, that is completely unrelated and submits to different API.

Since the popup could be implemented by using React.Portals which shares the context, or the popup could be just a position fixed div with high z-index, the form in the popup will trigger the formik in the first level.

One solution would be for every Formik to override the previous context and create a new one so it doesn't pollute the context down the tree.

Another might be also namespacing the contexts, just for safety. Since the connected Fields should keep not knowing about the context name, but just using the nearest context.

β€” You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/jaredpalmer/formik/issues/826#issuecomment-456076377, or mute the thread https://github.com/notifications/unsubscribe-auth/AAPdcuBe_m7NsbzE1DQRUyj0BApMyVbxks5vFcJagaJpZM4V7CTc .

gastonmorixe commented 5 years ago

Yes sorry, just realized this today. There might still a problem with the an html button submit type, it submits both forms.

Using a normal button with the onClick only to the right Formik works fine.

Best,

Gaston

Sent from my iPhone

On Jan 21, 2019, at 1:26 PM, ericbiewener notifications@github.com wrote:

It does override the context of the outer Formik form already. As I mentioned in the OP, the nested form approach already works fine.

On Mon, Jan 21, 2019, 5:36 AM Gaston Morixe <notifications@github.com wrote:

I see the issue here being Formik use of Context API, we might improve and maybe even namespace it.

Imagine you have one big form and then a popup appears which has another form, that is completely unrelated and submits to different API.

Since the popup could be implemented by using React.Portals which shares the context, or the popup could be just a position fixed div with high z-index, the form in the popup will trigger the formik in the first level.

One solution would be for every Formik to override the previous context and create a new one so it doesn't pollute the context down the tree.

Another might be also namespacing the contexts, just for safety. Since the connected Fields should keep not knowing about the context name, but just using the nearest context.

β€” You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/jaredpalmer/formik/issues/826#issuecomment-456076377, or mute the thread https://github.com/notifications/unsubscribe-auth/AAPdcuBe_m7NsbzE1DQRUyj0BApMyVbxks5vFcJagaJpZM4V7CTc .

β€” You are receiving this because you commented. Reply to this email directly, view it on GitHub, or mute the thread.

ericbiewener commented 5 years ago

@mellis481 see this comment

Andreyco commented 5 years ago

Using the subformik approach above, I'm seeing a warning in react-dom.development.js (line 518):

Warning: validateDOMNesting(...): <form> cannot appear as a descendant of <form>. in form (created by Formik)

This is nothing but React telling you YOU are nesting HTML elements in unpredictable way. The unfortunate is Form is rendered by Formik, thus easiest thing is to blame Formik.

"Nesting" can be done

NicholasPeretti commented 5 years ago

Hello there!

I know this is a closed issue, but I've had the same problem regard "subforms" as you did (or do). In the last few months I've been working a lot with forms and I thought to write an article about my personal experience.

In the article I explain how I've decided to organise the forms in the application in order to maximise reusability, flexibility and scalability.

That is just the best way I've found to deal with subforms in my use cases, so you may found it useless. Nevertheless, I hope to add some kind of value to the next person that reads this issue, giving at least one possible solution to deal with subforms

I hope not to offend anyone publishing the link to the article in here, and if I do I apologise. https://auto1.tech/the-concept-of-subforms-with-formik/

diehell commented 5 years ago

@NicholasPeretti https://auto1.tech/the-concept-of-subforms-with-formik/

I've tried to create sub-forms according to your post, had problem to access the variable for the subforms. Maybe this is a stupid question but please bear with me. Let say i wanted to check the value in the subform, how would one do that?

Simplified Example:

Condition syntax

[props.namespaces][FIRSTNAME]  && (
        <Field component="input" name={withNamespace(LAST_NAME)} />
        <ErrorMessage name={withNamespace(FIRST_NAME)} />
)}
NicholasPeretti commented 5 years ago

Hi @diehell, thanks for reaching out.

As default approach to this I'd use the get from lodash. Since the withNamespace function just joins the namespace and the field name with a dot (.) it's possible to use the lodash function to get and set values down the tree. Let me make it clearer with some examples

Subform case

If you need to get the value in your subform you could just use a custom render method. If you pass a function as render prop it will give you also the value as param, so your code should look like this:

<Field name={withNamespace(FIRST_NAME)} render={({ field }) => (
        <input type="text" name={withNamespace(LAST_NAME)} {...field} />
        <ErrorMessage name={withNamespace(FIRST_NAME)} />
)} />

The field param contains the value property, so if you want to do additional logic on that field you just need to access to the property

{field.value < 10 && (
  //  ...
)}

Main form case

You may need to access to your subforms data from your main form. In this component you'll have the values variable provided by <Formik />. From there you can use the lodash.get approach to get the values you want from the values tree:

<Formik 
  // ...
  render={({ values }) => (
    // ...
    {get(values, withNamespace(FIRST_NAME)) && (
      <Field component="input" name={withNamespace(LAST_NAME)} />
      <ErrorMessage name={withNamespace(FIRST_NAME)} />
    )}
    // ...
  )}
/>

I've tried to do my best to show you how I'd do it. Anyway I don't know why you need to get the value of the field and neither where you need to get it (main form, subform...). For this reason I'm not sure I've been helpful.

If you could add some more detail about your use-case I'm sure I could give you some more hint and this could be useful also to the future users that will read this issue.

Feel free to get in touch with me for any further question πŸ˜„

flavioespinoza commented 4 years ago

Working off of what @jaredpalmer did here I got nested inputs working and populating the outer forms values.

Result on submit

note it populated the sub-properties of homeAddress:

{
  "firstName": "Flavio",
  "lastName": "Espinoza",
  "email": "flavio.espinoza@gmail.com",
  "mobilePhone": "650-695-6911",
  "homeAddress": {
    "type": "home",
    "street_address": "1538 S 400 E",
    "city": "Salt Lake City",
    "state": "UT",
    "zipcode": "84115"
  }
}

React component

import * as React from 'react';
import _ from 'lodash';
import { Formik, Field } from 'formik';
import { Button, Grid } from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';

const useStyles = makeStyles((theme) => ({
  container: {
    display: 'flex',
    flexWrap: 'wrap',
  },
}));

const NestedForm = () => {
  const classes = useStyles();
  return (
    <Formik
      initialValues={{
        firstName: '',
        lastName: '',
        email: '',
        mobilePhone: '',
        homeAddress: {
          type: 'home',
          street_address: '',
          city: '',
          state: '',
          zipcode: '',
        },
      }}
      onSubmit={(values, { setSubmitting }) => {
        setTimeout(() => {
          alert(JSON.stringify(values, null, 2));
          setSubmitting(false);
        }, 500);
      }}>
      {(formik) => {
        return (
          <Grid justify="center" className={'pl24 pr24'}>
              <h4>Required</h4>
              <Field
                name="firstName"
                className={'mb24 mt12'}
                onChange={formik.handleChange}
                placeholder={'First name'}
              />
              <Field
                name="lastName"
                className={'mb24 mt12'}
                onChange={formik.handleChange}
                placeholder={'Last name'}
              />
              <Field
                name="email"
                className={'mb24 mt12'}
                onChange={formik.handleChange}
                placeholder={'Email'}
              />
              <Field
                name="mobilePhone"
                className={'mb24 mt12'}
                onChange={formik.handleChange}
                placeholder={'Mobile phone'}
              />
              <br />
              <Formik className={classes.container}>
                {(subformik) => {
                  return (
                    <section>
                      <div>
                        <h4>Home address</h4>
                        <Field
                          name="homeAddress.street_address"
                          className={'mb24 mt12'}
                          onChange={formik.handleChange}
                          placeholder={'street address'}
                        />
                        <Field
                          name="homeAddress.city"
                          className={'mb24 mt12'}
                          onChange={formik.handleChange}
                          placeholder={'city'}
                        />
                        <Field
                          name="homeAddress.state"
                          className={'mb24 mt12'}
                          onChange={formik.handleChange}
                          placeholder={'state'}
                        />
                        <Field
                          name="homeAddress.zipcode"
                          className={'mb24 mt12'}
                          onChange={formik.handleChange}
                          placeholder={'zipcode'}
                        />
                      </div>
                      <Button
                        variant="contained"
                        color="primary"
                        className={classes.button}
                        onClick={() => formik.handleSubmit()}>
                        Submit
                      </Button>
                    </section>
                  );
                }}
              </Formik>
          </Grid>
        );
      }}
    </Formik>
  );
};

export default NestedForm;
johnrom commented 4 years ago

@flavioespinoza just want to point out that you do not necessarily need a "subform" here, instead you can use a custom field which accepts a value of your object and handles that slice of your state as if it is a single field.

const myForm = () => (
    <Field component={AddressField} name="homeAddress" />
);

const AddressField = ({name: parentFieldName}) => {
    const parentField = useField(parentFieldName); // v2, but you can get this from v1 as well
    const handleChange = useCallback(event => formik.handleChange(parentFieldName)({
        ...parentField.value,
        [event.target.name]: event.target.value
    ), [parentFieldName, parentField.value]);

    return <fieldset className="input--address">
        <Field
          name={`${parentFieldName}.street_address`}
          className={'mb24 mt12'}
          onChange={handleChange}
          placeholder={'street address'}
        />
        {/* etc */}
    </fieldset>
}

There are a million ways you can expand on this, but this is how I usually handle this situation.

bhishp commented 4 years ago

Hello there!

I know this is a closed issue, but I've had the same problem regard "subforms" as you did (or do). In the last few months I've been working a lot with forms and I thought to write an article about my personal experience.

In the article I explain how I've decided to organise the forms in the application in order to maximise reusability, flexibility and scalability.

That is just the best way I've found to deal with subforms in my use cases, so you may found it useless. Nevertheless, I hope to add some kind of value to the next person that reads this issue, giving at least one possible solution to deal with subforms

I hope not to offend anyone publishing the link to the article in here, and if I do I apologise. https://auto1.tech/the-concept-of-subforms-with-formik/

@NicholasPeretti thanks for the post, we used this pattern in our app and it helped us structure our big forms in a more componentized way. I created a storybook addon to help maintain forms in this way so that you can play with subforms directly in storybook and see the formik state in a panel. I hope it helps with others using this pattern πŸ‘

https://www.npmjs.com/package/storybook-formik

NicholasPeretti commented 4 years ago

Hi @bhishp ! Thank you for your feedback! I'm very happy that subforms worked out for your team!

Months passed by since I wrote the article and, in the meantime, I've been able to improve the way I am using subforms.

If you'd like to read more about it, I've also given a talk (unfortunately not available online). You can find the slides here: https://docs.google.com/presentation/d/1MgdcKdsj7eIbt3NFiDq2hxnszFTP5WF60nu0TWiaDvg/edit#slide=id.p

There's also a little app that I've used as example that can be very helpful, you can find it here: https://github.com/NicholasPeretti/subforms-example

I would really like to hear what were your struggles with subforms (if any) and how you and your team solved them! I've found some flaws in these months, like how to scale with validation.

As you can see from the Github repo I've started to export a function that returns an object instead of the validation schema directly. This allow you to have two subforms at the root of the form, like this:

const rootValidationSchema = yup.shape({
  ...subForm1.validationSchema(),
  ...subForm2.validationSchema().
})

Please take a look at the codebase if you want to read more about it! ☺️

Jared-Dahlke commented 3 years ago

what if i have nested forms and need a 'isValid' prop and an 'errors' prop for each form?

johnrom commented 3 years ago

@JaredDahlke you can manage these in userland like

const isValid = childForm.isValid && parentForm.isValid;
// this is oversimplified. you'll want to memoize, and 
// if you have nested values like `{ name: { first: '' } }`, you'll need a deep merge.
const errors = { ...childForm.errors, ...parentForm.errors };
Jared-Dahlke commented 3 years ago

Guys I've been able to implement formik on all of my forms except the wizard/nested form. I've made multiple attempts but have been unable to update the master formik from within a child formik. Here is an example of what I am trying to do. Any help/pointers would be great. I am not married to this structure , but i do need live/real time validation on each sub form with the user having to click any button, needs to validate on input. The Wizard needs to know the validation status of the 2 sub components. This is a simplified example of what im trying to build for my company:

 //Wizard.js

 import React from 'react';
 import { Formik, Form, Field } from 'formik';
 import * as Yup from 'yup';

let defaultEmail = 'test@test.com'
let defaultLastName = 'Smith'

export default function WizardHome(props) {

const [activeComponent, setActiveComponent] = React.useState(0)

const handleChangeComponent=()=>{
  if(activeComponent === 0) {
    setActiveComponent(1)
  } else {
    setActiveComponent(0)
  }
}

  return (
    <Formik
      initialValues={{
        componentOneIsValid: false,
        componentTwoIsValid: false            
      }}
    > 
    {formik => (
    <div>   
        <div>Component 1 is valid?: {formik.values.componentOneIsValid ? 'True' : 'False'}</div> <br/>
        <div>Component 2 is valid?: {formik.values.componentTwoIsValid ? 'True' : 'False'}</div> 
          {activeComponent === 0 ?
          <div>         
            <ComponentOne masterFormik={formik}/>            
          </div>
            : 
          <div>         
            <ComponentTwo masterFormik={formik}/>            
          </div>

          }
          <button onClick={handleChangeComponent()}>Change Component</button>

    </div>

    )}

    </Formik>

  )
}

//ComponentOne.js

import { Formik, Form, Field } from 'formik';
import * as Yup from 'yup';

const componentOneSchema = Yup.object().shape({
 lastName: Yup.string()
   .min(2, 'Too Short!')
   .max(50, 'Too Long!')
   .required('Required'),
 email: Yup.string().email('Invalid email').required('Required'),
});

export default  ComponentOne =(props)=>{

  if (formikOne.isValid) {   //I know I don't have access to this here, but how do i get it?
    props.masterFormik.values.componentOneIsValid = true  //I don't think it lets me set this from here, how can I do?
  } 

  return (
    <Formik
      initialValues={{
        email: defaultEmail,
        lastName: defaultLastName            
      }}
      validationSchema={componentOneSchema}
    > 
    {formikOne => (
      <div>    
        <Field name="email"/>
        <Field name="lastName"/>                
      </div>

    )}

    </Formik>
  )
}

//ComponentTwo.js

  //... same as component one but with different fields
rich186 commented 3 years ago

I put together a NestedForm component for Formik using some of the snippets in this thread. Here you go:

import { Formik, FormikProps } from 'formik';
import React, { isValidElement } from 'react';

interface Props<Values> {
  initialValues: Values;
  onSubmit: (values: Values) => void;
  children?: ((props: FormikProps<Values>) => any) | any;
}

function NestedForm<Values>({
  initialValues,
  onSubmit,
  children,
}: Props<Values>) {
  function onKeyDownCapture(form: FormikProps<Values>) {
    return (e: any) => {
      if (e.key === 'Enter') {
        if (
          e.target instanceof HTMLInputElement ||
          e.target instanceof HTMLSelectElement ||
          (e.target instanceof HTMLButtonElement && e.target.type !== 'button')
        ) {
          e.preventDefault(); // Prevents outer form from submitting
          onSubmit(form.values);
        }
      } else if (e.key === 'Space') {
        checkForSubmitButton(e);
      }
    };
  }

  function checkForSubmitButton(form: FormikProps<Values>) {
    return (e: any) => {
      if (e.target instanceof HTMLButtonElement && e.target.type !== 'button') {
        onSubmit(form.values);
      }
    };
  }

  return (
    <Formik initialValues={initialValues} onSubmit={() => null}>
      {form => (
        <div onKeyDownCapture={onKeyDownCapture(form)} onClick={checkForSubmitButton(form)}>
          {isValidElement(children) ? children : children(form)}
        </div>
      )}
    </Formik>
  );
}

Use like a normal Formik form:

<NestedForm initialValues={values} onSubmit={handleSubmit}>
  ...
</NestedForm>

Just extend it if you want validation schemas etc.

rkingon commented 3 years ago

omg... the useFormik documentation makes soooooo much sense now... πŸ˜‚

"You are Jared" haha

andrew-aladev commented 2 years ago

I can describe use case where nested form is required. I have cards interface with grid layout. All cards are a part of main form. But one card is separated and it is a part of another form.

Grid layout is working with descendant nodes only. It is not possible to separate another form (into any ancestor) and keep grid layout. So I have to remove formik from nested form or remove grid layout.

My grid layout can't be implemented tables or floats, so I have to keep it. In this case I have to remove formik from nested form.