Firebase: как отправлять данные формы в разные коллекции?
У меня одна форма. Одно из полей в форме - массив полей - для повторяющихся полей. Помимо этого поля, все остальные поля формы хранятся в единой коллекции (родительской коллекции).
В родительской коллекции есть массив для массива полей, в котором хранятся значения каждой повторяющейся записи, которые должны быть сохранены в под-коллекции (Подколлекция).
Когда я пишу свою отправку 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.
Ответы
Каждый раз, когда вы звоните doc()
, вы будете генерировать ссылку на новый случайно сгенерированный документ. Это означает, что ваш первый вызов firestore.collection("glossary").doc()
будет генерировать новый идентификатор, а также последующий вызов. Если вы хотите повторно использовать ссылку на документ, вам нужно будет сохранить ее в переменной.
const firstDocRef = firestore.collection("glossary").doc()
firstDocRef.set(...)
Использование этой же переменной позже:
const secondDocRef = firstDocRef.collection('relatedTerms').doc()
secondDocRef.set(...)
У меня недостаточно кармы или чего-то еще, чтобы комментировать, поэтому я помещаю свой комментарий здесь.
Вот один из способов реализовать решение Дуга с вашим кодом. Приносим извинения за любые синтаксические ошибки - я не тестировал запуск этого кода.
Вы можете передать идентификаторы документов перед выполнением, даже если 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);
});
}}