问题描述
我试图弄清楚如何在React项目中使用Formik字段数组。
我有一个表单(词汇表),其中包含3个字段数组(每个相关术语,模板和referenceMaterials都一个)。
每个字段数组都在单独的组件中列出。当我仅使用其中一个时,便可以正常工作了。添加下一个导致了我无法解决的问题。
我的表单具有:
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;
然后,每个子表单如下(但将模板或referenceMaterials的relatedTerm替换)。
import React from "react";
import { Formik,Field } from "formik";
import { withStyles } from '@material-ui/core/styles';
import {
Button,} from '@material-ui/core';
import MuiTextField from '@material-ui/core/TextField';
import {
fieldToTextField,} 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;
然后,当我尝试呈现以表单提交的数据时,我有:
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',fontSize: "calc(2vw + 1vh + .5vmin)",marginBottom: '2vh',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="panel1@R_164_4049@"
>
<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;
当我仅使用relatedTerms字段数组进行尝试时,我可以在表单中提交数据并呈现列表。
当我为模板和ReferenceMaterials添加下两个字段数组组件时,出现错误消息:
TypeError:glossaryTerm.referenceMaterials.map不是函数
3个字段数组中的每一个都是重复的,在这里我只更改了主窗体中值的名称。您可以从所附的屏幕截图中看到,表单域中每个地图中的数据对于所有相关术语,模板和referenceMaterials都是相同的。当我从渲染的输出中注释掉模板和referenceMaterials时,一切都将正确渲染。当我注释掉relatedTerms并尝试呈现模板或referenceMaterials时,出现了我报告的错误。
如果我从呈现的输出中删除模板和referenceMaterials映射语句,则可以使用包含所有3个字段数组的表单。它们可以正确保存在Firebase中。我只是无法使用适用于relatedTerms的方法来显示它们。
解决方法
您的代码一切正常。我怀疑问题出在来自useGlossaryTerms
的firebase的数据中,glossary
集合中的某些条目可能没有referenceMaterials
或templates
字段(可能来自以前的表单)提交还没有的文件。
您可以:
- 在集合上运行迁移脚本,以为这些字段添加默认值(如果不存在)。
- 在客户端添加默认值:
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)
})
- 在客户端,在渲染之前检查这些字段是否存在:
{
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
}