Firebase: come inviare i dati dei moduli a diverse raccolte?

Aug 15 2020

Ho un modulo. Uno dei campi nel modulo è un array di campi, per i campi ripetibili. Oltre a questo campo, tutti gli altri campi del modulo sono archiviati in una singola raccolta (la raccolta padre).

La Parent Collection ha un array per il Field Array, che contiene i valori di ogni voce ripetuta, da memorizzare in una sotto-raccolta (la Sub Collection).

Quando scrivo il mio invio firestore, sto cercando di separare i campi da inviare alla raccolta padre, dai campi da inviare alla raccolta secondaria.

Il mio tentativo è di seguito.

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

Questo produce un errore che dice:

Riga 120:22: Previsto un compito o una chiamata di funzione e invece ha visto un'espressione no-unused-

Inoltre, come posso rendere noto il riferimento al documento della raccolta padre nel gestore di invio per la raccolta secondaria?

Ho visto questo post , che sta cercando di utilizzare gli stessi dati in 2 raccolte (con la stessa preoccupazione per trovare l'id).

Ho anche visto questo blog che mostra come utilizzare gli "input" come riferimento in una sotto-raccolta e sembra avere un modo per collegarli a un ID documento, ma il blog non mostra come vengono definiti gli input. Non riesco a vedere come applicare quell'esempio.

Per riferimento, il modulo principale, con la matrice di campi modulo ripetibile (in una forma separata) è illustrato di seguito.

Modulo 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;

Field Array per campo modulo ripetibile

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;

PROSSIMO ATTMEPT

Quando provo il suggerimento indicato da BrettS di seguito, ricevo un avviso della console che dice:

Avviso: un errore non gestito è stato rilevato da submitForm () FirebaseError: funzione DocumentReference.set () chiamata con dati non validi. Valore del campo non supportato: undefined (trovato nel titolo del campo)

Ho visto questo post che parla della strutturazione dell'oggetto da utilizzare nel tentativo, ma non riesco a vedere come applicare queste idee a questo problema.

Un altro tentativo che ho provato è illustrato di seguito:

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

Quando lo provo, ricevo lo stesso tipo di avviso. Dice:

Avviso: un errore non gestito è stato rilevato da submitForm () FirebaseError: funzione WriteBatch.set () chiamata con dati non validi. Valore del campo non supportato: undefined (trovato nel titolo del campo)

Ottengo un altro errore con questo formato di riferimento diviso, che dice:

Avvertenza: ogni bambino in un elenco dovrebbe avere un puntello "chiave" unico.

Penso che debba essere qualcosa a che fare con la nuova struttura dei riferimenti, ma non riesco a vedere come affrontarlo.

PROSSIMO TENTATIVO

Quando provo la risposta suggerita rivista di Brett, ho:

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

Nota, ho commentato tutto tranne l'attributo title nel documento relatedTerms in modo da poter vedere se funziona.

Non è così. il modulo viene ancora visualizzato e quando provo a premere invio, si blocca. Nessun messaggio di errore viene generato nella console, ma genera un messaggio di avviso che dice:

0.chunk.js: 141417 Avviso: è stato rilevato un errore non gestito da submitForm () FirebaseError: funzione WriteBatch.set () chiamata con dati non validi. Valore del campo non supportato: undefined (trovato nel titolo del campo)

Quando lo cerco su Google, sembra da questo post che forse c'è un problema con il modo in cui l'ID documento del genitore è definito nella raccolta relatedTerm.

Mi chiedo anche se i valori iniziali debbano forse essere definiti e inizializzati separatamente per ogni raccolta?

Quando provo la console a registrare i valori delle voci del modulo, posso vedere che viene catturato un oggetto con un valore di titolo. I valori iniziali per il modulo includono un array chiamato relatedTerms (valore iniziale: []).

Forse devo fare qualcosa per convertire quell'array nei valori che lo contengono prima di provare a inviarlo a firestore. Come potrei farlo?

Il post che ho collegato lo divide in 2 passaggi, ma sono troppo lento per capire cosa stanno facendo o come farli da solo. È strano però che questo problema non si verifichi quando non provo a dividere i valori del modulo tra le raccolte firestore: se uso solo un singolo documento, tutto ciò che deve accadere qui viene fatto per impostazione predefinita.

Non sono sicuro che quello che sto cercando di fare sia ciò che i documenti di Firestore stanno descrivendo nella sezione degli oggetti personalizzati . Noto che l'esempio di aggiunta di dati sopra mostra l'aggiunta di un array senza alcuna procedura eseguita per convertire gli elementi nell'array nel tipo di dati prima dell'invio. Non sono sicuro che questa sia la linea di indagine giusta dato che l'invio funziona bene se non provo a dividere i dati tra le raccolte.

PROSSIMO TENTATIVO

La risposta di Andreas su questo post è abbastanza semplice da capire. L'operatore spread funziona dove viene utilizzato nel metodo di invio per le voci relative ai termini.

Tuttavia, questo solleva la prossima sfida: come leggere i dati della raccolta secondaria. Questa parte della documentazione di Firebase mi lascia perplesso . Non riesco a capirlo.

Dice:

Il recupero di un elenco di raccolte non è possibile con le librerie client mobile / web.

Significa che non posso leggere i valori nella tabella relatedTerms?

In precedenza, ero in grado di leggere l'array di dati relatedTerms come segue:

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
  }

poi:

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

relatedTerms è ora una sotto-raccolta nella raccolta del glossario invece di un array nella raccolta del glossario. Capisco da questo post che devo interrogare le raccolte separatamente.

Quindi la prima domanda è come ottenere newDocRef.id da salvare come attributo nel documento relatedTerms. Ho provato ad aggiungere un attributo all'invio per questo.

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

Sebbene non abbia generato errori quando ho provato a inviare il modulo, non ha nemmeno creato una voce nel documento relativo ai Termini chiamato glossaryId. Anche il registro dei valori non lo include.

Ho visto questo post e la risposta di Jim. Non capisco come utilizzare il mio glossaryTerm.id come ID documento in un useEffect separato per trovare i relativi Termini.

Risposte

6 DougStevenson Aug 15 2020 at 12:24

Ogni volta che chiami doc(), genererai un riferimento a un nuovo documento generato in modo casuale. Ciò significa che la tua prima chiamata a firestore.collection("glossary").doc()genererà un nuovo ID, così come la chiamata successiva. Se vuoi riutilizzare un riferimento a un documento, dovrai memorizzarlo in una variabile.

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

L'utilizzo della stessa variabile in seguito:

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

Non ho abbastanza karma o altro da commentare, quindi metto il mio commento qui.

Ecco un modo per implementare la soluzione di Doug con il tuo codice. Ci scusiamo in anticipo per eventuali errori di sintassi - Non ho eseguito il test di questo codice.

Puoi passare gli ID documento prima dell'esecuzione anche se l'autoID viene generato quando viene effettuato l'invio.

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