Reuse in the react - javascript

I need to reuse some basic components to avoid code duplication but some examples are not serving me
the problem is here const [cpf, setCpf] = useState (data.cpf);
this data.cpf is getting information from an api but not filling the const as initial value
HOC
const onHandleChange = onChange => event => {
const { value } = event.target;
onChange(value);
};
Field:
export const Field = ({ component: Component, onChange, ...props }) => (
<div>
<Component onChange={onHandleChange(onChange)} {...props} />
</div>
);
InputField:
import React, { useState } from "react";
//Componentes
import { InputGroup, Button, FormControl } from "react-bootstrap";
import EditIcon from "#material-ui/icons/Edit";
import CheckIcon from "#material-ui/icons/Check";
export function InputField({ value, name, type, onChange }) {
const [teste, setTeste] = useState(true);
function click(e) {
setTeste(!teste);
}
return (
<div>
<InputGroup>
<InputGroup.Prepend>
<InputGroup.Text
style={{
backgroundColor: "white",
borderStyle: "hidden"
}}
>
{name}
</InputGroup.Text>
</InputGroup.Prepend>
<FormControl
style={{ borderStyle: "hidden" }}
disabled={teste}
value={value}
type={type}
onChange={onChange}
/>
<InputGroup.Append>
{teste === true ? (
<Button variant="white" onClick={click}>
<EditIcon />
</Button>
) : (
<Button variant="white" onClick={click}>
<CheckIcon />
</Button>
)}
</InputGroup.Append>
</InputGroup>
</div>
);
}
ViewClient:
import React, { useState } from "react";
//UI-Components
import { Modal, Card, ListGroup, Button } from "react-bootstrap";
import Account from "#material-ui/icons/AccountCircle";
//Reecriação de componentes
import { Field } from "./Components/Input";
import { InputField } from "./Components/InputFieldComponent";
//Redux
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
//Creators
import { Creators as ClientActions } from "../../../redux/store/ducks/cliente";
import { Creators as MapActions } from "../../../redux/store/ducks/map";
import { Creators as CaboActions } from "../../../redux/store/ducks/cabo";
function ViewClient(props) {
const { viewClient } = props.redux.client;
const { data } = viewClient; //Informações do usuario.
const [cpf, setCpf] = useState(data.cpf);
const [name, setName] = useState(data.name);
function handleHideModal() {
const { hideClientViewModal } = props;
hideClientViewModal();
}
function changeActive() {
console.log(cpf);
}
return (
<>
<Modal size="lg" show={viewClient.visible} onHide={handleHideModal}>
<Modal.Header
style={{
justifyContent: "center",
display: "flex",
flexDirection: "column",
alignItems: "center",
backgroundColor: "#F7D358"
}}
>
<h6 style={{ fontSize: "10px" }}>{data.created_at}</h6>
<Account
style={{
display: "block",
fontSize: "50px",
marginTop: "10px",
marginBottom: "10px"
}}
/>
<Modal.Title style={{ color: "#585858" }}>{data.name}</Modal.Title>
</Modal.Header>
<Modal.Body style={{ backgroundColor: "#FFFFFF" }}>
<Card style={{ width: "100%" }}>
<Card.Header
style={{ backgroundColor: "#D8D8D8", textAlign: "center" }}
>
Informações do cliente
</Card.Header>
<ListGroup variant="flush">
<ListGroup.Item>
<Field
component={InputField}
name={cpf}
type={"text"}
value={cpf}
onChange={setCpf}
/>
</ListGroup.Item>
<ListGroup.Item>
<Field
component={InputField}
name={"Nome"}
type={"text"}
value={name}
onChange={setName}
/>
</ListGroup.Item>
</ListGroup>
</Card>
</Modal.Body>
<Modal.Footer>
<Button variant="info">Salvar Alterações</Button>
{data.status === null ? (
<Button variant="primary" onClick={changeActive}>
Ativar cliente
</Button>
) : (
<Button variant="danger" onClick={changeActive}>
Desativar
</Button>
)}
<Button variant="danger">Excluir</Button>
<Button variant="secondary">Adicionar Cabo</Button>
<Button variant="secondary">Fechar</Button>
</Modal.Footer>
</Modal>
</>
);
}
const mapStateToProps = state => ({
redux: state
});
//Ações
const mapDispatchToProps = dispatch =>
bindActionCreators(
{ ...ClientActions, ...MapActions, ...CaboActions },
dispatch
);
export default connect(
mapStateToProps,
mapDispatchToProps
)(ViewClient);

Related

Cannot read properties of null (reading 'result') error in my comments section implementation function

In my application, I have a Meal object that has a comment section attached to it. I get an error when trying to make a comment. There is the code:
import React, { useState } from 'react';
import { Card, CardActions, CardContent, CardMedia, Button, Typography } from '#material-ui/core/';
import ThumbUpAltIcon from '#material-ui/icons/ThumbUpAlt';
import DeleteIcon from '#material-ui/icons/Delete';
import MoreHorizIcon from '#material-ui/icons/MoreHoriz';
import ThumbUpAltOutlined from '#material-ui/icons/ThumbUpAltOutlined';
import moment from 'moment';
import {useDispatch, useSelector} from 'react-redux';
import { Modal, Box, TextField } from "#material-ui/core";
import useStyles from './styles';
import {deleteMeal, getMeals, likeMeal} from '../../../actions/meals';
import { postCommentOnMeal } from '../../../actions/workouts';
const Meal=({meal, setCurrentId})=> {
const classes=useStyles();
const dispatch=useDispatch();
const user = JSON.parse(localStorage.getItem('profile'));
const Likes = () => {
if (meal.likes && meal.likes.length > 0) {
return meal.likes.find((like) => like === (user?.result?.googleId || user?.result?._id))
? (
<><ThumbUpAltIcon fontSize="small" /> {meal.likes.length > 2 ? `You and ${meal.likes.length - 1} others` : `${meal.likes.length} like${meal.likes.length > 1 ? 's' : ''}` }</>
) : (
<><ThumbUpAltOutlined fontSize="small" /> {meal.likes.length} {meal.likes.length === 1 ? 'Like' : 'Likes'}</>
);
}
return <><ThumbUpAltOutlined fontSize="small" /> Like</>;
};
const [open, setOpen] = React.useState(false);
const [currentMeal, setcurrentMeal] = React.useState("");
const handleOpen = (id) => {
setcurrentMeal(id);
setOpen(true);
};
const handleClose = () => setOpen(false);
const [comment, setComment] = useState("");
const style = {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: 400,
bgcolor: "background.paper",
border: "2px solid #000",
boxShadow: 24,
p: 4,
};
const currentUser = useSelector((state) => state.auth);
function addComment() {
const data = {
comment,
userId: currentUser.authData.result._id,
userName: currentUser.authData.result.name,
mealId: currentMeal,
};
dispatch(postCommentOnMeal(data));
dispatch(getMeals());
handleClose();
}
function getComments() {
return (
<>
{meal && meal.comments.length > 0
? meal.comments.map((item) => {
return (
<div
style={{
marginTop: 10,
backgroundColor: "grey",
padding: 2,
borderRadius: 4,
}}
>
<h5 style={{ marginBottom: -10 }}>{item.commentor}</h5>
<p>{item.comment}</p>
</div>
);
})
: "No comments yet"}
</>
);
}
return (
<>
<Card className={classes.card}>
<CardMedia className={classes.media} image={meal.selectedFile|| 'https://user-images.githubusercontent.com/194400/49531010-48dad180-f8b1-11e8-8d89-1e61320e1d82.png'} title={meal.category}/>
<div className={classes.overlay}>
<Typography variant="h6">{meal.calories}</Typography>
<Typography variant="body2">{moment(meal.createdAt).fromNow()}</Typography>
</div>
{(user?.result?.googleId===meal?.creator || user?.result?._id===meal?.creator) && (
<div className={classes.overlay2}>
<Button style={{color:'white'}} size="small" onClick={()=>{
setCurrentId(meal._id)
}}>
<MoreHorizIcon fontSize="medium"/>
</Button>
</div>
)}
<div className={classes.details}>
<Typography className={classes.title} gutterBottom variant="h5" component="h2">{meal.name}</Typography>
</div>
<Typography className={classes.difficulty} variant="body2" color="textSecondary" component="p">{meal.category}</Typography>
<CardContent>
<Typography variant="body2" color="textSecondary" component="p">{meal.calories}</Typography>
</CardContent>
<CardActions className={classes.cardActions}>
<Button size='small' color="primary" disabled={!user?.result} onClick={()=>{dispatch(likeMeal(meal._id))}}>
<Likes/>
</Button>
<Button
size="small"
color="primary"
disabled={!user?.result}
onClick={() => {
handleOpen(meal._id);
}}
>
Comments
</Button>
{(user?.result?.googleId===meal?.creator || user?.result?._id===meal?.creator) && (
<Button size='small' color="primary" onClick={()=>{dispatch(deleteMeal(meal._id))}}>
<DeleteIcon fontSize="small"/>
Delete
</Button>
)}
</CardActions>
</Card>
<Modal
className={classes.modal}
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box sx={style}>
{getComments()}
<TextField
margin="normal"
name="comment"
variant="outlined"
label="Comment"
fullWidth
value={comment}
onChange={(e) => {
setComment(e.target.value);
}}
/>
<Button
margin="normal"
variant="contained"
color="primary"
onClick={addComment}
>
POST
</Button>
</Box>
</Modal>
</>
);
}
export default Meal;
The error occurs at this line: userId: currentUser.authData.result._id, in the addComment() function and it says: Uncaught TypeError: Cannot read properties of null (reading 'result').
Could you help me?

Why are my testing-library queries not working?

I am using Jest/React Testing Library(RTL) to to unit testing for the UI.
None of my RTL queries work.
It keeps telling me errors like
TestingLibraryElementError: Unable to find an element by: [data-testid="analysis-categories-header"]
no matter how I query it.
I have tried getBy*, findBy*(yes, with async...await), getByTestId and everything.
The only difference is that when I use findBy* I get an extra error that says
Error: Error: connect ECONNREFUSED 127.0.0.1:80
even screen.debug() is ignored and not working.
Out of the 13 tests I have, only one test work surprisingly. The queries go through.
test:
import '#testing-library/jest-dom';
import { render, screen } from '#testing-library/react';
import { HelmetProvider } from 'react-helmet-async';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import Login from '../src/pages/login';
test('renders logo and login box with "LOG IN" text and two buttons', () => {
const initState = {};
const mockStore = configureStore();
render(
<HelmetProvider>
<Provider store={mockStore(initState)}>
<Login />
</Provider>
</HelmetProvider>,
);
expect(screen.getByTestId('login-logo')).toBeVisible();
expect(screen.getByTestId('login-title')).toBeVisible();
expect(screen.getByRole('button', { name: 'Sign In' })).toBeVisible();
expect(
screen.getByRole('button', { name: 'Sign In With Intuit' }),
).toBeVisible();
});
component:
import { useAuth0 } from '#auth0/auth0-react';
import {
Box,
Button,
Card,
CardContent,
Container,
Typography,
} from '#material-ui/core';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { FC } from 'react';
import React, { useEffect } from 'react';
import { Helmet } from 'react-helmet-async';
import Logo from 'src/components/Logo';
import axios from 'src/lib/axios';
import gtm from 'src/lib/gtm';
const Login: FC = () => {
const router = useRouter();
const { isLoading, isAuthenticated, loginWithRedirect } = useAuth0();
useEffect(() => {
gtm.push({ event: 'page_view' });
}, []);
useEffect(() => {
if (!isLoading && isAuthenticated) {
router.push('/recommendations');
}
}, [isLoading, isAuthenticated]);
const handleIntuitLogin = async () => {
try {
const response = await axios.get('/auth/sign-in-with-intuit');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
const handleAuth0Login = async () => {
try {
const response = await axios.get('/auth/sign-in-with-auth0');
window.location = response.data;
} catch (e) {
throw new Error(e);
}
};
return (
<>
<Helmet>
<title>Login</title>
</Helmet>
<Box
sx={{
backgroundColor: 'background.default',
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
}}
>
<Container maxWidth="sm" sx={{ py: '80px' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: 8,
}}
>
<Link href="/login">
<Box>
<Logo height={100} width={300} />
</Box>
</Link>
</Box>
<Card>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
p: 4,
}}
>
<Box
sx={{
alignItems: 'center',
display: 'flex',
justifyContent: 'space-between',
mb: 3,
}}
>
<div>
<Typography
color="textPrimary"
gutterBottom
variant="h4"
data-testid="login-title"
>
Log in
</Typography>
</div>
</Box>
<Box
sx={{
flexGrow: 1,
mt: 3,
}}
>
<Button
color="primary"
onClick={handleAuth0Login}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In
</Button>
</Box>
<Box sx={{ mt: 2 }}>
<Button
color="primary"
onClick={handleIntuitLogin}
fullWidth
size="large"
type="button"
variant="contained"
>
Sign In With Intuit
</Button>
</Box>
</CardContent>
</Card>
</Container>
</Box>
</>
);
};
export default Login;
The only difference is that this component (nextjs page) does not use redux thunk. This is how far I got to find the cause.

How to call a component from an onClick event

I would like to make it possible that whenever I click on a button (which is a component itself) a modal component is called. I did some research but none of the solutions is doing what I need. It partially works but not quite the way I want it to because, once the state is set to true and I modify the quantity in the Meals.js and click again the modal doesn't show up
Meals.js
import React, { useState } from "react";
import { Card } from "react-bootstrap";
import { Link } from "react-router-dom";
import NumericInput from "react-numeric-input";
import AddBtn from "./AddtoCartBTN";
function Meals({ product }) {
const [Food, setFood] = useState(0);
return (
<Card className="my-3 p-3 rounded">
<Link to={`/product/${product.id}`}>
<Card.Img src={product.image} />
</Link>
<Card.Body>
<Link to={`/product/${product.id}`} style={{ textDecoration: "none" }}>
<Card.Title as="div">
<strong>{product.name}</strong>
</Card.Title>
</Link>
Quantity{" "}
<NumericInput min={0} max={100} onChange={(value) => setFood(value)} />
{/* <NumericInput min={0} max={100} onChange={ChangeHandler(value)} /> */}
<Card.Text as="h6" style={{ color: "red" }}>
{Food === 0 ? product.price : Food * product.price} CFA
</Card.Text>
<AddBtn quantity={Food} price={product.price} productId={product.id} />
</Card.Body>
</Card>
);
}
export default Meals;
AddToCartBtn.js
import React, { useEffect, useState } from "react";
import { useDispatch } from "react-redux";
import { addToCart } from "../actions/cmdActions";
import toast, { Toaster } from "react-hot-toast";
import CartModal from "./CartModal";
function AddtoCartBTN({ quantity, price, productId }) {
const AddedPrice = quantity * price;
const [showModal, setShowModal] = useState(false)
const notify = () => toast("Ajout Effectue !", {});
const dispatch = useDispatch();
const AddtoCart = () => {
// console.log("Added to cart !");
// console.log("Added : ", AddedPrice);
if (AddedPrice > 0) {
dispatch(addToCart(productId, AddedPrice));
notify();
setShowModal(true)
}
};
return (
<button
className="btn btn-primary d-flex justify-content-center"
onClick={AddtoCart}
>
Ajouter
{showModal && <CartModal/>}
<Toaster />
</button>
);
}
export default AddtoCartBTN;
Modal.js
import React, { useState } from "react";
import { Modal, Button, Row, Col } from "react-bootstrap";
import { useSelector } from "react-redux";
import { useHistory } from "react-router-dom";
function CartModal () {
const [modalShow, setModalShow] = useState(true);
let history = useHistory();
const panierHandler = () => {
history.push('/commander')
}
const cart = useSelector((state) => state.cart);
const { cartItems } = cart;
function MyVerticallyCenteredModal(props) {
return (
<Modal
{...props}
size="md"
aria-labelledby="contained-modal-title-vcenter"
centered
backdrop="static"
>
<Modal.Header closeButton>
<Modal.Title id="contained-modal-title-vcenter">
<b style={{ color: "red" }}> choix de produits</b> <br />
</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<Col><b>Nom</b></Col>
<Col><b>Quantite</b></Col>
</Row><hr/>
{cartItems && cartItems.map((item,i)=>(
<>
<Row key={i}>
<Col>{item.name}</Col>
<Col>{item.total / item.price}</Col>
</Row><hr/>
</>
))}
</Modal.Body>
<Modal.Footer
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Button
// onClick={props.onHide}
onClick={panierHandler}
>
Aller au Panier
</Button>
</Modal.Footer>
</Modal>
);
}
return (
<>
<MyVerticallyCenteredModal
show={modalShow}
onHide={() => setModalShow(false)}
/>
</>
);
}
export default CartModal
in AddToCartBtn.js, you can pass setShowModal as a prop.
{showModal && <CartModal setShowModal={setShowModal} />}
And the, in Modal.js, you can use the prop setShowModal to set the value to false
<MyVerticallyCenteredModal show={true} onHide={() => setShowModal(false)} />
Eliminate the state in Modal.js, keep visible as always true

How to open a modal in drawer headerRight icon button?

I am new to react native and working on a dummy project. I use the react-navigation to set the header and headerRight and place a icon in the right side of the header. Now i want to open a modal whenever user click the icon it will show a modal. I tried many things but didn't find any proper solution. Now i am stuck and post this question. I am placing my code below.
Here is my Code:
//Navigation of my app
import * as React from "react";
import { View, TouchableOpacity, StyleSheet } from "react-native";
import { AntDesign, Fontisto } from "#expo/vector-icons";
import { createDrawerNavigator } from "#react-navigation/drawer";
import { createStackNavigator } from "#react-navigation/stack";
import AccountsScreen from "../screens/AccountsScreen";
import FavouritesScreen from "../screens/FavouritesScreen";
import HomeScreen from "../screens/HomeScreen";
import SettingsScreen from "../screens/SettingsScreen";
import TrendsScreen from "../screens/TrendsScreen";
import { createMaterialTopTabNavigator } from "#react-navigation/material-top-tabs";
import { Dimensions } from "react-native";
import Income from "../screens/Income";
import Expense from "../screens/Expense";
import FundTransferModal from "../components/Accounts/FundTransferModal";
const AppStack = () => {
return(
<Stack.Navigator mode="modal" headerMode="none">
<Stack.Screen name="Modal" component={FundTransferModal} />
</Stack.Navigator>
)
}
const AppDrawer = () => {
return (
<Drawer.Navigator
initialRouteName="Home"
screenOptions={{
headerShown: true,
}}
>
<Drawer.Screen
name="Home"
component={HomeScreen}
options={{
headerRight: () => {
return (
<TouchableOpacity onPress={() => AppStack() }>
<View style={styles.icons}>
<AntDesign name="piechart" size={30} color="#29416F" />
</View>
</TouchableOpacity>
);
},
}}
/>
<Drawer.Screen name="Accounts" component={AccountsScreen} options={{
headerRight: () => {
return (
<TouchableOpacity>
<View style={styles.icons}>
<Fontisto name="arrow-swap" size={24} color="#29416F" onPress={() =>{return <FundTransferModal />}} />
</View>
</TouchableOpacity>
);
},
}} />
<Drawer.Screen name="Categories" component={CategoriesTabScreens} />
<Drawer.Screen name="Trends" component={TrendsScreen} />
<Drawer.Screen name="Favourites" component={FavouritesScreen} />
<Drawer.Screen name="Settings" component={SettingsScreen} />
</Drawer.Navigator>
);
};
export default AppDrawer;
const styles = StyleSheet.create({
icons:{
marginRight:20,
}
})
//The Modal which i want to open
import React, { Fragment, useState } from "react";
import { globalStyles } from "../../style/Global";
import { AntDesign, Entypo } from "#expo/vector-icons";
import { MaterialCommunityIcons } from "#expo/vector-icons";
import { Formik } from "formik";
import * as yup from "yup";
import { Picker } from "#react-native-picker/picker";
import SubmitButton from "../SubmitButton";
import { ExampleUncontrolledVertical } from "../../assets/ExampleUncontrolledVertical";
import {
Modal,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
Button,
Keyboard,
ScrollView,
} from "react-native";
import DatePiker from "../DatePikers";
export default function FundTransferModal({navigation}) {
const [fundModal, setFundModal] = useState(true);
const [selectedValue, setValue] = useState(0);
showFundModal = () => {
setFundModal(true);
};
closeFundModal = () => {
setFundModal(false);
};
const validations = yup.object({
text: yup.string().required().min(3),
});
const values = ["Cash", "Bank", "Other"];
const valuesTwo = ["Cash", "Bank", "Other"]
const initialValues = { value: "", valueTwo: "" };
return (
<Modal visible={fundModal} animationType="fade">
<View style={globalStyles.modalHeader}>
<AntDesign
name="arrowleft"
size={30}
onPress={() => closeFundModal()}
/>
<Text style={styles.headerText}>Transfer</Text>
</View>
<Formik
validationSchema={validations}
initialValues={{ amount: "" , notes:"" }}
onSubmit={(values, actions) => {
actions.resetForm();
console.log(values);
}}
>
{(props) => (
<Fragment>
<ScrollView>
<View style={styles.container}>
<View style={styles.box}>
<View style={styles.picker}>
<Picker
mode="dropdown"
selectedValue={props.values.value}
onValueChange={(itemValue) => {
props.setFieldValue("value", itemValue);
setValue(itemValue);
}}
>
<Picker.Item
label="Account"
value={initialValues.value}
key={0}
color="#afb8bb"
/>
{values.map((value, index) => (
<Picker.Item label={value} value={value} key={index} />
))}
</Picker>
</View>
<View style={styles.inputValue}>
<TextInput
style={styles.inputName}
placeholder="Value"
keyboardType="numeric"
onChangeText={props.handleChange("amount")}
value={props.values.amount}
onBlur={props.handleBlur("amount")}
/>
</View>
</View>
<View style={styles.doubleArrow}>
<Entypo name="arrow-long-down" size={40} color="#afb8bb" />
<Entypo name="arrow-long-down" size={40} color="#afb8bb" />
</View>
<View style={styles.box}>
<View style={styles.picker}>
<Picker
mode="dropdown"
selectedValue={props.values.valueTwo}
onValueChange={(itemValue) => {
props.setFieldValue("valueTwo", itemValue);
setValue(itemValue);
}}
>
<Picker.Item
label="Account"
value={initialValues.valueTwo}
key={0}
color="#afb8bb"
/>
{valuesTwo.map((value, index) => (
<Picker.Item label={value} value={value} key={index} />
))}
</Picker>
</View>
<View style={styles.Calendar}><DatePiker /></View>
<View style={styles.inputValue}>
<TextInput
style={styles.inputName}
placeholder="Notes"
multiline= {true}
onChangeText={props.handleChange("notes")}
value={props.values.notes}
onBlur={props.handleBlur("notes")}
/>
</View>
</View>
<SubmitButton title="Save" onPress={props.handleSubmit} />
</View>
</ScrollView>
</Fragment>
)}
</Formik>
</Modal>
);
}
const styles = StyleSheet.create({
headerText: {
fontSize: 20,
marginLeft: 5,
},
container: {
flex: 1,
},
box: {
borderColor: "#afb8bb",
margin: 20,
flexDirection: "column",
borderWidth: 1,
borderStyle: "dashed",
borderRadius:5,
marginTop:40,
},
inputName:{
padding:10,
borderWidth:1,
borderColor:"#afb8bb",
},
inputValue:{
margin:10,
marginHorizontal:20,
},
picker:{
borderWidth:1,
borderColor:"#afb8bb",
margin:10,
marginHorizontal:20,
},
doubleArrow:{
flexDirection:'row',
alignContent:'center',
justifyContent:'center',
marginTop:10
},
Calendar: {
marginTop:-20,
borderColor: "#afb8bb",
paddingVertical: 10,
marginHorizontal:20,
fontSize: 20,
textAlign: 'center',
color: 'black',
}
});

React, Formik Field Arrays - mapping over repeatable fields

I'm trying to figure out how to use Formik field arrays in a react project.
I have one form (glossary) that has 3 Field Arrays within it (one for each of relatedTerms, templates and referenceMaterials).
Each of the field arrays is set out in a separate component. When I only used one of them, I had this working. Adding the next one has caused a problem that I can't solve.
My form has:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import {render} from 'react-dom';
import { Link } from 'react-router-dom';
import firebase, {firestore} from '../../../../firebase';
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
Divider,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
Formik, Form, Field, ErrorMessage, FieldArray,
} from 'formik';
import * as Yup from 'yup';
import {
Autocomplete,
ToggleButtonGroup,
AutocompleteRenderInputParams,
} from 'formik-material-ui-lab';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
import RelatedTerms from "./RelatedTerms";
import ReferenceMaterials from "./ReferenceMaterials";
import Templates from "./Templates";
const allCategories = [
{value: 'one', label: 'One'},
{value: 'two', label: 'Two'},
];
function UpperCasingTextField(props: TextFieldProps) {
const {
form: {setFieldValue},
field: {name},
} = props;
const onChange = React.useCallback(
event => {
const {value} = event.target;
setFieldValue(name, value ? value.toUpperCase() : '');
},
[setFieldValue, name]
);
return <MuiTextField {...fieldToTextField(props)} onChange={onChange} />;
}
function Glossary(props) {
const { classes } = props;
const [open, setOpen] = useState(false);
const [isSubmitionCompleted, setSubmitionCompleted] = useState(false);
function handleClose() {
setOpen(false);
}
function handleClickOpen() {
setSubmitionCompleted(false);
setOpen(true);
}
return (
<React.Fragment>
<Button
// component="button"
color="primary"
onClick={handleClickOpen}
style={{ float: "right"}}
variant="outlined"
>
Create Term
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
{!isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Create a defined term</DialogTitle>
<DialogContent>
<DialogContentText>
</DialogContentText>
<Formik
initialValues={{ term: "", definition: "", category: [], context: "", relatedTerms: [], templates: [], referenceMaterials: [] }}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(true);
firestore.collection("glossary").doc().set({
...values,
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(() => {
setSubmitionCompleted(true);
});
}}
validationSchema={Yup.object().shape({
term: Yup.string()
.required('Required'),
definition: Yup.string()
.required('Required'),
category: Yup.string()
.required('Required'),
context: Yup.string()
.required("Required"),
// relatedTerms: Yup.string()
// .required("Required"),
// templates: Yup.string()
// .required("Required"),
// referenceMaterials: Yup.string()
// .required("Required"),
})}
>
{(props) => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset,
} = props;
return (
<form onSubmit={handleSubmit}>
<TextField
label="Term"
name="term"
// className={classes.textField}
value={values.term}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.term && touched.term) && errors.term}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="Meaning"
name="definition"
multiline
rows={4}
// className={classes.textField}
value={values.definition}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<TextField
label="In what context is this term used?"
name="context"
// className={classes.textField}
multiline
rows={4}
value={values.context}
onChange={handleChange}
onBlur={handleBlur}
helperText={(errors.context && touched.context) && errors.context}
margin="normal"
style={{ width: "100%"}}
/>
<Box margin={1}>
<Field
name="category"
multiple
component={Autocomplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutocompleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autocomplete'] && !!errors['autocomplete']}
helperText={touched['autocomplete'] && errors['autocomplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<Divider style={{marginTop: "20px", marginBottom: "20px"}}></Divider>
<Box>
<Typography variant="subtitle2">
Add a related term
</Typography>
<FieldArray name="relatedTerms" component={RelatedTerms} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a reference document
</Typography>
<FieldArray name="referenceMaterials" component={ReferenceMaterials} />
</Box>
<Box>
<Typography variant="subtitle2">
Add a template
</Typography>
<FieldArray name="templates" component={Templates} />
</Box>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleReset}
disabled={!dirty || isSubmitting}
>
Reset
</Button>
<Button type="submit" disabled={isSubmitting}>
Submit
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</form>
);
}}
</Formik>
</DialogContent>
</React.Fragment>
}
{isSubmitionCompleted &&
<React.Fragment>
<DialogTitle id="form-dialog-title">Thanks!</DialogTitle>
<DialogContent>
<DialogContentText>
We appreciate your contribution.
</DialogContentText>
<DialogActions>
<Button
type="button"
className="outline"
onClick={handleClose}
>
Close
</Button>
{/* <DisplayFormikState {...props} /> */}
</DialogActions>
</DialogContent>
</React.Fragment>}
</Dialog>
</React.Fragment>
);
}
export default Glossary;
Then, each subform is as follows (but replacing relatedTerms for templates or referenceMaterials).
import React from "react";
import { Formik, Field } from "formik";
import { withStyles } from '#material-ui/core/styles';
import {
Button,
LinearProgress,
MenuItem,
FormControl,
InputLabel,
FormControlLabel,
TextField,
Typography,
Box,
Grid,
Checkbox,
} from '#material-ui/core';
import MuiTextField from '#material-ui/core/TextField';
import {
fieldToTextField,
TextFieldProps,
Select,
Switch,
} from 'formik-material-ui';
const initialValues = {
title: "",
description: "",
source: ""
};
class RelatedTerms extends React.Component {
render() {
const {form: parentForm, ...parentProps} = this.props;
return (
<Formik
initialValues={initialValues}
render={({ values, setFieldTouched }) => {
return (
<div>
{parentForm.values.relatedTerms.map((_notneeded, index) => {
return (
<div key={index}>
<TextField
label="Title"
name={`relatedTerms.${index}.title`}
placeholder=""
// className="form-control"
// value={values.title}
margin="normal"
style={{ width: "100%"}}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.title`,
e.target.value
);
}}
>
</TextField>
<TextField
label="Description"
name={`relatedTerms.${index}.description`}
placeholder="Describe the relationship"
// value={values.description}
onChange={e => {
parentForm.setFieldValue(
`relatedTerms.${index}.description`,
e.target.value
);
}}
// onBlur={handleBlur}
// helperText={(errors.definition && touched.definition) && errors.definition}
margin="normal"
style={{ width: "100%"}}
/>
<Button
variant="outlined"
color="secondary"
size="small"
onClick={() => parentProps.remove(index)}
>
Remove this term
</Button>
</div>
);
})}
<Button
variant="contained"
color="secondary"
size="small"
style={{ marginTop: "5vh"}}
onClick={() => parentProps.push(initialValues)}
>
Add a related term
</Button>
</div>
);
}}
/>
);
}
}
export default RelatedTerms;
Then when I try to render the data submitted in the form, I have:
import React, { useState, useEffect } from 'react';
import {Link } from 'react-router-dom';
import Typography from '#material-ui/core/Typography';
import ImpactMetricsForm from "./Form";
import firebase, { firestore } from "../../../../firebase.js";
import { makeStyles } from '#material-ui/core/styles';
import clsx from 'clsx';
import ExpansionPanel from '#material-ui/core/ExpansionPanel';
import ExpansionPanelDetails from '#material-ui/core/ExpansionPanelDetails';
import ExpansionPanelSummary from '#material-ui/core/ExpansionPanelSummary';
import ExpansionPanelActions from '#material-ui/core/ExpansionPanelActions';
import ExpandMoreIcon from '#material-ui/icons/ExpandMore';
import Chip from '#material-ui/core/Chip';
import Button from '#material-ui/core/Button';
import Divider from '#material-ui/core/Divider';
const useStyles = makeStyles((theme) => ({
root: {
width: '100%',
marginTop: '8vh',
marginBottom: '5vh'
},
heading: {
fontSize: theme.typography.pxToRem(15),
},
heading2: {
fontSize: theme.typography.pxToRem(15),
fontWeight: "500",
marginTop: '3vh',
marginBottom: '1vh',
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(15),
color: theme.palette.text.secondary,
textTransform: 'capitalize'
},
icon: {
verticalAlign: 'bottom',
height: 20,
width: 20,
},
details: {
alignItems: 'center',
},
column: {
flexBasis: '20%',
},
columnBody: {
flexBasis: '70%',
},
helper: {
borderLeft: `2px solid ${theme.palette.divider}`,
padding: theme.spacing(1, 2),
},
link: {
color: theme.palette.primary.main,
textDecoration: 'none',
'&:hover': {
textDecoration: 'underline',
},
},
}));
const Title = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "4vw",
marginBottom: '2vh'
};
const Subhead = {
fontFamily: "'Montserrat', sans-serif",
fontSize: "calc(2vw + 1vh + .5vmin)",
marginBottom: '2vh',
marginTop: '8vh',
width: "100%"
};
function useGlossaryTerms() {
const [glossaryTerms, setGlossaryTerms] = useState([])
useEffect(() => {
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => ({
id: doc.id,
...doc.data(),
}))
setGlossaryTerms(glossaryTerms)
})
}, [])
return glossaryTerms
}
const GlossaryTerms = () => {
const glossaryTerms = useGlossaryTerms()
const classes = useStyles();
return (
<div style={{ marginLeft: "3vw"}}>
<div className={classes.root}>
{glossaryTerms.map(glossaryTerm => {
return (
<ExpansionPanel defaultcollapsed>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="panel1c-content"
id="panel1c-header"
>
<div className={classes.column}>
<Typography className={classes.heading}>{glossaryTerm.term}</Typography>
</div>
<div className={classes.column}>
{glossaryTerm.category.map(category => (
<Typography className={classes.secondaryHeading}>
{category.label}
</Typography>
)
)}
</div>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.details}>
<div className={clsx(classes.columnBody)}>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Meaning</Typography>
<Typography>{glossaryTerm.definition}</Typography>
</div>
<div>
<Typography variant="subtitle2" className={classes.heading2}>Context</Typography>
<div>
<Typography>{glossaryTerm.context}</Typography>
</div>
<div className={clsx(classes.helper)}>
<div>
<Typography variant="caption">Related Terms</Typography>
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Typography variant="body2" className="blogParagraph" key={relatedTerm.id}>
{relatedTerm.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
<div>
<Typography variant="caption">Related Reference Materials</Typography>
{glossaryTerm.referenceMaterials.map(referenceMaterial => (
<Typography variant="body2" className="blogParagraph" key={referenceMaterial.id}>
{referenceMaterial.title}
</Typography>
))}
</div>
</div>
</ExpansionPanelDetails>
<Divider />
<ExpansionPanelActions>
{glossaryTerm.attribution}
</ExpansionPanelActions>
</ExpansionPanel>
)
})}
</div>
</div>
);
}
export default GlossaryTerms;
When I try this using only the relatedTerms field array, I can submit data in the form and render the list.
When I add in the next two Field Array components for Templates and ReferenceMaterials, I get an error that says:
TypeError: glossaryTerm.referenceMaterials.map is not a function
Each of the 3 field arrays is a duplicate, where I've only changed the name of the value in the main form. You can see from the screen shot attached that the data within each map from the form fields is the same for each of relatedTerms, templates and referenceMaterials. When I comment out templates and referenceMaterials from the rendered output, everything renders properly. When I comment out relatedTerms and try to render either templates or referenceMaterials, I get the error I reported.
If I remove the templates and referenceMaterials map statements from the rendered output, I can use the form with all 3 field arrays in it. They save properly in firebase. I just can't display them using the method that works for relatedTerms.
Everything seems ok with your code. I suspect that the problem is in the data coming from firebase in useGlossaryTerms, some entries in the glossary collection may not have referenceMaterials or templates fields (maybe from a previous form submit that did not have those yet).
You could :
Run a migration script on the collection to add defaults for those fields if they don't exist.
Add defaults on client side :
firebase
.firestore()
.collection("glossary")
.orderBy('term')
.onSnapshot(snapshot => {
const glossaryTerms = snapshot.docs.map(doc => {
const data = doc.data();
return {
id: doc.id,
...data,
referenceMaterials: data.referenceMaterials || [],
templates: data.templates || []
};
}
setGlossaryTerms(glossaryTerms)
})
On the client side, check if those fields exists before rendering :
{
glossaryTerm.templates ? (
<div>
<Typography variant="caption" >Related Templates</Typography>
{glossaryTerm.templates.map(template => (
<Typography variant="body2" className="blogParagraph" key={template.id}>
{template.title}
</Typography>
))}
</div>
) : null
}

Categories

Resources