Frontend Serverless with React and GraphQL

Start at the beginning and work your way through this project. The code for each step as well as the finished project can be found in the Github repository.


31. Add Form Ingredient

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.

components/GenerateIngredients.tsx

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.

utils/submitForm.ts

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.

components/CreateRecipe.tsx

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>
);
};