Archived course lesson

Add Form Ingredient

Part of Frontend Serverless with React and GraphQL

Objective: Create a GenerateIngredients component that will allow for users to add ingredients into a list for the recipe.

The next field type that we need to create is for handling ingredients which are stored as a JSON object. We will build on the components that we’ve already made to create this. We will use a table component from ant design as our structure for holding the data. The table component requires a list of values, which we get from the current ingredient data and also a list of columns which dictate how each value gets laid out in the table.

An ingredient has an ‘amount’ (a number), a ‘unit’ (essentially an enum), and a ‘type’ (a string). Since units can only be from a dropdown list of possibilities, we use the dropdown component there but otherwise we use an input field type.

In the code below, there is a small bug that we will fix in step 33 where it is missing the value field. We will see that it works here, but in a future step we will come back and fix that component to ensure that the fields exactly match what is being stored in the submitForm state.

import { Row, Col, Button, Table, Input } from 'antd';

type IngredientsProps = {
  names?: string[];
  values?: [{ amount: string; unit: string; type: string }];
  handleAddIngredient: (event: any) => void;
  handleDeleteIngredient: (event: any) => void;
  handleInputChange: (event: any) => void;
  handleDropdownChange: (event: any) => void;
};

export const GenerateIngredients = ({
  names,
  values,
  handleAddIngredient,
  handleDeleteIngredient,
  handleInputChange,
  handleDropdownChange,
}: IngredientsProps) => {
  const columns = names.map((name) => ({
    title: `${name}`,
    key: `${name}`,
    render: (ingredient, _record, index: number) => {
      return (
        <Input
          placeholder={`${name}`}
          name={`ingredients[${index}].${name}`}
          onChange={handleInputChange}
        />
      );
    },
  }));

  return (
    <>
      <Row>
        <Col span={12} offset={6}>
          <p>
            <Button
              onClick={handleAddIngredient}
              type="primary"
              shape="circle"
              size="small"
            >
              +
            </Button>
            ingredients:
          </p>
        </Col>
      </Row>
      {values.length > 0 ? (
        <Row>
          <Col span={12} offset={6}>
            <Table
              dataSource={values}
              columns={columns}
              pagination={{ pageSize: 25 }}
            />
          </Col>
        </Row>
      ) : null}
    </>
  );
};

Due to the added complexity of this ingredient form, we need to add an add ingredient handler which will allow us to add new ingredients and ensure that all items in the list have a key which is an ordered list starting from 0.

import { useState } from 'react';
import * as _ from 'lodash';

export const submitForm = (initialValues, callback) => {
  const [inputs, setInputs] = useState(initialValues);

  const handleInputChange = (event) => {
    event.persist();
    setInputs((inputs) => {
      const newInputs = _.cloneDeep(inputs);
      _.set(newInputs, event.target.name, event.target.value);
      return newInputs;
    });
  };
  const handleAddIngredient = (event) => {
    console.log('added');

    event.persist();
    setInputs((inputs) => {
      const sortedIngredients = _.sortBy(inputs.ingredients, ['key']);

      const key =
        sortedIngredients.length > 0
          ? sortedIngredients[sortedIngredients.length - 1].key + 1
          : 0;
      return {
        ...inputs,
        ingredients: _.concat(inputs.ingredients, [
          { key, amount: '', unit: '-', type: '' },
        ]),
      };
    });
  };

  const handleDeleteIngredient = () => console.log('deleted');

  const handleSubmit = () => {
    callback();
    setInputs(() => ({ ...initialValues }));
  };

  return {
    inputs,
    handleSubmit,
    handleInputChange,
    handleAddIngredient,
    handleDeleteIngredient,
  };
};

We can add the <GenerateIngredients> component to the <CreateRecipe> component and test that we can add ingredients and they should appear in a console.log after the form is submitted.

import { Row, Col, Form, Button } from 'antd';
import { submitForm } from '../utils/submitForm';
import { GenerateInput, GenerateTextInput } from './GenerateFields';
import { GenerateIngredients } from './GenerateIngredients';

export const CreateRecipe = () => {
  const initiateCreateRecipe = () => {
    console.log('submitted form');
    console.log(inputs);
  };

  const {
    inputs,
    handleInputChange,
    handleAddIngredient,
    handleDeleteIngredient,
    handleSubmit,
  } = submitForm(
    {
      title: '',
      description: '',
      content: '',
      ingredients: [],
    },
    initiateCreateRecipe,
  );

  return (
    <Form onFinish={handleSubmit}>
      <GenerateInput
        name="title"
        value={inputs.title}
        handleInputChange={handleInputChange}
      />
      <GenerateInput
        name="description"
        value={inputs.description}
        handleInputChange={handleInputChange}
      />
      <GenerateTextInput
        name="content"
        value={inputs.content}
        handleInputChange={handleInputChange}
      />
      <GenerateIngredients
        names={['amount', 'unit', 'type']}
        values={inputs.ingredients}
        handleAddIngredient={handleAddIngredient}
        handleDeleteIngredient={handleDeleteIngredient}
        handleInputChange={handleInputChange}
      />
      <Row>
        <Col span={16} />
        <Col span={4}>
          <Form.Item label="Create Recipe">
            <Button type="primary" htmlType="submit">
              Create Recipe
            </Button>
          </Form.Item>
        </Col>
      </Row>
    </Form>
  );
};