Formik Material UI React - Autocomplete - uncontrolled to controlled state
Asked Answered
L

2

6

I'm trying to figure out how to follow the instructions in the documentation for the Autocomplete field of the Formik, Material UI, React tool here.

The example given in the documentation is:

import { Autocomplete } from 'formik-material-ui-lab';

const options = [{ title: 'The Shawshank Redemption', year: 1994 }, ...]

<Field
  name="name"
  component={Autocomplete}
  options={options}
  getOptionLabel={(option: Movie) => option.title}
  style={{ width: 300 }}
  renderInput={(params: AutocompleteRenderInputParams) => (
    <TextField
      {...params}
      error={touched['name'] && !!errors['name']}
      helperText={errors['name']}
      label="Autocomplete"
      variant="outlined"
    />
  )}
/>;

No clues are given as to the meaning of Movie where it is used in getOptionLabel. When I try to use this, Movie is underlined as is AutocompleteRenderInputParams in the renderInput object. I don't know why.

I have seen this post which tries an alternative approach, but I can't get that to work either.

I have a form, with two Autocomplete fields. Currently, it looks like this.

When I try to use the form, the submit button hangs and the console log says:

Material-UI: The getOptionLabel method of Autocomplete returned undefined instead of a string for "".

import React, { useState } from 'react';
import { Link  } from 'react-router-dom';
import firebase, {firestore} from '../../../firebase';
import { withStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Button from '@material-ui/core/Button';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';

import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import {
  Formik, Form, Field, ErrorMessage,
} from 'formik';
import * as Yup from 'yup';
import { Autocomplete, ToggleButtonGroup } from 'formik-material-ui-lab';
import { Switch } from 'formik-material-ui';


const styles = {

};

const allCategories = [
    {value: 'culture', label: 'Culture'},
    {value: 'other', label: 'Other'},
    
];

const sharingOptions = [
    {value: 'open', label: 'Openly'},
    
    {value: 'me', label: 'Only me'},
    
];

function Contact(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 an Impact Metric
        </Button>
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        {!isSubmitionCompleted &&
          <React.Fragment>
            <DialogTitle id="form-dialog-title">Create an Impact Metric</DialogTitle>
            <DialogContent>
              <DialogContentText>
                test form.
              </DialogContentText>
              <Formik
                initialValues={{ title: "", category: "",  sharing: "" }}
                
                onSubmit={(values, { setSubmitting }) => {
                   setSubmitting(true);
                   firestore.collection("testing").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'),
                  sharing: 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"
                            component={Autocomplete}
                            options={allCategories}
                            getOptionLabel={option => option.label}
                            style={{ width: 300 }}
                            renderInput={(params: AutocompleteRenderInputParams) => (
                            <TextField
                                {...params}
                                error={touched['category'] && !!errors['category']}
                                helperText={
                                touched['category'] && errors['category']
                                }
                                label="Select Category"
                                variant="outlined"
                            />
                            )}
                        />
                      </Box>

                      <Box margin={1}>
                        <Field
                            name="sharing"
                            component={Autocomplete}
                            options={sharingOptions}
                            getOptionLabel={option => option.label}

                            style={{ width: 300 }}
                            renderInput={(params: AutocompleteRenderInputParams) => (
                            <TextField
                                {...params}
                                error={touched['sharing'] && !!errors['sharing']}
                                helperText={
                                touched['sharing'] && errors['sharing']
                                }
                                label="Select Sharing Option"
                                variant="outlined"
                            />
                            )}
                        />
                      </Box>
                      <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>
                test
              </DialogContentText>
              <DialogActions>
                <Button
                  type="button"
                  className="outline"
                  onClick={handleClose}
                >
                  Close
                  </Button>
                {/* <DisplayFormikState {...props} /> */}
              </DialogActions>
            </DialogContent>
          </React.Fragment>}
      </Dialog>
    </React.Fragment>
  );
}

export default withStyles(styles)(Contact);

Can anyone see how to get the autocomplete working with formik, material ui in line with the documentation published at the link above?

I also tried using the regular select form input. This is the form field:

<Box margin={1}>
                        <Field
                          component={TextField}
                          type="text"
                          name="category"
                          label="Category"
                          select
                          variant="outlined"
                          helperText="Select a category"
                          margin="normal"
                          style={{ width: "100%"}}
                          InputLabelProps={{
                            shrink: true,
                          }}
                        >
                          {allCategories.map(option => (
                            <MenuItem key={option.value} value={option.value}>
                              {option.label}
                            </MenuItem>
                          ))}
                        </Field>

When I try this, I get a warning in the console that says:

instrument.ts:129 Material-UI: You have provided an out-of-range value `undefined` for the select component.
Consider providing a value that matches one of the available options or ''

This warning doesn't make any sense - the form renders with a menu correctly populated.

I also get an error that says:

index.js:1 Warning: A component is changing an uncontrolled input of type undefined to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info

In relation to that error, I have seen this post, which recommends using value (rather than input - which I do) and defining all the initial values as a type. For me, they are all strings, although I tried replacing the select fields with empty arrays. In both alternatives, the same error message is returned in the console.

At this point - I don't care which of autocomplete or select I use, I just want to get one of them working.

It is interesting that in both cases (using select and autocomplete) the console logs warnings that say:

Material-UI: You have provided an out-of-range value `undefined` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are `one`, `two`.
(anonymous) @ 0.chunk.js:141301
0.chunk.js:141301 Material-UI: You have provided an out-of-range value `undefined` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are `a`, `b`, `c`, `d`.

BUT, only one instance of the error that says:

 A component is changing an uncontrolled input of type undefined to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: react-website -controlled-components
    in input (created by ForwardRef(SelectInput))
    in ForwardRef(SelectInput) (created by ForwardRef(InputBase))
    in div (created by ForwardRef(InputBase))
    in ForwardRef(InputBase) (created by WithStyles(ForwardRef(InputBase)))
    in Wi

This error points to the category select form input.

I also tried adding the gender select form field from this code sandbox to my form to see if I could get this working. When I comment out the category and sharing fields described above, and add a gender field with a default value of an empty string, the form loads.

The field is:

<Field
                      name="gender"
                      label="Gender"
                      options={[
                        { value: "Male", label: "Male" },
                        { value: "Female", label: "Female" },
                        { value: "Other", label: "Other" }
                      ]}
                      component={Select}
                    />

The select field for gender appears but is about 1cm wide and the options menu does not populate with options, I can't select anything. BUT the form does load to firebase with an empty string in the gender field. That's progress but not enough to move forward.

The same code sandbox shows a field that uses Autocomplete. I tried to adapt it and use it in my form as follows:

<Field
                      name="gender"
                      label="Gender"
                      options={sharingOptions}
                      component={Autocomplete}
                      textFieldProps={{
                        label: sharingOptions.label
                      }}
                    />

When I try that, I get an error that says:

TypeError: renderInput is not a function

This error message makes no sense to me because I'm not using renderInput anywhere in the form.

When I try:

<Box margin={1}>
                        <Field
                          component={Select}
                          type="text"
                          name="category"
                          label="Impact Category"
                          select
                          variant="outlined"
                          helperText="Select a category"
                          margin="normal"
                          style={{ width: "100%"}}
                          InputLabelProps={{
                            shrink: true,
                          }}
                        >
                          {allCategories.map(option => (
                            <MenuItem key={option.value} value={option.value}>
                              {option.label}
                            </MenuItem>
                          ))}
                        </Field>
                      </Box>

I get no errors and can save the form with the option details. However, this does not actually solve the problem about why Autocomplete will not work. This is also not using the Select field as shown in the linked documentation. So I'm no clearer on why this works or why the method shown in the documentation does not work.

NEXT ATTEMPT

Using the autocomplete example in this codesandbox as a guide, I tried:

<Field
              name="autocomplete"
              multiple
              component={Autocomplete}
              options={sharingOptions}
              getOptionLabel={(option: any) => option.title}
              style={{width: 300}}
              renderInput={(params: AutocompleteRenderInputParams) => (
                <MuiTextField
                  {...params}
                  error={touched['autocomplete'] && !!errors['autocomplete']}
                  helperText={touched['autocomplete'] && errors['autocomplete']}
                  label="Autocomplete"
                  variant="outlined"
                />
              )}
            />

As with the earlier example, my code editor underlines the value "any" where it appears in getOptionLabel and it also underlines AutocompleteRenderInputParams. I can't find any documentation explaining what these elements of the form field mean or do. In any event, I have imported AutocompleteRenderInputParams as shown in the code sandbox.

I made the initial value of the autocomplete field in my form an empty array - although I note the code sandbox does not set an initial value in this example. When I try removing the initial value of autocomplete, I get the same errors as are generated when the initial value is an empty array, but I also get a warning in the console that says:

Warning: value for autocomplete is not an array, this can caused unexpected behaviour

When I try this code, my console logs the following errors:

TypeError: Cannot read property 'toLowerCase' of undefined

Material-UI: The getOptionLabel method of Autocomplete returned undefined instead of a string for {"value":"open","label":"Open "}.

Liripipe answered 8/8, 2020 at 7:42 Comment(1)
May you provide codesandbox.io sample?Sade
O
4

Working example:

Demo

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

import { Formik, Field } from "formik";
import { Autocomplete } from "formik-material-ui-lab";
import { TextField } from "@material-ui/core";

const options = [
  { title: "The Shawshank Redemption", year: 1994 },
  { title: "Inglourious Basterds", year: 2009 },
  { title: "Snatch", year: 2000 },
  { title: "3 Idiots", year: 2009 },
  { title: "Monty Python and the Holy Grail", year: 1975 }
];

function App() {
  return (
    <Formik
      initialValues={{
        autocomplete: null
      }}
    >
      {() => (
        <Field
          name="autocomplete"
          component={Autocomplete}
          options={options}
          getOptionLabel={(option) => option.title}
          style={{ width: 300 }}
          renderInput={(params) => (
            <TextField {...params} label="Autocomplete" variant="outlined" />
          )}
        />
      )}
    </Formik>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Oxbow answered 15/8, 2020 at 4:11 Comment(9)
Hi @Kuf. thank you for taking a look at this. When I load your demo and click in the Autocomplete field, I get an error that says: undefined is not an object (evaluating '(i === 0 ? obj : resVal)[pathArray[i]]')Liripipe
@Liripipe should be ok now, I added the initial stateOxbow
Thanks @Kuf. I can see that your demo renders the form properly. Is there a reason you didn't follow the documentation and use: AutocompleteRenderInputParams in the renderInput or the getOptionLabel as set out in the example? I'm trying to learn how to use the documentation as much as I'm trying to learn to use the tool. I can't make sense of the example.Liripipe
Do you mean why did I omit the Movie part? that's a static type checking that I omitted since I thought the focus here is about the code itself. you can learn more about it here in this linkOxbow
Thanks. Do you know what this bit is trying to do: AutocompleteRenderInputParams?Liripipe
@Liripipe for the sake of completeness, I've updated my demo to use typescript. as for your question, AutocompleteRenderInputParams is an interface - a typescript type definition.Oxbow
Thanks @Kuf. I really appreciate the help.Liripipe
sure thing, happy to help! you can learn more about interfaces here. lmk if you have more questionsOxbow
Hi @Oxbow - can you see what I'm doing wrong here: #63803858 Thank youLiripipe
L
1

For others who are stuck, this works - although I don't understand why. The underlined errors noted in the post above remain and I don't know how to solve for them.

Sharing this as a way forward - rather than a good solution.

import React, { useState } from 'react';
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 ToggleButton from '@material-ui/lab/ToggleButton';
import FormatAlignLeftIcon from '@material-ui/icons/FormatAlignLeft';
import FormatAlignCenterIcon from '@material-ui/icons/FormatAlignCenter';
import FormatAlignRightIcon from '@material-ui/icons/FormatAlignRight';
import FormatAlignJustifyIcon from '@material-ui/icons/FormatAlignJustify';

import {
  Formik, Form, Field, ErrorMessage,
} from 'formik';
import * as Yup from 'yup';
// import { Autocomplete, ToggleButtonGroup } from 'formik-material-ui-lab';
import {
  Autocomplete,
  ToggleButtonGroup,
  AutocompleteRenderInputParams,
} from 'formik-material-ui-lab';
import {
  fieldToTextField,
  TextFieldProps,
  Select,
  Switch,
} from 'formik-material-ui';




const allCategories = [
    
    {value: 'one', label: 'Col'},
    {value: 'two', label: 'Com'},
    
];


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 a Defined Term
        </Button>
      <Dialog
        open={open}
        onClose={handleClose}
        aria-labelledby="form-dialog-title"
      >
        {!isSubmitionCompleted &&
          <React.Fragment>
            <DialogTitle id="form-dialog-title">Create </DialogTitle>
            <DialogContent>
              <DialogContentText>
                
              </DialogContentText>
              <Formik
                initialValues={{ term: "",  definition: "",  category: [],  attribution: true, attributionRegion: '', context: "", relatedTerms: "", linkedTemplates: "", referenceMaterials: "" }}
                
                onSubmit={(values, { setSubmitting }) => {
                   setSubmitting(true);
                   firestore.collection("glossary").doc().set({
                    ...values,
                    createdAt: firebase.firestore.FieldValue.serverTimestamp()
                })
                
                  .then(() => {
                    setSubmitionCompleted(true);
                  });
                }}

                validationSchema={Yup.object().shape({
                  term: Yup.string()
                    .required('Required'),
                  definition: Yup.string()
                    .required('Required'),
                  category: Yup.string()
                    .required('Required'),
                  attribution: Yup.boolean()
                    .required('Required'),
                  context: 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="How is it 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}>
                        <Typography component="div" style={{ marginTop: "5vh", marginBottom: "5vh"}}>
                        Choose)?
                                <Grid component="label" container alignItems="center" spacing={1}>
                                <Grid item>Attribution</Grid>
                                <Grid item>
                                    <Field 
                                        component={Switch} 
                                        name="attribution"
                                        type="checkbox"
                                        
                                    >
                                    
                                    </Field>  
                                  </Grid>
                                <Grid item>Anonymous</Grid>
                                </Grid>
                            </Typography>
                        
                      </Box>
                      <Box margin={1}>
                        <Field
                          name="category"
                          multiple
                          component={Autocomplete}
                          options={allCategories}
                          getOptionLabel={(option: any) => option.label}
                          style={{width: 300}}
                          renderInput={(params: AutocompleteRenderInputParams) => (
                            <MuiTextField
                              {...params}
                              error={touched['autocomplete'] && !!errors['autocomplete']}
                              helperText={touched['autocomplete'] && errors['autocomplete']}
                              label="Category"
                              variant="outlined"
                            />
                          )}
                        />
                      </Box>                      
                      
                      <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>
Thank you              </DialogContentText>
              <DialogActions>
                <Button
                  type="button"
                  className="outline"
                  onClick={handleClose}
                >
                  Close
                  </Button>
                {/* <DisplayFormikState {...props} /> */}
              </DialogActions>
            </DialogContent>
          </React.Fragment>}
      </Dialog>
    </React.Fragment>
  );
}

export default withStyles(styles)(Glossary);
Liripipe answered 13/8, 2020 at 23:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.