Firebase: comment soumettre des données de formulaire à différentes collections?

Aug 15 2020

J'ai un formulaire. L'un des champs du formulaire est un tableau de champs - pour les champs répétables. En dehors de ce champ, tous les autres champs du formulaire sont stockés dans une seule collection (la collection parent).

La collection parent a un tableau pour le tableau de champs, qui contient les valeurs de chaque entrée répétée, à stocker dans une sous-collection (la sous-collection).

Lorsque j'écris ma soumission Firestore, j'essaie de séparer les champs à soumettre à la collection parent des champs à soumettre à la sous-collection.

Ma tentative est ci-dessous.

<Formik
                  initialValues={{ term: "",    category: [],  relatedTerms: [],  }}
                  
                  onSubmit={(values, { setSubmitting }) => {
                     setSubmitting(true);
                     firestore.collection("glossary").doc().set({
                      term: values.term,
                      category: values.category,
                      createdAt: firebase.firestore.FieldValue.serverTimestamp()
                      }),
                      firestore.collection("glossary").doc().collection('relatedTerms').doc().set({
                        dataType: values.dataType,
                        title: values.Title,
                        description: values.description,
                        
                      })
                    .then(() => {
                      setSubmitionCompleted(true);
                    });
                  }}
  

Cela produit une erreur qui dit:

Ligne 120: 22: Attendu une affectation ou un appel de fonction et à la place vu une expression non-inutilisée-

De plus, comment puis-je faire connaître la référence doc de la collection parent dans le gestionnaire d'envoi de la sous-collection?

J'ai vu ce post , qui tente d'utiliser les mêmes données dans 2 collections (avec le même souci de trouver l'identifiant).

J'ai aussi vu ce blog qui montre comment utiliser les "inputs" comme référence dans une sous-collection et semble avoir un moyen de les attacher à un doc id - mais le blog ne montre pas comment les entrées sont définies. Je ne vois pas comment appliquer cet exemple.

Pour référence, le formulaire principal, avec le tableau de champs de formulaire répétable (sous un formulaire séparé) est présenté ci-dessous.

Forme principale

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,
  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 "./Form2";

const allCategories = [
    {value: 'one', label: 'I'},
    {value: 'two', label: 'C'},
    
];


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>
                  Your contribution to the research community is appreciated. 
                </DialogContentText>
                <Formik
                  initialValues={{ term: "",  definition: "",  category: [],   context: "", relatedTerms: []  }}
                  
                  onSubmit={(values, { setSubmitting }) => {
                     setSubmitting(true);
                     firestore.collection("glossary").doc().set({
                      term: values.term,
                      definition: values.definition,
                      category: values.category,
                      context: values.context,
                      createdAt: firebase.firestore.FieldValue.serverTimestamp()
                      }),
                      firestore.collection("glossary").doc().collection('relatedTerms').doc().set({
                        dataType: values.dataType,
                        title: values.title,
                        description: values.description,
                        
                      })
                    .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"),
                      
  
                  })}
                >
                  {(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>     
                        
                        <FieldArray name="relatedTerms" component={RelatedTerms} />
                        <Button type="submit">Submit</Button>
                        
                        <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>
                  
                </DialogContentText>
                <DialogActions>
                  <Button
                    type="button"
                    className="outline"
                    onClick={handleClose}
                  >
                    Close
                    </Button>
                  {/* <DisplayFormikState {...props} /> */}
                </DialogActions>
              </DialogContent>
            </React.Fragment>}
        </Dialog>
      </React.Fragment>
    );
  }



export default Glossary;

Matrice de champs pour champ de formulaire répétable

import React from "react";
import { Formik, Field } from "formik";
import Button from '@material-ui/core/Button';

const initialValues = {
  dataType: "",
  title: "",
  description: "",
  
};

const dataTypes = [
  { value: "primary", label: "Primary (raw) data" },
  { value: "secondary", label: "Secondary data" },
 ];

class DataRequests 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}>
                    
                            <div className="form-group">
                              <label htmlFor="relatedTermsTitle">Title</label>
                              <Field
                                name={`relatedTerms.${index}.title`} placeholder="Add a title" className="form-control" onChange={e => { parentForm.setFieldValue( `relatedTerms.${index}.title`,
                                    e.target.value
                                  );
                                }}
                              ></Field>
                            </div>
                          
                            <div className="form-group">
                              <label htmlFor="relatedTermsDescription">
                                Description
                              </label>
                              <Field
                                name={`relatedTerms.${index}.description`} component="textarea" rows="10" placeholder="Describe use" className="form-control" onChange={e => { parentForm.setFieldValue( `relatedTerms.${index}.description`,
                                    e.target.value
                                  );
                                }}
                              ></Field>
                            </div>
                          
                            
                            
                          <Button
                            
                            onClick={() => parentProps.remove(index)}
                          >
                            Remove
                          </Button>
                        
                  </div>
                );
              })}
              <Button
                variant="primary"
                size="sm"
                onClick={() => parentProps.push(initialValues)}
              >
                Add another
              </Button>
            </div>
          );
        }}
      />
    );
  }
}

export default DataRequests;

PROCHAINE ATTMEPT

Lorsque j'essaie la suggestion présentée par BrettS ci-dessous, je reçois un avertissement de console qui dit:

Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Function DocumentReference.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)

J'ai vu cet article qui parle de structurer l'objet à utiliser dans la tentative, mais je ne vois pas comment appliquer ces idées à ce problème.

Une autre tentative que j'ai essayée est présentée ci-dessous:

onSubmit={(values, { setSubmitting }) => {
                     setSubmitting(true);

                    //   const newGlossaryDocRef = firestore.collection("glossary").doc(); 
                    //   newGlossaryDocRef.set({
                    //     term: values.term,
                    //     definition: values.definition,
                    //     category: values.category,
                    //     context: values.context,
                    //     createdAt: firebase.firestore.FieldValue.serverTimestamp()
                    //     });
                    //   newGlossaryDocRef.collection('relatedTerms').doc().set({
                    // //     dataType: values.dataType,
                    //       title: values.title,
                    // //     description: values.description,
                        
                    //    })

                    const glossaryDoc = firestore.collection('glossary').doc()
                      
                    const relatedTermDoc = firestore
                      .collection('glossary')
                      .doc(glossaryDoc.id) // <- we use the id from docRefA
                      .collection('relatedTerms')
                      .doc()
                      

                    var writeBatch = firestore.batch();

                    writeBatch.set(glossaryDoc, {
                      term: values.term,
                      category: values.category,
                      createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                    });

                    writeBatch.set(relatedTermDoc, {
                      // dataType: values.dataType,
                      title: values.Title,
                      // description: values.description,
                    });

                    writeBatch.commit().then(() => {
                      // All done, everything is in Firestore.
                    })
                    .catch(() => {
                      // Something went wrong.
                      // Using firestore.batch(), we know no data was written if we get here.
                    })
                    .then(() => {
                      setSubmitionCompleted(true);
                    });
                    
                  }}
  

Quand j'essaye ceci, j'obtiens le même genre d'avertissement. Ça dit:

Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Function WriteBatch.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)

J'obtiens une autre erreur avec ce format de référence fractionné, qui dit:

Attention: chaque enfant d'une liste doit avoir un accessoire "clé" unique.

Je pense que cela doit être lié à la nouvelle structure des références - mais je ne vois pas comment y remédier.

PROCHAINE TENTATIVE

Lorsque j'essaye la réponse suggérée révisée de Brett, j'ai:

            onSubmit={(values, { setSubmitting }) => {
                 setSubmitting(true);
                 
                //  firestore.collection("glossary").doc().set({
                //   ...values,
                //   createdAt: firebase.firestore.FieldValue.serverTimestamp()
                //   })
                // .then(() => {
                //   setSubmitionCompleted(true);
                // });
              // }}
              const newDocRef = firestore.collection("glossary").doc() 

// auto generated doc id saved here
  let writeBatch = firestore.batch();
  writeBatch.set(newDocRef,{
    term: values.term,
    definition: values.definition,
    category: values.category,
    context: values.context,
    createdAt: firebase.firestore.FieldValue.serverTimestamp()
  });
  writeBatch.set(newDocRef.collection('relatedTerms').doc(),{
    // dataType: values.dataType,
    title: values.title,
    // description: values.description,
  })
  writeBatch.commit()
    .then(() => {
      setSubmitionCompleted(true);
    });
}}

Remarque, j'ai tout commenté sauf l'attribut title sur le document relatedTerms afin que je puisse voir si cela fonctionne du tout.

Ce n'est pas le cas. le formulaire est toujours affiché et lorsque j'essaie d'appuyer sur Soumettre, il se bloque. Aucun message d'erreur n'est généré dans la console, mais il génère un message d'avertissement qui dit:

0.chunk.js: 141417 Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Fonction WriteBatch.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)

Lorsque je cherche sur Google, il semble d' après ce post qu'il y a peut-être un problème avec la façon dont l'identifiant de document du parent est défini dans la collection relatedTerm.

Je me demande également si les valeurs initiales doivent peut-être être définies et initialisées séparément pour chaque collection?

Lorsque j'essaie de consigner par console les valeurs des entrées de formulaire, je peux voir qu'un objet avec une valeur de titre est capturé. Les valeurs initiales du formulaire incluent un tableau appelé relatedTerms (valeur initiale: []).

Peut-être que j'ai besoin de faire quelque chose pour convertir ce tableau dans les valeurs qui y entrent avant d'essayer d'envoyer cela à Firestore. Comment pourrais-je faire ça?

Le message que j'ai lié le divise en 2 étapes, mais je suis trop lent pour comprendre ce qu'ils font ou comment les faire moi-même. Il est cependant étrange que ce problème ne se pose pas lorsque je n'essaye pas de diviser les valeurs de formulaire entre les collections firestore - si j'utilise juste un seul document, alors tout ce qui doit se passer ici est fait par défaut.

Je ne sais pas si ce que j'essaie de faire est ce que la documentation de Firestore décrit dans la section des objets personnalisés . Je note que l'exemple d'ajout de données ci-dessus montre l'ajout d'un tableau sans aucune mesure prise pour convertir les éléments du tableau en type de données avant de les soumettre. Je ne suis pas sûr que ce soit la bonne ligne d'enquête étant donné que la soumission fonctionne bien si je n'essaye pas de diviser les données entre les collections.

PROCHAINE TENTATIVE

La réponse d'Andreas sur ce post est assez simple pour que je la comprenne. L'opérateur de diffusion fonctionne là où il est utilisé dans la méthode de soumission pour les entrées liées aux termes.

Cependant, cela pose le prochain défi - qui est de savoir comment lire les données de la sous-collection. Cette partie de la documentation de Firebase me déroute . Je ne peux pas y comprendre.

Ça dit:

La récupération d'une liste de collections n'est pas possible avec les bibliothèques client mobile / Web.

Cela signifie-t-il que je ne peux pas lire les valeurs de la table relatedTerms?

Auparavant, je pouvais lire le tableau de données relatedTerms comme suit:

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
  }

puis:

{glossaryTerm.relatedTerms.map(relatedTerm => (
                                
                                <Link to="" className="bodylinks" key={relatedTerm.id}>
                                 {relatedTerm.title}
                          </Link>                                   ))}

relatedTerms est désormais une sous-collection dans la collection de glossaires au lieu d'un tableau dans la collection de glossaires. Je comprends de ce post que je dois interroger les collections séparément.

La première question est donc de savoir comment obtenir newDocRef.id à enregistrer en tant qu'attribut dans le document relatedTerms. J'ai essayé d'ajouter un attribut à la soumission pour cela.

glossaryId: newDocRef.id,
    ...values.relatedTerms

Bien qu'il n'ait généré aucune erreur lorsque j'essaie de soumettre le formulaire, il n'a pas non plus créé d'entrée dans le document relatedTerms appelé glossaryId. Le journal des valeurs ne l'inclut pas non plus.

J'ai vu ce post et la réponse de Jim. Je ne comprends pas comment utiliser mon glossaryTerm.id comme doc id dans un useEffect séparé pour trouver les relatedTerms.

Réponses

6 DougStevenson Aug 15 2020 at 12:24

Chaque fois que vous appelez doc(), vous allez générer une référence à un nouveau document généré aléatoirement. Cela signifie que votre premier appel firestore.collection("glossary").doc()générera un nouvel identifiant, ainsi que l'appel suivant. Si vous souhaitez réutiliser une référence de document, vous allez devoir la stocker dans une variable.

const firstDocRef = firestore.collection("glossary").doc()
firstDocRef.set(...)

L'utilisation de cette même variable plus tard:

const secondDocRef = firstDocRef.collection('relatedTerms').doc()
secondDocRef.set(...)
3 BrettS Aug 24 2020 at 08:34

Je n'ai pas assez de karma ou quoi que ce soit pour commenter, alors je mets mon commentaire ici.

Voici une façon d'implémenter la solution de Doug avec votre code. Désolé à l'avance pour les erreurs de syntaxe - je n'ai pas testé l'exécution de ce code.

Vous pouvez transmettre les identifiants de document avant l'exécution même si l'autoID est généré lors de la soumission.

onSubmit={(values, { setSubmitting }) => {
  setSubmitting(true);
  const newDocRef = firestore.collection("glossary").doc() // auto generated doc id saved here
  let writeBatch = firestore.batch();
  writeBatch.set(newDocRef,{
    term: values.term,
    definition: values.definition,
    category: values.category,
    context: values.context,
    createdAt: firebase.firestore.FieldValue.serverTimestamp()
  }),
  writeBatch.set(newDocRef.collection('relatedTerms').doc(),{
    dataType: values.dataType,
    title: values.title,
    description: values.description,
  })
  writeBatch.commit()
    .then(() => {
      setSubmitionCompleted(true);
    });
}}