3

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

Mel
  • 2,481
  • 26
  • 113
  • 273

2 Answers2

3

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);
    });
}}
Mohamed Ramrami
  • 12,026
  • 4
  • 33
  • 49
2

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.

gso_gabriel
  • 4,199
  • 1
  • 10
  • 22
  • Thank you for this note @gso_gabirel. – Mel Sep 09 '20 at 19:51
  • Firstly, the reason for the question about the Autocomplete is that if I don't use the AllCategories array in the form element, then the displayed options in the select menu don't use the label to populate the dropdown. I can't find an example that shows how to just save the value instead of both label and value. – Mel Sep 09 '20 at 19:52
  • Second. I don't have a subcollection in this collection. I don't know why you think I do. I have a collection, that includes an attribute called category. Category is an array. The array has a property called 'value' which I'm trying to query. Your subcollection const example is exactly as I have tried to use (as shown in the post above). It does not work to find the relevant documents. – Mel Sep 09 '20 at 19:54
  • As for the final point about structuring the data, I'm no clearer on the subject. Do you know where examples might be found? When you say that I can't search for an object inside an array, do you mean that it is not possible to query 'value' inside 'category'? If so, do you know how to find a way to structure things differently to make this data searchable? – Mel Sep 09 '20 at 19:56
  • For completeness, I have an attribute in the same document as shown in the picture above called 'title'. When I use .where("title", "==", "test"), the query works to return the data, so I'm confident that your comments relating to sub collections are not applicable to this problem. – Mel Sep 09 '20 at 19:58
  • Hi @Mel I appreciate your clarification about the array! You didn't show a complete screenshot from your collection including the `title`, so I couldn't get a complete glance from your collection. Maybe using a subcollection then, would make more sense, as it's easier to deal with them than with arrays. I have updated my answer removing the subcollections part. – gso_gabriel Sep 10 '20 at 05:50
  • About examples on structuring data, you may find similar examples, as this one [here](https://stackoverflow.com/questions/54266090/are-there-any-benefits-of-using-subcollections-in-firestore) with very good explanation on how Subcollections is good and how to work with it. Adding to that, have you tried changing your collection to not work with arrays, so you have only `value`? In this line `initialValues={{ title: "", category: [], subcategory: "" }}` you are sending an array, have you tried changing the `category: []` to only a value? Something like `category[0].value`. – gso_gabriel Sep 10 '20 at 05:50
  • Hi @gso_gabriel - thanks. the category needs to be an array because the form field is a multiple select option. Thanks anyway for the time you've invested in trying to help. – Mel Sep 10 '20 at 19:57