Firebase: как отправлять данные формы в разные коллекции?

Aug 15 2020

У меня одна форма. Одно из полей в форме - массив полей - для повторяющихся полей. Помимо этого поля, все остальные поля формы хранятся в единой коллекции (родительской коллекции).

В родительской коллекции есть массив для массива полей, в котором хранятся значения каждой повторяющейся записи, которые должны быть сохранены в под-коллекции (Подколлекция).

Когда я пишу свою отправку firestore, я пытаюсь отделить поля, которые будут отправлены в родительскую коллекцию, от полей, которые будут отправлены во вспомогательную коллекцию.

Моя попытка ниже.

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

Это вызывает ошибку, которая говорит:

Строка 120: 22: ожидалось присвоение или вызов функции, а вместо этого было обнаружено выражение no-unused-

Кроме того, как я могу сделать ссылку на документ родительской коллекции известной в обработчике отправки для дополнительной коллекции?

Я видел этот пост , в котором пытаются использовать одни и те же данные в 2 коллекциях (с той же заботой о поиске идентификатора).

Я также видел этот блог, в котором показано, как использовать «входы» в качестве ссылки в подколлекции, и, кажется, есть способ прикрепить их к идентификатору документа, но в блоге не показано, как определяются входные данные. Я не понимаю, как применить этот пример.

Для справки, основная форма с массивом повторяемых полей формы (в отдельной форме) представлена ​​ниже.

Основная форма

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;

Массив полей для повторяемого поля формы

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;

СЛЕДУЮЩАЯ ЗАПИСЬ

Когда я пробую предложение, изложенное ниже BrettS, я получаю предупреждение консоли, в котором говорится:

Предупреждение: необработанная ошибка была обнаружена в submitForm () FirebaseError: функция DocumentReference.set (), вызванная с недопустимыми данными. Неподдерживаемое значение поля: undefined (найдено в заголовке поля)

Я видел этот пост, в котором говорится о структурировании объекта для использования в попытке, но я не вижу, как применить эти идеи к этой проблеме.

Другая попытка, которую я пробовал, изложена ниже:

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

Когда я пытаюсь это сделать, я получаю такое же предупреждение. Он говорит:

Предупреждение: необработанная ошибка была обнаружена в submitForm () FirebaseError: функция WriteBatch.set (), вызванная с недопустимыми данными. Неподдерживаемое значение поля: undefined (найдено в заголовке поля)

Я получаю еще одну ошибку с этим форматом разделенной ссылки, в котором говорится:

Предупреждение: каждый дочерний элемент в списке должен иметь уникальную «ключевую» опору.

Я думаю, что это должно быть связано с новой структурой ссылок, но я не вижу, как это исправить.

СЛЕДУЮЩАЯ ПОПЫТКА

Когда я пробую исправленный вариант ответа Бретта, я получаю:

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

Обратите внимание, я прокомментировал все, кроме атрибута title в документе relatedTerms, чтобы я мог видеть, работает ли это вообще.

Это не так. форма все еще отображается, и когда я пытаюсь нажать кнопку «Отправить», она просто зависает. В консоли не генерируются сообщения об ошибках, но появляется предупреждающее сообщение, в котором говорится:

0.chunk.js: 141417 Предупреждение. В submitForm () обнаружена необработанная ошибка. FirebaseError: функция WriteBatch.set (), вызванная с недопустимыми данными. Неподдерживаемое значение поля: undefined (найдено в заголовке поля)

Когда я гуглил это - из этого сообщения видно, что, возможно, есть проблема с тем, как идентификатор документа родительского элемента определен в коллекции relatedTerm.

Мне также интересно, нужно ли отдельно определять и инициализировать начальные значения для каждой коллекции?

Когда я пытаюсь записать в консоль значения записей формы, я вижу, что объект со значением title захвачен. Начальные значения для формы включают массив с именем relatedTerms (начальное значение: []).

Возможно, мне нужно что-то сделать, чтобы преобразовать этот массив в значения, которые в нем содержатся, прежде чем я попытаюсь отправить это в firestore. Как бы я это сделал?

Сообщение, которое я связал, разбивает это на 2 этапа, но я слишком медленно понимаю, что они делают или как это делать сам. Странно, однако, что эта проблема не возникает, когда я не пытаюсь разделить значения формы между коллекциями firestore - если я просто использую один документ, то все, что здесь должно произойти, делается по умолчанию.

Я не уверен, что то, что я пытаюсь сделать, соответствует описанию документации firestore в разделе настраиваемых объектов . Я отмечаю, что приведенный выше пример добавления данных показывает добавление массива без каких-либо шагов, предпринятых для преобразования элементов массива в тип данных перед отправкой. Я не уверен, что это правильная линия запроса, учитывая, что отправка работает нормально, если я не пытаюсь разделить данные между коллекциями.

СЛЕДУЮЩАЯ ПОПЫТКА

Ответ Андреаса на этот пост для меня достаточно прост. Оператор распространения работает там, где он используется в методе отправки для записей relatedTerms.

Однако возникает следующая проблема - как читать данные субколлекции. Эта часть документации по firebase меня сбивает с толку. Я не могу понять этого.

Он говорит:

Получение списка коллекций невозможно с помощью клиентских библиотек для мобильных / веб-приложений.

Означает ли это, что я не могу прочитать значения в таблице relatedTerms?

Раньше я мог читать массив данных relatedTerms следующим образом:

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
  }

тогда:

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

relatedTerms теперь является вложенной коллекцией в коллекции глоссария, а не массивом в коллекции глоссария. Из этого поста я понимаю, что мне нужно запрашивать коллекции отдельно.

Итак, первый вопрос - как сохранить newDocRef.id как атрибут в документе relatedTerms. Я попытался добавить для него атрибут в отправку.

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

Хотя при отправке формы никаких ошибок не возникло, в документе relatedTerms также не было создано записи с именем glossaryId. Журнал значений также не включает его.

Я видел этот пост и ответ Джима. Я не понимаю, как использовать мой glossaryTerm.id в качестве идентификатора документа в отдельном useEffect для поиска связанныхTerms.

Ответы

6 DougStevenson Aug 15 2020 at 12:24

Каждый раз, когда вы звоните doc(), вы будете генерировать ссылку на новый случайно сгенерированный документ. Это означает, что ваш первый вызов firestore.collection("glossary").doc()будет генерировать новый идентификатор, а также последующий вызов. Если вы хотите повторно использовать ссылку на документ, вам нужно будет сохранить ее в переменной.

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

Использование этой же переменной позже:

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

У меня недостаточно кармы или чего-то еще, чтобы комментировать, поэтому я помещаю свой комментарий здесь.

Вот один из способов реализовать решение Дуга с вашим кодом. Приносим извинения за любые синтаксические ошибки - я не тестировал запуск этого кода.

Вы можете передать идентификаторы документов перед выполнением, даже если autoID создается при отправке.

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