0

I have a component with a a form, AddExpense.tsx. The form should have the option to add another category. This is a separate, decoupled form, stored in the AddCategory.tsx component. I need this component to appear visually in the AddExpense form, but unfortunately this seems to break the AddCategory event handler. How can I nest these forms and have them work with separate submitHandlers?

AddExpense.tsx:

export const AddExpense = (props: AddExpenseProps) => {
  const { user } = useAuth()
  const [addCategoryIsHidden, setAddCategoryIsHidden] = useState(true)
  const [state, setState] = useState({
    category: '',
    amount: 0, 
    notes: '', 
    date: '',
    user_id: user.info.user_id
  })
  const [validated, setValidated] = useState(false)

  const { categories } = props
  

  const handleSubmit = async(e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    console.log('yo')
    const form = e.currentTarget;
    if (form.checkValidity() === true) {
      
      await fetch('/api/addExpense', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(state)
      }).then(result => {
        console.log(result)
        if (result.status === 200) {
          console.log('success!')
        } else {
          console.log('error!')
        }
      })

    } else {
      e.preventDefault();
      e.stopPropagation();
    } 
    setValidated(true);
  }

  return (
    <div className='form-wrapper'>
      <Form noValidate validated={validated} onSubmit={handleSubmit}>
        <Form.Group controlId='expenseCategory'>
          <Form.Label>Category</Form.Label>
          <Form.Control 
            required as='select'
            onChange={(e) => setState({...state, category: e.target.value })}
          >
            {categories.map((category, index) => (
              <option key={index} value={category.category_name}>{category.category_name}</option>
            ))}
          </Form.Control>
        </Form.Group>
        <a
        onClick={(e) => setAddCategoryIsHidden(false)}
        style={{cursor: 'pointer', color: 'blue', textDecoration: 'underline', textDecorationColor: 'blue'}}>
          Add a new category
        </a>
        <AddCategory hidden={addCategoryIsHidden} type='expenses'/>
        <Form.Group controlId='expenseAmount'>
          <Form.Label>Amount</Form.Label>
          <Form.Control 
            required 
            type="number"
            step=".01"
            onChange={(e) => setState({...state, amount: parseFloat(e.target.value) })}/>
          <Form.Control.Feedback type="invalid">
            Please provide a valid amount.
          </Form.Control.Feedback>
        </Form.Group> 
        <Form.Group controlId="expenseNotes">
          <Form.Label>Notes</Form.Label>
          <Form.Control 
            as="textarea" 
            rows={3} 
            placeholder="(optional)"
            onChange={(e) => setState({...state, notes: e.target.value})}/>
        </Form.Group>
        <Form.Group controlId="expenseDate">
          <Form.Label>Date</Form.Label>
          <Form.Control 
            required 
            type="date" 
            name='date_of_expense'
            onChange={(e) => setState({...state, date: e.target.value})}/>
          <Form.Control.Feedback type="invalid">
            Please select a date.
          </Form.Control.Feedback>
        </Form.Group>
        <Button variant="primary" type="submit">
          Submit
        </Button>
      </Form>
    </div>
  )
}

AddCategory.tsx:

export const AddCategory = ({ type, hidden }: AddCategoryProps) => {
  const [newCategory, setNewCategory] = useState<string>()
  const { user } = useAuth()

  const handleSubmit = async(e: React.FormEvent<HTMLFormElement>) => {
    await fetch(`/api/addCategory`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        category_name: newCategory,
        user_id: user.info.user_id,
        type: type})
    })
  }

  return (
    hidden ? null : 
    <Form onSubmit={handleSubmit}>
      <Form.Group controlId='addCategory'>
        <Form.Control
          required
          type="text"
          placeholder="new category name"
          onChange={(e) => setNewCategory(e.target.value)}>
        </Form.Control>
      </Form.Group>
      <Button variant="primary" type="submit">
        Submit
      </Button>
    </Form>
  )
}
Leaozinho
  • 139
  • 1
  • 13

1 Answers1

0

The HTML5 spec doesn't allow nesting <form> elements (see discussion). I'm not sure what form library you're using, but it likely respects that convention and will misbehave when you nest one <Form> inside another.

You'll need to either

  • restructure your components by moving <AddCategory> to the bottom of <AddExpense> and outside of the parent <Form> component, or somewhere else like a modal, or

  • not use <Form> inside AddCategory; it looks fairly trivial, so you could just wire the input up to a useState and handle form "submission" with a button's onClick, or

  • remove the wrapping <Form> from AddCategory and just handle the category creation with the parent component's onSubmit

superhawk610
  • 2,457
  • 2
  • 18
  • 27