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