Dean
Dean

Reputation: 517

MaterialUI Compoent does not AutoComplete when data is available

I can't get my component to show my autosuggestions. It is observed in the console that my data is available and I sent it to this component using the suggestions prop, using Material UI AutoComplete component feature here I am trying to set my options, and these are changing as I type as it's handled in a parent component, but setting the values does not seem to reflect nor bring up my suggestions. I am very confused. my code is below.

import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "@material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "@material-ui/lab/Autocomplete";

type CreateProjectFormProps = {
    model: CreateProjectModel;
    errors: ErrorModel<CreateProjectModel>;
    onChange: (changes: Partial<CreateProjectModel>) => void;
    onSubmit?: () => Promise<void>;
    suggestions: JobModel[];
};

const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
    model,
    errors,
    onChange,
    onSubmit,
    suggestions,
}) => {
    const [open, setOpen] = React.useState(false);
    const [options, setOptions] = React.useState<JobModel[]>([]);
    const loading = open && options.length === 0;
    const [inputValue, setInputValue] = React.useState('');
    React.useEffect(() => {
        let active = true;

        if (!loading) {
            return undefined;
        }

        (async () => {
            if (active) {
                setOptions(suggestions);
            }
        })();

        return () => {
            active = false;
        };
    }, [loading]);

    React.useEffect(() => {
        if (!open) {
            setOptions([]);
        }
    }, [open]);

    const submit = async (event: FormEvent) => {
        event.preventDefault();
        event.stopPropagation();

        await onSubmit();
    };

    const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
        setInputValue(event.target.value);

        onChange({
            [name]: event.target.value,
        });
    };

    const getFieldProps = (id: string, label: string) => {
        return {
            id,
            label,
            helperText: errors[id],
            error: Boolean(errors[id]),
            value: model[id],
            onChange: change(id),
        };
    };

    return (
        <Autocomplete
            {...getFieldProps}
            open={open}
            onOpen={() => {
                setOpen(true);
            }}
            onClose={() => {
                setOpen(false);
            }}
            getOptionSelected={(option, value) => option.id === value.id}
            getOptionLabel={(option) => option.id}
            options={options}
            loading={loading}
            autoComplete
            includeInputInList            
            renderInput={(params) => (
                <TextField
                    {...getFieldProps("jobNumber", "Job number")}
                    required
                    fullWidth
                    autoFocus
                    margin="normal"
                />
            )}
            renderOption={(option) => {        
                return (
                  <Grid container alignItems="center">

                    <Grid item xs>
                      {options.map((part, index) => (
                        <span key={index}>
                          {part.id}
                        </span>
                      ))}
                      <Typography variant="body2" color="textSecondary">
                        {option.name}
                      </Typography>
                    </Grid>
                  </Grid>
                );
              }}            
        />
    );
};

export default CreateProjectForm;

Example of my data in suggestions look like this:

[{"id":"BR00001","name":"Aircrew - Standby at home base"},{"id":"BR00695","name":"National Waste"},{"id":"BR00777B","name":"Airly Monitor Site 2018"},{"id":"BR00852A","name":"Cracow Mine"},{"id":"BR00972","name":"Toowoomba Updated"},{"id":"BR01023A","name":"TMRGT Mackay Bee Creek"},{"id":"BR01081","name":"Newman Pilot Job (WA)"},{"id":"BR01147","name":"Lake Vermont Monthly 2019"},{"id":"BR01158","name":"Callide Mine Monthly Survey 2019"},{"id":"BR01182","name":"Lake Vermont Quarterly 2019 April"}]

Upvotes: 6

Views: 557

Answers (3)

user3366943
user3366943

Reputation: 263

i noticed a few issues with your code, getFieldProps is being called without the id or name params which cause the page to not load. More importantly, you should consider following the autocomplete docs when passing and using props to it. for example:

renderInput={(params) => <TextField {...params} label="Controllable" variant="outlined" />}

i asked a few questions, pls let me know when you can get those answers so i may address all the issues that may come up.

Q1. should the user input provide relevant matches from the name property in your suggestions or just the id? for ex. if i type "lake", do you want to show BRO1182, Lake Vermont Quarterly 2019 April as a match?

Q2. how did you want to address the error case? i see you have a error model, but unsure how you wish to use it to style the autocomplete when an error occurs

Q3. are we missing a submit button? i see the onSubmit function but it's not used in our code.

Q4. is there a particular reason why you need the open and loading states?

below is what i attempted so far to show related matches from user input

import React, { FunctionComponent, FormEvent, ChangeEvent } from "react";
import { Grid, TextField, Typography } from "@material-ui/core";
import { CreateProjectModel, JobModel } from "~/Models/Projects";
import ErrorModel from "~/Models/ErrorModel";
import Autocomplete from "@material-ui/lab/Autocomplete";

type CreateProjectFormProps = {
  model: CreateProjectModel;
  errors: ErrorModel<CreateProjectModel>;
  onChange: (changes: Partial<CreateProjectModel>) => void;
  onSubmit?: () => Promise<void>;
  suggestions: JobModel[];
};

const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
  model,
  errors,
  // mock function for testing
  // consider a better name like selectChangeHandler
  onChange = val => console.log(val),
  // consider a better name like submitJobFormHandler
  onSubmit,
  suggestions: options = [
    { id: "BR00001", name: "Aircrew - Standby at home base" },
    { id: "BR00695", name: "National Waste" },
    { id: "BR00777B", name: "Airly Monitor Site 2018" },
    { id: "BR00852A", name: "Cracow Mine" },
    { id: "BR00972", name: "Toowoomba Updated" },
    { id: "BR01023A", name: "TMRGT Mackay Bee Creek" },
    { id: "BR01081", name: "Newman Pilot Job (WA)" },
    { id: "BR01147", name: "Lake Vermont Monthly 2019" },
    { id: "BR01158", name: "Callide Mine Monthly Survey 2019" },
    { id: "BR01182", name: "Lake Vermont Quarterly 2019 April" }
  ]
}) => {
  const [value, setValue] = React.useState<JobModel>({});
  const loading = open && options.length === 0;

  // this pc of code is not used, why?
  const submit = async (event: FormEvent) => {
    event.preventDefault();
    event.stopPropagation();

    await onSubmit();
  };

  const handleChange = (_: any, value: JobModel | null) => {
    setValue(value);

    onChange({
      [value.name]: value.id
    });
  };

  // consider passing in props instead
  const getFieldProps = (id: string, label: string) => {
    return {
      id,
      label,
      // not sure what this is
      helperText: errors[id],
      // not sure what this is
      error: Boolean(errors[id]),
      value: model[id],
      onChange: change(id)
    };
  };

  return (
    <Autocomplete
      id="placeholder-autocomplete-input-id"
      // for selection, use value see docs for more detail
      value={value}
      onChange={handleChange}
      getOptionSelected={(option, value) => option.id === value.id}
      getOptionLabel={option => option.id}
      options={options}
      loading={loading}
      autoComplete
      includeInputInList
      renderInput={params => (
        // spreading the params here will transfer native input attributes from autocomplete
        <TextField
          {...params}
          label="placeholder"
          required
          fullWidth
          autoFocus
          margin="normal"
        />
      )}
      renderOption={option => (
        <Grid container alignItems="center">
          <Grid item xs>
            <span key={option}>{option.id}</span>
            <Typography variant="body2" color="textSecondary">
              {option.name}
            </Typography>
          </Grid>
        </Grid>
      )}
    />
  );
};

export default CreateProjectForm;

and you can see the code running in my codesandbox by clicking the button below

Edit sharp-newton-87eng

Upvotes: 1

Hagai Harari
Hagai Harari

Reputation: 2877

If I understand your code and issue right, you want -


    React.useEffect(() => {
      let active = true;

      if (!loading) {
        return undefined;
      }

      (async () => {
         if (active) {
             setOptions(suggestions);
          }
       })(); 

      return () => {
         active = false;
      };
   }, [loading]);

to run each time and update options, but the thing is, [loading] dependency setted like


     const loading = open && suggestions.length === 0;

and not gonna trigger changes.

Consider doing it like so -


 const loading = useLoading({open, suggestions})


 const useLoading = ({open, suggestions}) => open && suggestions.length === 0;

Upvotes: 0

Shubham Khatri
Shubham Khatri

Reputation: 281626

The problem in your code are the useEffects that you use.

In the below useEffect, you are actually setting the options to an empty array initially. That is because you autocomplete is not open and the effect runs on initial mount too. Also since you are setting options in another useEffect the only time your code is supposed to work is when loading state updates and you haven't opened the autocomplete dropdown.

The moment you close it even once, the state is updated back to empty and you won't see suggestions any longer.

React.useEffect(() => {
    if (!open) {
        setOptions([]);
    }
}, [open]);

The solution is simple. You don't need to keep a local state for options but use the values coming in from props which is suggestions

You only need to keep a state for open

const CreateProjectForm: FunctionComponent<CreateProjectFormProps> = ({
    model,
    errors,
    onChange,
    onSubmit,
    suggestions,
}) => {
    const [open, setOpen] = React.useState(false);
    const loading = open && suggestions.length === 0;
    const [inputValue, setInputValue] = React.useState('');

    const submit = async (event: FormEvent) => {
        event.preventDefault();
        event.stopPropagation();

        await onSubmit();
    };

    const change = (name: string) => (event: ChangeEvent<HTMLInputElement>) => {
        setInputValue(event.target.value);

        onChange({
            [name]: event.target.value,
        });
    };

    const getFieldProps = (id: string, label: string) => {
        return {
            id,
            label,
            helperText: errors[id],
            error: Boolean(errors[id]),
            value: model[id],
            onChange: change(id),
        };
    };

    return (
        <Autocomplete
            {...getFieldProps}
            open={open}
            onOpen={() => {
                setOpen(true);
            }}
            onClose={() => {
                setOpen(false);
            }}
            getOptionSelected={(option, value) => option.id === value.id}
            getOptionLabel={(option) => option.id}
            options={suggestions}
            loading={loading}
            autoComplete
            includeInputInList            
            renderInput={(params) => (
                <TextField
                    {...getFieldProps("jobNumber", "Job number")}
                    required
                    fullWidth
                    autoFocus
                    margin="normal"
                />
            )}
            renderOption={(option) => {        
                return (
                  <Grid container alignItems="center">

                    <Grid item xs>
                      {options.map((part, index) => (
                        <span key={index}>
                          {part.id}
                        </span>
                      ))}
                      <Typography variant="body2" color="textSecondary">
                        {option.name}
                      </Typography>
                    </Grid>
                  </Grid>
                );
              }}            
        />
    );
};

export default CreateProjectForm;

Upvotes: 4

Related Questions