Firebase: comment soumettre des données de formulaire à différentes collections?
J'ai un formulaire. L'un des champs du formulaire est un tableau de champs - pour les champs répétables. En dehors de ce champ, tous les autres champs du formulaire sont stockés dans une seule collection (la collection parent).
La collection parent a un tableau pour le tableau de champs, qui contient les valeurs de chaque entrée répétée, à stocker dans une sous-collection (la sous-collection).
Lorsque j'écris ma soumission Firestore, j'essaie de séparer les champs à soumettre à la collection parent des champs à soumettre à la sous-collection.
Ma tentative est ci-dessous.
<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);
});
}}
Cela produit une erreur qui dit:
Ligne 120: 22: Attendu une affectation ou un appel de fonction et à la place vu une expression non-inutilisée-
De plus, comment puis-je faire connaître la référence doc de la collection parent dans le gestionnaire d'envoi de la sous-collection?
J'ai vu ce post , qui tente d'utiliser les mêmes données dans 2 collections (avec le même souci de trouver l'identifiant).
J'ai aussi vu ce blog qui montre comment utiliser les "inputs" comme référence dans une sous-collection et semble avoir un moyen de les attacher à un doc id - mais le blog ne montre pas comment les entrées sont définies. Je ne vois pas comment appliquer cet exemple.
Pour référence, le formulaire principal, avec le tableau de champs de formulaire répétable (sous un formulaire séparé) est présenté ci-dessous.
Forme principale
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;
Matrice de champs pour champ de formulaire répétable
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;
PROCHAINE ATTMEPT
Lorsque j'essaie la suggestion présentée par BrettS ci-dessous, je reçois un avertissement de console qui dit:
Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Function DocumentReference.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)
J'ai vu cet article qui parle de structurer l'objet à utiliser dans la tentative, mais je ne vois pas comment appliquer ces idées à ce problème.
Une autre tentative que j'ai essayée est présentée ci-dessous:
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);
});
}}
Quand j'essaye ceci, j'obtiens le même genre d'avertissement. Ça dit:
Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Function WriteBatch.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)
J'obtiens une autre erreur avec ce format de référence fractionné, qui dit:
Attention: chaque enfant d'une liste doit avoir un accessoire "clé" unique.
Je pense que cela doit être lié à la nouvelle structure des références - mais je ne vois pas comment y remédier.
PROCHAINE TENTATIVE
Lorsque j'essaye la réponse suggérée révisée de Brett, j'ai:
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);
});
}}
Remarque, j'ai tout commenté sauf l'attribut title sur le document relatedTerms afin que je puisse voir si cela fonctionne du tout.
Ce n'est pas le cas. le formulaire est toujours affiché et lorsque j'essaie d'appuyer sur Soumettre, il se bloque. Aucun message d'erreur n'est généré dans la console, mais il génère un message d'avertissement qui dit:
0.chunk.js: 141417 Avertissement: Une erreur non gérée a été interceptée à partir de submitForm () FirebaseError: Fonction WriteBatch.set () appelée avec des données non valides. Valeur de champ non prise en charge: non définie (trouvée dans le titre du champ)
Lorsque je cherche sur Google, il semble d' après ce post qu'il y a peut-être un problème avec la façon dont l'identifiant de document du parent est défini dans la collection relatedTerm.
Je me demande également si les valeurs initiales doivent peut-être être définies et initialisées séparément pour chaque collection?
Lorsque j'essaie de consigner par console les valeurs des entrées de formulaire, je peux voir qu'un objet avec une valeur de titre est capturé. Les valeurs initiales du formulaire incluent un tableau appelé relatedTerms (valeur initiale: []).
Peut-être que j'ai besoin de faire quelque chose pour convertir ce tableau dans les valeurs qui y entrent avant d'essayer d'envoyer cela à Firestore. Comment pourrais-je faire ça?
Le message que j'ai lié le divise en 2 étapes, mais je suis trop lent pour comprendre ce qu'ils font ou comment les faire moi-même. Il est cependant étrange que ce problème ne se pose pas lorsque je n'essaye pas de diviser les valeurs de formulaire entre les collections firestore - si j'utilise juste un seul document, alors tout ce qui doit se passer ici est fait par défaut.
Je ne sais pas si ce que j'essaie de faire est ce que la documentation de Firestore décrit dans la section des objets personnalisés . Je note que l'exemple d'ajout de données ci-dessus montre l'ajout d'un tableau sans aucune mesure prise pour convertir les éléments du tableau en type de données avant de les soumettre. Je ne suis pas sûr que ce soit la bonne ligne d'enquête étant donné que la soumission fonctionne bien si je n'essaye pas de diviser les données entre les collections.
PROCHAINE TENTATIVE
La réponse d'Andreas sur ce post est assez simple pour que je la comprenne. L'opérateur de diffusion fonctionne là où il est utilisé dans la méthode de soumission pour les entrées liées aux termes.
Cependant, cela pose le prochain défi - qui est de savoir comment lire les données de la sous-collection. Cette partie de la documentation de Firebase me déroute . Je ne peux pas y comprendre.
Ça dit:
La récupération d'une liste de collections n'est pas possible avec les bibliothèques client mobile / Web.
Cela signifie-t-il que je ne peux pas lire les valeurs de la table relatedTerms?
Auparavant, je pouvais lire le tableau de données relatedTerms comme suit:
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
}
puis:
{glossaryTerm.relatedTerms.map(relatedTerm => (
<Link to="" className="bodylinks" key={relatedTerm.id}>
{relatedTerm.title}
</Link> ))}
relatedTerms est désormais une sous-collection dans la collection de glossaires au lieu d'un tableau dans la collection de glossaires. Je comprends de ce post que je dois interroger les collections séparément.
La première question est donc de savoir comment obtenir newDocRef.id à enregistrer en tant qu'attribut dans le document relatedTerms. J'ai essayé d'ajouter un attribut à la soumission pour cela.
glossaryId: newDocRef.id,
...values.relatedTerms
Bien qu'il n'ait généré aucune erreur lorsque j'essaie de soumettre le formulaire, il n'a pas non plus créé d'entrée dans le document relatedTerms appelé glossaryId. Le journal des valeurs ne l'inclut pas non plus.
J'ai vu ce post et la réponse de Jim. Je ne comprends pas comment utiliser mon glossaryTerm.id comme doc id dans un useEffect séparé pour trouver les relatedTerms.
Réponses
Chaque fois que vous appelez doc()
, vous allez générer une référence à un nouveau document généré aléatoirement. Cela signifie que votre premier appel firestore.collection("glossary").doc()
générera un nouvel identifiant, ainsi que l'appel suivant. Si vous souhaitez réutiliser une référence de document, vous allez devoir la stocker dans une variable.
const firstDocRef = firestore.collection("glossary").doc()
firstDocRef.set(...)
L'utilisation de cette même variable plus tard:
const secondDocRef = firstDocRef.collection('relatedTerms').doc()
secondDocRef.set(...)
Je n'ai pas assez de karma ou quoi que ce soit pour commenter, alors je mets mon commentaire ici.
Voici une façon d'implémenter la solution de Doug avec votre code. Désolé à l'avance pour les erreurs de syntaxe - je n'ai pas testé l'exécution de ce code.
Vous pouvez transmettre les identifiants de document avant l'exécution même si l'autoID est généré lors de la soumission.
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);
});
}}