admin管理员组文章数量:1023773
I have one form. One of the fields in the form is a Field Array - for repeatable fields. Apart from this field, all the other form fields are stored in a single collection (the Parent Collection).
The Parent Collection has an array for the Field Array, which holds the values of each repeated entry, to be stored in a sub-collection (the Sub Collection).
When I'm writing my firestore submit, I'm trying to separate the fields to be submitted to the Parent Collection, from the fields to be submitted to the Sub Collection.
My attempt is below.
<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);
});
}}
This produces an error that says:
Line 120:22: Expected an assignment or function call and instead saw an expression no-unused-
Also, how can I make the doc reference of the Parent Collection known in the submit handler for the Sub Collection?
I have seen this post, which is trying to use the same data in 2 collections (with the same concern for finding the id).
I have also seen this blog which shows how to use "inputs" as a reference in a sub-collection and seems to have a way to attach them to a doc id - but the blog doesn't show how inputs is defined. I can't see how to apply that example.
For reference, the main form, with the repeatable form field array (in a separate form) is set out below.
Main form
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 {
Autoplete,
ToggleButtonGroup,
AutopleteRenderInputParams,
} 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
// ponent="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 munity 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
ponent={Autoplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutopleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autoplete'] && !!errors['autoplete']}
helperText={touched['autoplete'] && errors['autoplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<FieldArray name="relatedTerms" ponent={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 for repeatable form field
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`}
ponent="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;
NEXT ATTMEPT
When I try the suggestion set out by BrettS below, I get a console warning that says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field title)
I have seen this post that talks about structuring the object to use in the attempt, but I can't see how to apply those ideas to this problem.
Another attempt I've tried is set out below:
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,
});
writeBatchmit().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);
});
}}
When I try this, I get the same sort of warning. It says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
I get another error with this split reference format, which says:
Warning: Each child in a list should have a unique "key" prop.
I think that must be something to do with the new structure of the references - but I can't see how to address it.
NEXT ATTEMPT
When I try Brett's revised suggested answer, I have:
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,
})
writeBatchmit()
.then(() => {
setSubmitionCompleted(true);
});
}}
Note, I mented everything but the title attribute on the relatedTerms document so that I could see if this works at all.
It doesn't. the form still renders and when I try to press submit, it just hangs. No error messages are generated in the console, but it does generate a warning message that says:
0.chunk.js:141417 Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
When I google this - it looks from this post that maybe there is a problem with the way the doc id of the parent is defined in the relatedTerm collection.
I'm also wondering if the initial values maybe need to be separately defined and initialised for each collection?
When I try console logging the values of the form entries, I can see that an object with a value of title is captured. The initial values for the form include an array called relatedTerms (initial value: []).
Maybe I need to do something to convert that array into the values that go in it before I try sending this to firestore. How would I do that?
The post I linked breaks this into 2 steps, but I am too slow to figure out what they are doing or how to do them myself. It is strange though that this problem doesn't arise when I don't try to split the form values between firestore collections - if I just use a single document, then whatever needs to happen here is being done by default.
I'm not sure if what I'm trying to do is what the firestore docs are describing in the custom objects section. I note that the adding data example above it shows adding an array without any steps taken to convert the items in the array to the data type before submitting. I'm not sure if this is the right line of enquiry given that the submission works fine if I don't try to split the data between collections.
NEXT ATTEMPT
The answer from Andreas on this post is simple enough for me to grasp. The spread operator works where it is used in the submit method for the relatedTerms entries.
However, that throws up the next challenge - which is how to read the sub collection data. This part of the firebase documentation is baffling to me. I can't make sense of it.
It says:
Retrieving a list of collections is not possible with the mobile/web client libraries.
Does it mean I can't read the values in relatedTerms table?
Previously, I was able to read the array of relatedTerms data as follows:
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
}
then:
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Link to="" className="bodylinks" key={relatedTerm.id}>
{relatedTerm.title}
</Link> ))}
relatedTerms is now a sub collection in the glossary collection instead of an array in the glossary collection. I understand from this post that I have to query the collections separately.
So the first query is how to get newDocRef.id to save as an attribute in the relatedTerms document. I tried adding an attribute to the submit for it.
glossaryId: newDocRef.id,
...values.relatedTerms
Whilst it didn't generate any errors when I try submitting the form, it also didn't create an entry in the relatedTerms document called glossaryId. The log of values doesn't include it either.
I have seen this post and the answer by Jim. I don't understand how to use my glossaryTerm.id as the doc id in a separate useEffect to find the relatedTerms.
I have one form. One of the fields in the form is a Field Array - for repeatable fields. Apart from this field, all the other form fields are stored in a single collection (the Parent Collection).
The Parent Collection has an array for the Field Array, which holds the values of each repeated entry, to be stored in a sub-collection (the Sub Collection).
When I'm writing my firestore submit, I'm trying to separate the fields to be submitted to the Parent Collection, from the fields to be submitted to the Sub Collection.
My attempt is below.
<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);
});
}}
This produces an error that says:
Line 120:22: Expected an assignment or function call and instead saw an expression no-unused-
Also, how can I make the doc reference of the Parent Collection known in the submit handler for the Sub Collection?
I have seen this post, which is trying to use the same data in 2 collections (with the same concern for finding the id).
I have also seen this blog which shows how to use "inputs" as a reference in a sub-collection and seems to have a way to attach them to a doc id - but the blog doesn't show how inputs is defined. I can't see how to apply that example.
For reference, the main form, with the repeatable form field array (in a separate form) is set out below.
Main form
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 {
Autoplete,
ToggleButtonGroup,
AutopleteRenderInputParams,
} 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
// ponent="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 munity 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
ponent={Autoplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutopleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autoplete'] && !!errors['autoplete']}
helperText={touched['autoplete'] && errors['autoplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<FieldArray name="relatedTerms" ponent={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 for repeatable form field
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`}
ponent="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;
NEXT ATTMEPT
When I try the suggestion set out by BrettS below, I get a console warning that says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field title)
I have seen this post that talks about structuring the object to use in the attempt, but I can't see how to apply those ideas to this problem.
Another attempt I've tried is set out below:
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.mit().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);
});
}}
When I try this, I get the same sort of warning. It says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
I get another error with this split reference format, which says:
Warning: Each child in a list should have a unique "key" prop.
I think that must be something to do with the new structure of the references - but I can't see how to address it.
NEXT ATTEMPT
When I try Brett's revised suggested answer, I have:
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.mit()
.then(() => {
setSubmitionCompleted(true);
});
}}
Note, I mented everything but the title attribute on the relatedTerms document so that I could see if this works at all.
It doesn't. the form still renders and when I try to press submit, it just hangs. No error messages are generated in the console, but it does generate a warning message that says:
0.chunk.js:141417 Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
When I google this - it looks from this post that maybe there is a problem with the way the doc id of the parent is defined in the relatedTerm collection.
I'm also wondering if the initial values maybe need to be separately defined and initialised for each collection?
When I try console logging the values of the form entries, I can see that an object with a value of title is captured. The initial values for the form include an array called relatedTerms (initial value: []).
Maybe I need to do something to convert that array into the values that go in it before I try sending this to firestore. How would I do that?
The post I linked breaks this into 2 steps, but I am too slow to figure out what they are doing or how to do them myself. It is strange though that this problem doesn't arise when I don't try to split the form values between firestore collections - if I just use a single document, then whatever needs to happen here is being done by default.
I'm not sure if what I'm trying to do is what the firestore docs are describing in the custom objects section. I note that the adding data example above it shows adding an array without any steps taken to convert the items in the array to the data type before submitting. I'm not sure if this is the right line of enquiry given that the submission works fine if I don't try to split the data between collections.
NEXT ATTEMPT
The answer from Andreas on this post is simple enough for me to grasp. The spread operator works where it is used in the submit method for the relatedTerms entries.
However, that throws up the next challenge - which is how to read the sub collection data. This part of the firebase documentation is baffling to me. I can't make sense of it.
It says:
Retrieving a list of collections is not possible with the mobile/web client libraries.
Does it mean I can't read the values in relatedTerms table?
Previously, I was able to read the array of relatedTerms data as follows:
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
}
then:
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Link to="" className="bodylinks" key={relatedTerm.id}>
{relatedTerm.title}
</Link> ))}
relatedTerms is now a sub collection in the glossary collection instead of an array in the glossary collection. I understand from this post that I have to query the collections separately.
So the first query is how to get newDocRef.id to save as an attribute in the relatedTerms document. I tried adding an attribute to the submit for it.
glossaryId: newDocRef.id,
...values.relatedTerms
Whilst it didn't generate any errors when I try submitting the form, it also didn't create an entry in the relatedTerms document called glossaryId. The log of values doesn't include it either.
I have seen this post and the answer by Jim. I don't understand how to use my glossaryTerm.id as the doc id in a separate useEffect to find the relatedTerms.
Share Improve this question edited Aug 28, 2020 at 2:02 Mel asked Aug 15, 2020 at 5:18 MelMel 2,72531 gold badges137 silver badges313 bronze badges 4- 1 Firestore (and other document-based databases) have techniques to achieve what you're trying to do in, relatively speaking, better ways. I would start here if you're not into technical literature: youtube./watch?v=lW7DWV2jST0. – OFRBG Commented Aug 21, 2020 at 23:12
- 1 thank you @Cehhiro for the suggestion. I just watched this 40min video. Whilst it theoretically speaks to how data might be structured and stored, it doesn't actually answer my question. Is there a particular timestamp on the video that you think is relevant to the question I asked? – Mel Commented Aug 22, 2020 at 3:12
- 1 Hi @Mel, so now you are trying to get subcollections and you are not able to do that, am i right? Sorry, i didn't get it quite well... – Luis Paulo Pinto Commented Aug 31, 2020 at 15:12
- 1 Hi @LuisPauloPinto - yes - i made a new post for it here: stackoverflow./questions/63642448/… – Mel Commented Aug 31, 2020 at 21:04
2 Answers
Reset to default 6 +100Every time you call doc()
, you're going to generate a reference to a new randomly generated document. That means your first call to firestore.collection("glossary").doc()
will generate a new ID, as well as the subsequent call. If you want to reuse a document reference, you're going to have to store it in a variable.
const firstDocRef = firestore.collection("glossary").doc()
firstDocRef.set(...)
The use that same variable later:
const secondDocRef = firstDocRef.collection('relatedTerms').doc()
secondDocRef.set(...)
I don't have enough karma or whatever to ment so I'm putting my ment here.
Here's one way to implement Doug's solution with your code. Sorry in advanced for any syntax errors -- I did not test run this code.
You can pass document ids before execution even though the autoID is generated when the submit is made.
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.mit()
.then(() => {
setSubmitionCompleted(true);
});
}}
I have one form. One of the fields in the form is a Field Array - for repeatable fields. Apart from this field, all the other form fields are stored in a single collection (the Parent Collection).
The Parent Collection has an array for the Field Array, which holds the values of each repeated entry, to be stored in a sub-collection (the Sub Collection).
When I'm writing my firestore submit, I'm trying to separate the fields to be submitted to the Parent Collection, from the fields to be submitted to the Sub Collection.
My attempt is below.
<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);
});
}}
This produces an error that says:
Line 120:22: Expected an assignment or function call and instead saw an expression no-unused-
Also, how can I make the doc reference of the Parent Collection known in the submit handler for the Sub Collection?
I have seen this post, which is trying to use the same data in 2 collections (with the same concern for finding the id).
I have also seen this blog which shows how to use "inputs" as a reference in a sub-collection and seems to have a way to attach them to a doc id - but the blog doesn't show how inputs is defined. I can't see how to apply that example.
For reference, the main form, with the repeatable form field array (in a separate form) is set out below.
Main form
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 {
Autoplete,
ToggleButtonGroup,
AutopleteRenderInputParams,
} 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
// ponent="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 munity 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
ponent={Autoplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutopleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autoplete'] && !!errors['autoplete']}
helperText={touched['autoplete'] && errors['autoplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<FieldArray name="relatedTerms" ponent={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 for repeatable form field
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`}
ponent="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;
NEXT ATTMEPT
When I try the suggestion set out by BrettS below, I get a console warning that says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field title)
I have seen this post that talks about structuring the object to use in the attempt, but I can't see how to apply those ideas to this problem.
Another attempt I've tried is set out below:
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,
});
writeBatchmit().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);
});
}}
When I try this, I get the same sort of warning. It says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
I get another error with this split reference format, which says:
Warning: Each child in a list should have a unique "key" prop.
I think that must be something to do with the new structure of the references - but I can't see how to address it.
NEXT ATTEMPT
When I try Brett's revised suggested answer, I have:
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,
})
writeBatchmit()
.then(() => {
setSubmitionCompleted(true);
});
}}
Note, I mented everything but the title attribute on the relatedTerms document so that I could see if this works at all.
It doesn't. the form still renders and when I try to press submit, it just hangs. No error messages are generated in the console, but it does generate a warning message that says:
0.chunk.js:141417 Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
When I google this - it looks from this post that maybe there is a problem with the way the doc id of the parent is defined in the relatedTerm collection.
I'm also wondering if the initial values maybe need to be separately defined and initialised for each collection?
When I try console logging the values of the form entries, I can see that an object with a value of title is captured. The initial values for the form include an array called relatedTerms (initial value: []).
Maybe I need to do something to convert that array into the values that go in it before I try sending this to firestore. How would I do that?
The post I linked breaks this into 2 steps, but I am too slow to figure out what they are doing or how to do them myself. It is strange though that this problem doesn't arise when I don't try to split the form values between firestore collections - if I just use a single document, then whatever needs to happen here is being done by default.
I'm not sure if what I'm trying to do is what the firestore docs are describing in the custom objects section. I note that the adding data example above it shows adding an array without any steps taken to convert the items in the array to the data type before submitting. I'm not sure if this is the right line of enquiry given that the submission works fine if I don't try to split the data between collections.
NEXT ATTEMPT
The answer from Andreas on this post is simple enough for me to grasp. The spread operator works where it is used in the submit method for the relatedTerms entries.
However, that throws up the next challenge - which is how to read the sub collection data. This part of the firebase documentation is baffling to me. I can't make sense of it.
It says:
Retrieving a list of collections is not possible with the mobile/web client libraries.
Does it mean I can't read the values in relatedTerms table?
Previously, I was able to read the array of relatedTerms data as follows:
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
}
then:
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Link to="" className="bodylinks" key={relatedTerm.id}>
{relatedTerm.title}
</Link> ))}
relatedTerms is now a sub collection in the glossary collection instead of an array in the glossary collection. I understand from this post that I have to query the collections separately.
So the first query is how to get newDocRef.id to save as an attribute in the relatedTerms document. I tried adding an attribute to the submit for it.
glossaryId: newDocRef.id,
...values.relatedTerms
Whilst it didn't generate any errors when I try submitting the form, it also didn't create an entry in the relatedTerms document called glossaryId. The log of values doesn't include it either.
I have seen this post and the answer by Jim. I don't understand how to use my glossaryTerm.id as the doc id in a separate useEffect to find the relatedTerms.
I have one form. One of the fields in the form is a Field Array - for repeatable fields. Apart from this field, all the other form fields are stored in a single collection (the Parent Collection).
The Parent Collection has an array for the Field Array, which holds the values of each repeated entry, to be stored in a sub-collection (the Sub Collection).
When I'm writing my firestore submit, I'm trying to separate the fields to be submitted to the Parent Collection, from the fields to be submitted to the Sub Collection.
My attempt is below.
<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);
});
}}
This produces an error that says:
Line 120:22: Expected an assignment or function call and instead saw an expression no-unused-
Also, how can I make the doc reference of the Parent Collection known in the submit handler for the Sub Collection?
I have seen this post, which is trying to use the same data in 2 collections (with the same concern for finding the id).
I have also seen this blog which shows how to use "inputs" as a reference in a sub-collection and seems to have a way to attach them to a doc id - but the blog doesn't show how inputs is defined. I can't see how to apply that example.
For reference, the main form, with the repeatable form field array (in a separate form) is set out below.
Main form
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 {
Autoplete,
ToggleButtonGroup,
AutopleteRenderInputParams,
} 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
// ponent="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 munity 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
ponent={Autoplete}
options={allCategories}
getOptionLabel={(option: any) => option.label}
style={{width: '100%'}}
renderInput={(params: AutopleteRenderInputParams) => (
<MuiTextField
{...params}
error={touched['autoplete'] && !!errors['autoplete']}
helperText={touched['autoplete'] && errors['autoplete']}
label="Category"
variant="outlined"
/>
)}
/>
</Box>
<FieldArray name="relatedTerms" ponent={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 for repeatable form field
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`}
ponent="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;
NEXT ATTMEPT
When I try the suggestion set out by BrettS below, I get a console warning that says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field title)
I have seen this post that talks about structuring the object to use in the attempt, but I can't see how to apply those ideas to this problem.
Another attempt I've tried is set out below:
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.mit().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);
});
}}
When I try this, I get the same sort of warning. It says:
Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
I get another error with this split reference format, which says:
Warning: Each child in a list should have a unique "key" prop.
I think that must be something to do with the new structure of the references - but I can't see how to address it.
NEXT ATTEMPT
When I try Brett's revised suggested answer, I have:
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.mit()
.then(() => {
setSubmitionCompleted(true);
});
}}
Note, I mented everything but the title attribute on the relatedTerms document so that I could see if this works at all.
It doesn't. the form still renders and when I try to press submit, it just hangs. No error messages are generated in the console, but it does generate a warning message that says:
0.chunk.js:141417 Warning: An unhandled error was caught from submitForm() FirebaseError: Function WriteBatch.set() called with invalid data. Unsupported field value: undefined (found in field title)
When I google this - it looks from this post that maybe there is a problem with the way the doc id of the parent is defined in the relatedTerm collection.
I'm also wondering if the initial values maybe need to be separately defined and initialised for each collection?
When I try console logging the values of the form entries, I can see that an object with a value of title is captured. The initial values for the form include an array called relatedTerms (initial value: []).
Maybe I need to do something to convert that array into the values that go in it before I try sending this to firestore. How would I do that?
The post I linked breaks this into 2 steps, but I am too slow to figure out what they are doing or how to do them myself. It is strange though that this problem doesn't arise when I don't try to split the form values between firestore collections - if I just use a single document, then whatever needs to happen here is being done by default.
I'm not sure if what I'm trying to do is what the firestore docs are describing in the custom objects section. I note that the adding data example above it shows adding an array without any steps taken to convert the items in the array to the data type before submitting. I'm not sure if this is the right line of enquiry given that the submission works fine if I don't try to split the data between collections.
NEXT ATTEMPT
The answer from Andreas on this post is simple enough for me to grasp. The spread operator works where it is used in the submit method for the relatedTerms entries.
However, that throws up the next challenge - which is how to read the sub collection data. This part of the firebase documentation is baffling to me. I can't make sense of it.
It says:
Retrieving a list of collections is not possible with the mobile/web client libraries.
Does it mean I can't read the values in relatedTerms table?
Previously, I was able to read the array of relatedTerms data as follows:
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
}
then:
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Link to="" className="bodylinks" key={relatedTerm.id}>
{relatedTerm.title}
</Link> ))}
relatedTerms is now a sub collection in the glossary collection instead of an array in the glossary collection. I understand from this post that I have to query the collections separately.
So the first query is how to get newDocRef.id to save as an attribute in the relatedTerms document. I tried adding an attribute to the submit for it.
glossaryId: newDocRef.id,
...values.relatedTerms
Whilst it didn't generate any errors when I try submitting the form, it also didn't create an entry in the relatedTerms document called glossaryId. The log of values doesn't include it either.
I have seen this post and the answer by Jim. I don't understand how to use my glossaryTerm.id as the doc id in a separate useEffect to find the relatedTerms.
Share Improve this question edited Aug 28, 2020 at 2:02 Mel asked Aug 15, 2020 at 5:18 MelMel 2,72531 gold badges137 silver badges313 bronze badges 4- 1 Firestore (and other document-based databases) have techniques to achieve what you're trying to do in, relatively speaking, better ways. I would start here if you're not into technical literature: youtube./watch?v=lW7DWV2jST0. – OFRBG Commented Aug 21, 2020 at 23:12
- 1 thank you @Cehhiro for the suggestion. I just watched this 40min video. Whilst it theoretically speaks to how data might be structured and stored, it doesn't actually answer my question. Is there a particular timestamp on the video that you think is relevant to the question I asked? – Mel Commented Aug 22, 2020 at 3:12
- 1 Hi @Mel, so now you are trying to get subcollections and you are not able to do that, am i right? Sorry, i didn't get it quite well... – Luis Paulo Pinto Commented Aug 31, 2020 at 15:12
- 1 Hi @LuisPauloPinto - yes - i made a new post for it here: stackoverflow./questions/63642448/… – Mel Commented Aug 31, 2020 at 21:04
2 Answers
Reset to default 6 +100Every time you call doc()
, you're going to generate a reference to a new randomly generated document. That means your first call to firestore.collection("glossary").doc()
will generate a new ID, as well as the subsequent call. If you want to reuse a document reference, you're going to have to store it in a variable.
const firstDocRef = firestore.collection("glossary").doc()
firstDocRef.set(...)
The use that same variable later:
const secondDocRef = firstDocRef.collection('relatedTerms').doc()
secondDocRef.set(...)
I don't have enough karma or whatever to ment so I'm putting my ment here.
Here's one way to implement Doug's solution with your code. Sorry in advanced for any syntax errors -- I did not test run this code.
You can pass document ids before execution even though the autoID is generated when the submit is made.
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.mit()
.then(() => {
setSubmitionCompleted(true);
});
}}
本文标签: javascriptFirebase How to submit form data to different collectionsStack Overflow
版权声明:本文标题:javascript - Firebase: How to submit form data to different collections? - Stack Overflow 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://it.en369.cn/questions/1745605455a2158703.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论