3

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 {
  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;

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

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

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.commit()
    .then(() => {
      setSubmitionCompleted(true);
    });
}}

Note, I commented 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: []).

enter image description here

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.

enter image description here

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.

Mel
  • 2,481
  • 26
  • 113
  • 273
  • 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: https://www.youtube.com/watch?v=lW7DWV2jST0. – OFRBG Aug 21 '20 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 Aug 22 '20 at 03: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 Aug 31 '20 at 15:12
  • 1
    Hi @LuisPauloPinto - yes - i made a new post for it here: https://stackoverflow.com/questions/63642448/reading-firestore-sub-collection-data-in-react – Mel Aug 31 '20 at 21:04

2 Answers2

6

Every 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(...)
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • But the first ref won't be generated until after the submit is complete? So - how can I give the glossary id to the relatedTerms collection? – Mel Aug 15 '20 at 05:25
  • I'm not sure what you're asking. What specifically does not work here the way you expect? – Doug Stevenson Aug 15 '20 at 05:34
  • I have a form which has fields for "term", "definition" and "relatedTerms". The first two fields are strings and get submitted as a document in the glossary collection, along with an array for relatedTerms. relatedTerms is a repeatable form filed (users can submit several field entries). Each one gets saved in the 'relatedTerms' sub-collection, along with the id of the relevant glossary document. These terms get completed in a single form, but the submit handler needs to send the data to different collections. I'm trying to figure out how to do that submission. – Mel Aug 15 '20 at 05:47
  • What specifically about my suggestion didn't work the way you expect? Do you see how you were generating a different document ID for the document immediately nested under "glossary"? – Doug Stevenson Aug 15 '20 at 05:49
  • I don't understand your suggestion. The autoID is generated on glossary when the submit is made. How can I catch that and then use it in the relatedTerms field? As for the format of your consts, I had understood that the doc id goes inside the parenthes not after: .doc() firstDocRef. I don't know how to use these consts in the submit handler. – Mel Aug 15 '20 at 06:19
  • I don't know enough about formik to know what the specific syntax will be. You'll need to find a way to share a common document reference between the two writes. What I provided is how you would do it in plain JavaScript. – Doug Stevenson Aug 15 '20 at 06:32
  • You can grab it like this: `let autoID = db.collection(....).doc().id` – UnReal G Aug 17 '20 at 09:07
  • @UnRealG - but that's my point - the Parent Collection doc id is set on submit. How can I use it in the Sub Collection within the same submit handler? – Mel Aug 17 '20 at 22:29
  • Then you can do this: `db.collection("cities").doc().set(data);` To set the new doc of random ID and set data inside of it – UnReal G Aug 18 '20 at 00:32
  • @UnRealG - thank you for the suggestion. I'm afraid I don't understand how it relates to my question. The attempt I made for setting the Sub Collection was: firestore.collection("glossary").doc().collection('relatedTerms').doc().set({ ... That didn't work. I can't figure out how to do this in the context of the issue I have set out in the post. – Mel Aug 18 '20 at 23:15
  • Thank you for the help. Ill add the points tomorrow. – Mel Aug 24 '20 at 23:45
3

I don't have enough karma or whatever to comment so I'm putting my comment 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.commit()
    .then(() => {
      setSubmitionCompleted(true);
    });
}}
Brett S
  • 579
  • 4
  • 8
  • 1
    I haven't figured out how to use this yet, when I try what you've given me, I get an error that says: Expected an assignment or function call and instead saw an expression no-unused-expressions - but I will keep trying and share what I discover when I get to a solution that works. You've opened my eyes. If your approach can be made to work, then I have banked a new piece of knowledge. Thank you – Mel Aug 25 '20 at 23:58
  • 1
    I think the problem may have something to do with this firebase warning: Warning: An unhandled error was caught from submitForm() FirebaseError: Function DocumentReference.set() called with invalid data. Unsupported field value: undefined (found in field title) – Mel Aug 26 '20 at 00:07
  • 2
    Are you sure you can use the the doc ref in the same submit handler? – Mel Aug 26 '20 at 00:07
  • 1
    Good point. I see the issue here. Needed to set things up as a batch write. Will edit my answer. – Brett S Aug 26 '20 at 04:20
  • 1
    The `Unsupported field value: undefined (found in field title)` error occurs when something is undefined. For example trying `firestore.collection('Example').doc().set({ field: undefined })` would result in this error. – Brett S Aug 26 '20 at 04:27
  • That's what occurs when I try to submit the form using the suggestion above. It doesn't submit, instead I get this warning. – Mel Aug 26 '20 at 05:07
  • Thanks for having another look. I'll try this approach later this afternoon. Thank you – Mel Aug 26 '20 at 05:08
  • I cannot get this code to work. Replacing the , with a ; where it appears after the first writeBatch segment gets rid of the first error but the warning remains: 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 try to submit, the form hangs, no error but no submission. This warning maybe indicates a problem with the way the initial values are defined now? – Mel Aug 26 '20 at 22:19
  • Maybe if the 'title' is being stored in a different table, it needs to be established in a different const for initial values? This is a guess - but I can't make sense of this. Thanks anyway for trying to help. – Mel Aug 26 '20 at 22:19