Formik, Material UI Autocomplete and Firestore - where query to find the array parameter

Viewed 813

How do you modify the formik onChange handler so that it saves the value only (rather than an array of the value plus label) for options passed to Material UI Autocomplete field?

I have a collection which has a document with an attribute called category. Currently, the category gets populated with both the label and value from the form entry options.

enter image description here

I'm struggling to find a way to get a firebase where query to find the value attribute of the array.

I'm wondering if I might get closer to a working solution if I try to just save the value instead of both the label and the value into firestore.

I have a Formik form with:

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,
  Divider,
  InputLabel,
  FormControlLabel,
  TextField,
  Typography,
  Box,
  Grid,
  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,
  CheckboxWithLabel,
  Checkbox
} from 'formik-material-ui';


const allCategories = [
    {value: 'health', label: 'Health & Medical'},
    {value: 'general', label: 'General'},    
];


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 Summary(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 
          </Button>
        <Dialog
          open={open}
          onClose={handleClose}
          aria-labelledby="form-dialog-title"
        >
          {!isSubmitionCompleted &&
            <React.Fragment>
              
              <DialogContent>
                <Formik
                  initialValues={{ title: "",  category: [], subcategory: "" }}
                  
                  onSubmit={(values, { setSubmitting }) => {
                     setSubmitting(true);
                     
                 
    firestore.collection("study").doc().set({
                      ...values,
                      createdAt: firebase.firestore.FieldValue.serverTimestamp()
                      })
                    .then(() => {
                      setSubmitionCompleted(true);
                    });
                  }}
  
                  validationSchema={Yup.object().shape({
                    title: Yup.string()
                      .required('Required'),
                    category: Yup.string()
                      .required('Required'),
                    
                  })}
                >
                  {(props) => {
                    const {
                      values,
                      touched,
                      errors,
                      dirty,
                      isSubmitting,
                      handleChange,
                      handleBlur,
                      handleSubmit,
                      handleReset,
                    } = props;
                    return (
                      <form onSubmit={handleSubmit}>
                        <TextField
                          label="Title"
                          name="title"
                          //   className={classes.textField}
                          value={values.title}
                          onChange={handleChange}
                          onBlur={handleBlur}
                          helperText={(errors.title && touched.title) && errors.title}
                          margin="normal"
                          style={{ width: "100%"}}
                        />

                        
                        <Box margin={1}>
                          <Field
                            name="category"
                            multiple
                            component={Autocomplete}
                            options={allCategories}
                            // value={values.label}
                            // value={values.value}
                            // value={allCategories.value} 
                           // value={values.category.allCategories.value}

I tried each of these attempts (one at a time) at getting the array to populate with a single field only - but none of them work to do that. Instead, firebase records both label and value in its array.

                            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> 
                        
                        
                        <TextField
                          label="Subcategory "
                          name="subcategory"
                          //   className={classes.textField}
                          value={values.subcategory}
                          onChange={handleChange}
                          onBlur={handleBlur}
                          helperText={(errors.subcategory && touched.subcategory) && errors.subcategory}
                          margin="normal"
                          style={{ width: "100%"}}
                        />
  
                        
                      
                        <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">Done!</DialogTitle>
              <DialogContent>
               
                <DialogActions>
                  <Button
                    type="button"
                    className="outline"
                    onClick={handleClose}
                  >
                    Close
                    </Button>
                  {/* <DisplayFormikState {...props} /> */}
                </DialogActions>
              </DialogContent>
            </React.Fragment>}
        </Dialog>
      </React.Fragment>
    );
  }

export default Summary;

Then when I try to query firebase, I'm trying to find documents where the category includes health.

I have tried each of the where queries below but I can't get any of them to return the queried results (I can return all the results if I remove the where query):

function useHealthTerms() {
    const [healthTerms, setHealthTerms] = useState([])
    useEffect(() => {
      firebase
        .firestore()
        .collection("study")
    //.where("title", "==", "ss") 

NOTE - this works to find the title. The title field is at the same level as the category field

        //.where('category', '==', 'health')
        //.where('category.value', "array-contains", 'health")
        //.where('category', 'array-contains', 'health')
        //.where('category', 'array-contains', 1)
    //.where("category.1.value", '==', 'health')
        .onSnapshot(snapshot => {
          const healthTerms = snapshot.docs.map(doc => ({
            id: doc.id,
            ...doc.data(),
          }))
          setHealthTerms(healthTerms)
        })
    }, [])
    return healthTerms
  }

I have seen this post, but I'm not clever enough to make any sense from the answer to it.

I've also seen this post and the answer suggested by Betty. I've tried multiple variations on the following query construct to try and use the idea, but each time, I get an error with the form of the query.

.where(new firebase.firestore().FieldPath("category", "value"), '==', 'health')

I'm wondering if I can try to get the category form field in formik just to save the option.value instead of both label and value.

I can't see how the formik handleChange works to ask it to save just the value.

Even then, I can't see how to query firestore to use the content of an array as a query parameter.

Does anyone know:

  1. how to save just the option value (instead of both option label and value) in the Autocomplete via formik form submission to firestore?

  2. how to query the content of the array in firestore to see if one of its attributes matches the query?

It's strange because this post suggests that a where query on an array should be possible using the forms I've tried above. However, this post suggests the following format .collection(study/[docId]).where("value", "==", "health"). I need it to search each document in the collection so I don't know how to apply that approach to this problem.

The answer from gso_gabriel below suggests two confusing things. First, there is an assumption that I have used a subcollection. I haven't. Adding the picture below to show the category field is in the parent document. I can do a where query on title using the formats shown above to extract the value.

enter image description here

Secondly, and the bit that is most confusing - it says: "As you can't search for a object inside an Array". What does this mean? Is it suggesting that the query cannot be performed on the content of value inside the category field? If this is the case, are there resources providing guidance for how to query this piece of data?

I have also seen this post - the answer to which suggests that querying value within category is not possible using firebase. The problem is that I can't understand the suggested alternative approach. If I have understood this post correctly, are there any tutorials which expand on the principles so that I can try to find a different query strategy?

The first answer on this post also suggests that it isn't possible to query value inside category. The second answer suggests using a different format in the where query - as follows.

.where("category", "array-contains", {value: "health", label: "Health & Medical"})

The answer stresses the importance of adding the entire array contents to the curly braces. This works.

So - this brings me back to the Autocomplete submit handler. It's a multiple select field, so there may be more than one value selected. How do I make those into a set of single values on the firebase document. Even if there were only one, how do I change the submit handler so that it only sends the select option value instead of both the value and the label?

If it isn't possible to query a object in an array - how do I change the submit handler to add just the selected option(s) values to firebase, instead of both labels and values? The suggested workaround in the first answer to this post is to add a field which just holds the value to be queried (so: health).

2 Answers

Before submitting to firebase in onSubmit you can change the shape of the data you send

onSubmit={(values, { setSubmitting }) => {
    setSubmitting(true);

    firestore.collection("study").doc().set({
     ...values,
     category: values.category.map(c => c.value),
     createdAt: firebase.firestore.FieldValue.serverTimestamp()
    })
    .then(() => {
        setSubmitionCompleted(true);
    });
}}

I will try to clarify your doubts, by answering your points individually.

  • For you to save just the value, you will need to change the way you are sending information to Firestore. Change your values variable, as this one is maintaining values from the label as well. To summarize, you will need to basically change your code when relating to Firestore, where you variables with label values.
  • Clarifying this post you mentioned here - usually NoSQL databases are created based in the way that you will be querying. So, you will first right the queries as you want to work with them and the structure your database, so you don't have problems with it. As you can't search for a object inside an Array, indeed, I would say that restructuring your scheme would be the best choice.
  • Maybe using a subcollection as clarified in this example here might help you make things easier.

Changing would be the best option for you, so you can have a scheme that fits exactly your queries and needs - that's one of the many good parts of NoSQL, the freedom for creating the scheme.

Related