Firebase: como enviar dados de formulário para coleções diferentes?

Aug 15 2020

Eu tenho um formulário. Um dos campos do formulário é uma Matriz de campo - para campos repetíveis. Além desse campo, todos os outros campos do formulário são armazenados em uma única coleção (a coleção pai).

A coleção principal tem uma matriz para a matriz de campo, que contém os valores de cada entrada repetida, para serem armazenados em uma subcoleção (a subcoleção).

Quando estou escrevendo meu envio do firestore, estou tentando separar os campos a serem enviados para a coleção pai dos campos a serem enviados para a sub-coleção.

Minha tentativa está abaixo.

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

Isso produz um erro que diz:

Linha 120: 22: Esperava-se uma atribuição ou chamada de função e, em vez disso, vi uma expressão no-unused-

Além disso, como posso tornar a referência de documento da coleção pai conhecida no manipulador de envio da sub coleção?

Já vi esse post , que está tentando usar os mesmos dados em 2 coleções (com a mesma preocupação em encontrar o id).

Também vi este blog que mostra como usar "entradas" como referência em uma sub-coleção e parece ter uma maneira de anexá-las a um id de documento - mas o blog não mostra como as entradas são definidas. Não consigo ver como aplicar esse exemplo.

Para referência, o formulário principal, com a matriz de campo de formulário repetível (em um formulário separado) é definido abaixo.

Formulário 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 formulário repetível

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

Quando tento a sugestão apresentada por BrettS abaixo, recebo um aviso do console que diz:

Aviso: um erro não tratado foi detectado em submitForm () FirebaseError: Function DocumentReference.set () chamada com dados inválidos. Valor de campo não suportado: indefinido (encontrado no título do campo)

Já vi esse post que fala sobre como estruturar o objeto a ser usado na tentativa, mas não consigo ver como aplicar essas ideias a esse problema.

Outra tentativa que tentei está descrita abaixo:

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 tento fazer isso, recebo o mesmo tipo de aviso. Diz:

Aviso: um erro não tratado foi detectado em submitForm () FirebaseError: Função WriteBatch.set () chamada com dados inválidos. Valor de campo não suportado: indefinido (encontrado no título do campo)

Recebo outro erro com este formato de referência de divisão, que diz:

Aviso: cada criança em uma lista deve ter uma prop "chave" exclusiva.

Acho que isso deve ter algo a ver com a nova estrutura das referências - mas não vejo como lidar com isso.

PRÓXIMA TENTATIVA

Quando tento a resposta sugerida revisada de Brett, tenho:

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

Observe, eu comentei tudo menos o atributo title no documento relatedTerms para que eu pudesse ver se isso funciona.

Não é verdade. o formulário ainda é renderizado e quando tento pressionar enviar, ele simplesmente trava. Nenhuma mensagem de erro é gerada no console, mas ele gera uma mensagem de aviso que diz:

0.chunk.js: 141417 Aviso: um erro não tratado foi capturado em submitForm () FirebaseError: Função WriteBatch.set () chamada com dados inválidos. Valor de campo não suportado: indefinido (encontrado no título do campo)

Quando eu google isso - parece partir deste posto que talvez haja um problema com a forma como o ID de doc do pai é definido na coleção relatedTerm.

Também estou me perguntando se os valores iniciais talvez precisem ser definidos e inicializados separadamente para cada coleção.

Quando tento o console registrar os valores das entradas do formulário, posso ver que um objeto com um valor de título foi capturado. Os valores iniciais do formulário incluem uma matriz chamada relatedTerms (valor inicial: []).

Talvez eu precise fazer algo para converter esse array nos valores que vão nele antes de tentar enviar para o firestore. Como eu faria isso?

O post que eu vinculei divide isso em 2 etapas, mas sou muito lento para descobrir o que eles estão fazendo ou como fazer sozinho. É estranho, porém, que esse problema não surja quando eu não tento dividir os valores do formulário entre as coleções do firestore - se eu apenas usar um único documento, o que quer que aconteça aqui está sendo feito por padrão.

Não tenho certeza se o que estou tentando fazer é o que os documentos do firestore estão descrevendo na seção de objetos personalizados . Noto que o exemplo de adição de dados acima mostra a adição de uma matriz sem quaisquer etapas executadas para converter os itens na matriz para o tipo de dados antes de enviar. Não tenho certeza se esta é a linha certa de investigação, visto que o envio funciona bem se eu não tentar dividir os dados entre as coleções.

PRÓXIMA TENTATIVA

A resposta de Andreas neste post é simples o suficiente para eu entender. O operador de propagação trabalha onde é usado no método de envio para as entradas relacionadas aos Termos.

No entanto, isso lança o próximo desafio - que é como ler os dados da subcoleção. Esta parte da documentação do firebase é desconcertante para mim. Eu não consigo entender isso.

Diz:

A recuperação de uma lista de coleções não é possível com as bibliotecas de cliente móvel / web.

Isso significa que não consigo ler os valores na tabela relatedTerms?

Anteriormente, eu era capaz de ler a matriz de dados relatedTerms da seguinte maneira:

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
  }

então:

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

relatedTerms é agora uma subcoleção na coleção de glossários em vez de uma matriz na coleção de glossários. Eu entendo por esta postagem que tenho que consultar as coleções separadamente.

Portanto, a primeira consulta é como obter newDocRef.id para salvar como um atributo no documento relatedTerms. Tentei adicionar um atributo ao envio para ele.

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

Embora não tenha gerado nenhum erro quando tento enviar o formulário, também não criou uma entrada no documento de Termos relacionados chamado glossaryId. O log de valores também não inclui.

Eu vi este post e a resposta de Jim. Não entendo como usar meu glossaryTerm.id como o id de documento em um useEffect separado para localizar os termos relacionados.

Respostas

6 DougStevenson Aug 15 2020 at 12:24

Cada vez que você ligar doc(), você vai gerar uma referência para um novo documento gerado aleatoriamente. Isso significa que sua primeira chamada para firestore.collection("glossary").doc()gerará uma nova ID, bem como a chamada subsequente. Se quiser reutilizar uma referência de documento, você terá que armazená-la em uma variável.

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

Use essa mesma variável mais tarde:

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

Eu não tenho carma suficiente ou qualquer coisa para comentar, então estou colocando meu comentário aqui.

Esta é uma maneira de implementar a solução de Doug com seu código. Desculpe em avanço por quaisquer erros de sintaxe - eu não testei a execução deste código.

Você pode passar ids de documentos antes da execução, mesmo que o autoID seja gerado quando o envio é feito.

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