首页 > 解决方案 > React,Formik 字段数组 - 映射可重复字段

问题描述

我试图弄清楚如何在反应项目中使用 Formik 字段数组。

我有一个表单(词汇表),其中包含 3 个字段数组(每个相关术语、模板和参考材料一个)。

每个字段数组都设置在一个单独的组件中。当我只使用其中一个时,我就有了这个工作。添加下一个导致了我无法解决的问题。

我的表格有:

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;

然后,每个子表单如下(但将relatedTerms 替换为templates 或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;

然后当我尝试渲染表单中提交的数据时,我有:

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;

  

当我只使用relatedTerms 字段数组尝试此操作时,我可以在表单中提交数据并呈现列表。

当我为模板和参考材料添加接下来的两个字段数组组件时,我收到一条错误消息:

类型错误:glossaryTerm.referenceMaterials.map 不是函数

3 个字段数组中的每一个都是重复的,我只更改了主窗体中值的名称。您可以从随附的屏幕截图中看到,表单字段中的每个地图中的数据对于每个相关术语、模板和参考材料都是相同的。当我从渲染输出中注释掉模板和 referenceMaterials 时,一切都会正确渲染。当我注释掉 relatedTerms 并尝试呈现模板或 referenceMaterials 时,我收到了我报告的错误。

在此处输入图像描述

如果我从呈现的输出中删除模板和 referenceMaterials 映射语句,我可以使用包含所有 3 个字段数组的表单。他们在firebase中正确保存。我只是无法使用适用于相关术语的方法来显示它们。

标签: javascriptarraysreactjsformik

解决方案


您的代码似乎一切正常。我怀疑问题出在来自 firebase in 的数据中useGlossaryTerms,集合中的某些条目glossary可能没有referenceMaterialstemplates字段(可能来自以前的表单提交,但还没有这些)。

你可以 :

  1. 如果这些字段不存在,则在集合上运行迁移脚本以添加这些字段的默认值。
  2. 在客户端添加默认值:

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)
})
  1. 在客户端,在渲染之前检查这些字段是否存在:

{
  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
}

推荐阅读