Firebase: ¿Cómo enviar datos de formularios a diferentes colecciones?

Aug 15 2020

Tengo una forma. Uno de los campos del formulario es una matriz de campos, para campos repetibles. Aparte de este campo, todos los demás campos del formulario se almacenan en una sola colección (la colección principal).

La colección principal tiene una matriz para la matriz de campo, que contiene los valores de cada entrada repetida, para ser almacenados en una subcolección (la subcolección).

Cuando estoy escribiendo mi envío de firestore, intento separar los campos que se enviarán a la Colección principal de los campos que se enviarán a la Subcolección.

Mi intento está abajo.

<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);
                    });
                  }}
  

Esto produce un error que dice:

Línea 120: 22: Se esperaba una asignación o llamada de función y en su lugar vio una expresión no-sin usar-

Además, ¿cómo puedo dar a conocer la referencia de documento de la colección principal en el controlador de envío de la subcolección?

He visto esta publicación , que intenta usar los mismos datos en 2 colecciones (con la misma preocupación por encontrar la identificación).

También he visto este blog que muestra cómo usar "entradas" como referencia en una subcolección y parece tener una forma de adjuntarlas a una identificación de documento, pero el blog no muestra cómo se definen las entradas. No veo cómo aplicar ese ejemplo.

Como referencia, el formulario principal, con la matriz de campos del formulario repetible (en un formulario separado) se establece a continuación.

Forma principal

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;

Matriz de campo para campo de formulario repetible

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;

PRÓXIMO ATTMEPT

Cuando pruebo la sugerencia establecida por BrettS a continuación, recibo una advertencia de consola que dice:

Advertencia: se detectó un error no controlado de submitForm () FirebaseError: se llamó a la función DocumentReference.set () con datos no válidos. Valor de campo no admitido: indefinido (se encuentra en el título del campo)

He visto esta publicación que habla sobre la estructuración del objeto a usar en el intento, pero no veo cómo aplicar esas ideas a este problema.

Otro intento que he probado se establece a continuación:

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);
                    });
                    
                  }}
  

Cuando intento esto, recibo el mismo tipo de advertencia. Dice:

Advertencia: se detectó un error no controlado de submitForm () FirebaseError: se llamó a la función WriteBatch.set () con datos no válidos. Valor de campo no admitido: indefinido (se encuentra en el título del campo)

Recibo otro error con este formato de referencia dividido, que dice:

Advertencia: Cada niño en una lista debe tener un accesorio "clave" único.

Creo que debe tener algo que ver con la nueva estructura de las referencias, pero no veo cómo abordarlo.

PRÓXIMO INTENTO

Cuando pruebo la respuesta sugerida revisada de Brett, tengo:

            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);
    });
}}

Tenga en cuenta que comenté todo menos el atributo de título en el documento relatedTerms para poder ver si esto funciona.

No es así. el formulario aún se muestra y cuando intento presionar enviar, simplemente se cuelga. No se generan mensajes de error en la consola, pero genera un mensaje de advertencia que dice:

0.chunk.js: 141417 Advertencia: Se detectó un error no controlado de submitForm () FirebaseError: Se llamó a la función WriteBatch.set () con datos no válidos. Valor de campo no admitido: indefinido (se encuentra en el título del campo)

Cuando busco esto en Google, parece en esta publicación que tal vez haya un problema con la forma en que se define el ID de documento del padre en la colección relatedTerm.

También me pregunto si los valores iniciales tal vez deban definirse e inicializarse por separado para cada colección.

Cuando intento registrar en la consola los valores de las entradas del formulario, puedo ver que se captura un objeto con un valor de título. Los valores iniciales del formulario incluyen una matriz llamada relatedTerms (valor inicial: []).

Tal vez necesite hacer algo para convertir esa matriz en los valores que contiene antes de intentar enviar esto a firestore. ¿Como podría hacerlo?

La publicación que vinculé divide esto en 2 pasos, pero soy demasiado lento para descubrir qué están haciendo o cómo hacerlo yo mismo. Sin embargo, es extraño que este problema no surja cuando no trato de dividir los valores del formulario entre las colecciones de Firestore; si solo uso un solo documento, entonces lo que sea necesario que suceda aquí se hace de forma predeterminada.

No estoy seguro de si lo que estoy tratando de hacer es lo que describen los documentos de Firestore en la sección de objetos personalizados . Observo que el ejemplo de adición de datos anterior muestra la adición de una matriz sin que se hayan realizado pasos para convertir los elementos de la matriz al tipo de datos antes de enviarlos. No estoy seguro de si esta es la línea de consulta correcta dado que el envío funciona bien si no intento dividir los datos entre colecciones.

PRÓXIMO INTENTO

La respuesta de Andreas en esta publicación es lo suficientemente simple para que la entienda. El operador de propagación funciona donde se utiliza en el método de envío para las entradas de términos relacionados.

Sin embargo, eso presenta el siguiente desafío, que es cómo leer los datos de la subcolección. Esta parte de la documentación de la base de fuego me desconcierta. No puedo encontrarle sentido.

Dice:

No es posible recuperar una lista de colecciones con las bibliotecas cliente web / móvil.

¿Significa que no puedo leer los valores en la tabla relatedTerms?

Anteriormente, pude leer la matriz de datos de relatedTerms de la siguiente manera:

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
  }

luego:

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

relatedTerms es ahora una subcolección en la colección del glosario en lugar de una matriz en la colección del glosario. Entiendo por esta publicación que tengo que consultar las colecciones por separado.

Entonces, la primera consulta es cómo obtener newDocRef.id para guardar como un atributo en el documento relatedTerms. Intenté agregar un atributo al envío.

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

Si bien no generó ningún error cuando intenté enviar el formulario, tampoco creó una entrada en el documento de Términos relacionados llamado glossaryId. El logaritmo de valores tampoco lo incluye.

He visto esta publicación y la respuesta de Jim. No entiendo cómo usar mi glossaryTerm.id como ID de documento en un useEffect separado para encontrar los términos relacionados.

Respuestas

6 DougStevenson Aug 15 2020 at 12:24

Cada vez que llame doc(), generará una referencia a un nuevo documento generado aleatoriamente. Eso significa que su primera llamada a firestore.collection("glossary").doc()generará una nueva identificación, así como la siguiente llamada. Si desea reutilizar una referencia de documento, tendrá que almacenarla en una variable.

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

El uso de esa misma variable más tarde:

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

No tengo suficiente karma o lo que sea para comentar, así que pongo mi comentario aquí.

Esta es una forma de implementar la solución de Doug con su código. Lo siento de antemano por cualquier error de sintaxis; no probé la ejecución de este código.

Puede pasar identificadores de documentos antes de la ejecución, aunque el autoID se genera cuando se realiza el envío.

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);
    });
}}