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.


32. Add Dropdown Input

Objective: Create a GenerateDropdown component that will allow for users to select from a dropdown list of options.

We will need to create one final input field type and that is for dropdown options. We will need this in the ingredient list in order to allow people to select a particular unit for he new ingredient that they are adding.

In the <GenerateIngredients> component, add an if block to the render part of the columns which will check to see if the name equals 'unit'. If it does, we will want to use this special dropdown input type, otherwise we will use our regular input type. We will be calling a <MenuList> component that we will be creating next but it takes a list of options called iterableList, a change handler and a name, which will be a path to the particular ingredient and field that we are on.

components/GenerateIngredients.tsx

import { Row, Col, Button, Table, Input, Dropdown } from 'antd';
import { MenuList } from './MenuList';
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;
};
const units = ['-', 'ounce', 'lb', 'cup', 'tb', 'tsp', 'g', 'kg'];
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 name === 'unit' ? (
<Dropdown
overlay={
<MenuList
iterableList={units}
name={`ingredients[${index}].${name}`}
handleDropdownChange={handleDropdownChange}
/>
}
placement="bottomLeft"
>
<Button>{ingredient[name]}</Button>
</Dropdown>
) : (
<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}
</>
);
};

Here is what the <MenuList> component looks like. It uses a <Menu> with an array of <Menu.Item> components inside of it, one for each option. When the user clicks the dropdown list and changes the selected unit, we fire off a handleDropdownChange change handler which we need to define next.

components/MenuList.tsx

import { Menu } from 'antd';
type MenuListProps = {
iterableList: string[];
name: string;
handleDropdownChange: (event: any) => void;
};
export const MenuList = ({
iterableList,
name,
handleDropdownChange,
}: MenuListProps) => (
<Menu>
{iterableList.map((item: string) => (
<Menu.Item
key={`${item}`}
title={`${name}`}
onClick={handleDropdownChange}
>
{item}
</Menu.Item>
))}
</Menu>
);

Due to the structure of the dropdown component, we need a slightly different handleDropdownChange handler which fetches information from different parts of the event object.

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 handleDropdownChange = (event) => {
setInputs((inputs) => {
const newInputs = _.cloneDeep(inputs);
_.set(newInputs, event.item.props.title, event.key);
return newInputs;
});
};
const handleAddIngredient = (event) => {
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,
handleDropdownChange,
};
};

Finally, we have to pass the handleDropdownChange into the <GenerateIngredients> component:

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,
handleDropdownChange,
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}
handleDropdownChange={handleDropdownChange}
/>
<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>
);
};