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"}]
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;
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
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;
Related
I have an array of object that looks like this:
[
{
_id: "6311c197ec3dc8c083d6b632",
name: "Safety"
},
........
];
I load this array as potential Menu Items for my Select:
{categoryData &&
categoryData.map((cat: any) => (
<MenuItem key={cat._id} value={cat}>
<Checkbox
checked={categories.some((el: any) => el._id === cat._id)}
/>
<ListItemText primary={cat.name} />
</MenuItem>
))}
In my Select I have predefined value for it:
const [categories, setCategories] = useState([
{
name: "Safety",
_id: "6311c197ec3dc8c083d6b632"
}
]);
.......
<Select
labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox"
multiple
value={categories}
onChange={(event: any) => {
const {
target: { value }
} = event;
console.log(value);
setCategories(value);
}}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => selected.map((cat) => cat.name).join(", ")}
>
The problem is I am unable to unselect(de-select) the predefined value. In stead of removing it from array of categories I got it once again in it.
Here is the sandbox example:
https://codesandbox.io/s/recursing-river-1i5jw8?file=/src/Select.tsx:632-757
I understand that values has to be exactly equal to be removed but how I can do that? What is wrong with this kind of handling?
Also I found this case as reference but still couldn't do it as in the case they use formik:
Unselect MUI Multi select with initial value
You can't directly save the Object as the value. You must use a unique string or stringify the entire object and store it as the value. And based on that value calculate the selected value rendered text. Here is something that will work for you.
Changes: use _id as the value instead of the entire object. And added a new selected value renderer.
import {
FormControl,
Select,
MenuItem,
InputLabel,
Checkbox,
ListItemText,
OutlinedInput
} from "#mui/material";
import React, { useState, useMemo } from "react";
const categoryData = [
{
_id: "6311c197ec3dc8c083d6b632",
name: "Safety"
},
{
_id: "6311c8e6ec3dc8c083d6b63b",
name: "Environment"
},
];
const SelectForm = () => {
const [categories, setCategories] = useState(["6311c197ec3dc8c083d6b632"]);
const selectedCategories = useMemo(() => {
let value = "";
categoryData.forEach((cat) => {
if (categories.some((catId: any) => catId === cat._id)) {
if (value) {
value += ", " + cat.name;
} else {
value = cat.name;
}
}
});
return value;
}, [categories]);
return (
<FormControl fullWidth>
<InputLabel id="demo-multiple-checkbox-label">Category</InputLabel>
<Select
labelId="demo-multiple-checkbox-label"
id="demo-multiple-checkbox"
multiple
value={categories}
onChange={(event: any) => {
const {
target: { value }
} = event;
console.log(value);
setCategories(value);
}}
input={<OutlinedInput label="Tag" />}
renderValue={() => selectedCategories}
>
{categoryData &&
categoryData.map((cat: any) => (
<MenuItem key={cat._id} value={cat._id}>
<Checkbox
checked={categories.some((catId: any) => catId === cat._id)}
/>
<ListItemText primary={cat.name} />
</MenuItem>
))}
</Select>
</FormControl>
);
};
export default SelectForm;
Initially, you have to pass an empty array while setting the state. This will solve your problem.
Code changes will look like this -
const [categories, setCategories] = useState([]);
I want to use an MUI stepper to replace a Select component. The select component is used to indicate the status of the document the user is working in (New, In Progress, Complete, etc.). I have managed to display the correct status in the stepper, but I cannot interact with it to move the status forward or back.
This is my stepper file. I am passing the status value through props:
export default function IntakeStatusBar(props) {
const { status } = props;
const classes = useStyles();
const [activeStep, setActiveStep] = useState(0);
const steps = ["New", "In Progress", "Completed"];
useEffect(() => {
if (status === "In Progress") {
setActiveStep(1);
} else if (status === "Completed") {
setActiveStep(2);
} else setActiveStep(0);
}, [status, activeStep]);
const handleStep = (step) => () => {
setActiveStep(step);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label, index) => (
<Step key={label}>
<StepButton onClick={handleStep(index)}>{label}</StepButton>
</Step>
))}
</Stepper>
</div>
);
}
This is where I call and display the stepper:
export default function IntakeDetails() {
const [details, setDetails] = useState("");
const onTextChange = (e) => {
var id = e.target.id ? e?.target.id : e?.target.name;
var value = e.target.value;
setDetails({ ...details, [id]: value });
}
....
return (
<IntakeStatusBar status={details?.Status} onChange={onTextChange} />
// This is the Select drop down menu I have been using
<TextField
label="Status"
error={requiredField && details?.Status?.length <= 0}
value={details?.Status}
disabled={!(adminRole && isSolutionsTab && details?.Status !== "In Plan")}
select
onChange={onTextChange}
>
{details.StatusList?.map((choice) => {
return (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
);
})}
</TextField>
)
}
This is what the status field looks like in JSON:
{
Status: "New"
}
besides changing this:
<StepButton onClick={() => handleStep(index)}>{label}</StepButton>
you have to change this:
const handleStep = (step) => {
setActiveStep(step);
};
and set Stepper to nonLinear if you want user to click on steps:
<Stepper nonLinear activeStep={activeStep} alternativeLabel>
I also commented out useEffect since I had no idea what its purpose is and it's messing with activeStep state.
I have a react-select component (which is generated using CreatableSelect), it is a multi select text input which allows users to add keywords as options.
It's working fine but I need a way to allow users to copy some comma separated text and paste it into the component so that each item is going to be added as a single option.
For instance, if the text is "123,456,789", the expected output will be 3 individual options: 123, 456 and 789, respectively.
Here's my component
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
console.group('Value Changed');
console.log(value);
console.log(`action: ${actionMeta.action}`);
console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
if (props.value.map(v => v.label).includes(props.inputValue)) {
console.log('Value Already Exists!')
props.setInputValue('');
}
else {
console.group('Value Added');
console.log(props.value);
console.groupEnd();
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue)])
}
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={props.placeholder}
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I call this component from a page (of a next.js project) like below. The component modifies the state of the page in which it is called.
...
import MultiSelectTextInput from "../components/Form/MultiSelect/MultiSelectTextInput";
...
const NcciLite = () => {
const [value, setValue] = useState<any>([]);
const [inputValue, setInputValue] = useState<any>('');
...
return (
<React.Fragment>
<Container fluid={true}>
<Breadcrumbs title="Tools" breadcrumbItem="NCCI Lite" />
<Row>
<Col>
<Card>
<CardBody>
<CardTitle className="mb-4 fw-light">...</CardTitle>
<Form onSubmit={event => event.preventDefault()}>
...
<Row className="mb-4">
<Col>
<div className="d-inline-flex col-md-9">
<MultiSelectTextInput
id="codes"
value={value}
setValue={setValue}
inputValue={inputValue}
setInputValue={setInputValue}
placeholder="Type Next Code(s) or Paste Here"
/>
</div>
</Col>
</Row>
...
</Form>
</CardBody>
</Card>
</Col>
</Row>
</Container>
</React.Fragment>
};
export default NcciLite;
Any help would be appreciated
Allright, I will answer my own question. I thought it would be difficult but it was actually not. Just a few lines of javascript code.
Modified component
import React, {KeyboardEventHandler} from 'react';
import CreatableSelect from 'react-select/creatable';
import { ActionMeta, OnChangeValue } from 'react-select';
const MultiSelectTextInput = (props) => {
const components = {
DropdownIndicator: null,
};
interface Option {
readonly label: string;
readonly value: string;
}
const createOption = (label: string) => ({
label,
value: label,
});
const handleChange = (value: OnChangeValue<Option, true>, actionMeta: ActionMeta<Option>) => {
// console.group('Value Changed');
// console.log(value);
// console.log(`action: ${actionMeta.action}`);
// console.groupEnd();
props.setValue(value);
};
const handleInputChange = (inputValue: string) => {
props.setInputValue(inputValue);
};
const handleKeyDown: KeyboardEventHandler<HTMLDivElement> = (event) => {
if (!props.inputValue) return;
switch (event.key) {
case 'Enter':
case 'Tab':
if (props.value.map(v => v.label.trim()).includes(props.inputValue.trim())) {
// console.log('Value Already Exists!')
props.setInputValue('');
}
else {
// console.group('Value Added');
// console.log(props.value);
// console.groupEnd();
if (!props.customizedSetter && props.inputValue.trim().indexOf(',') > -1) {
props.setInputValue('');
const values = props.inputValue.trim().split(',').filter(iv => !props.value.map(v => v.label.trim()).includes(iv.trim()));
for (let i = 0; i < values.length; i++) {
props.setValue((oldValue) =>[...oldValue, createOption(values[i].trim())]);
}
}
else {
props.setInputValue('');
props.setValue([...props.value, createOption(props.inputValue.trim())]);
}
}
event.preventDefault();
}
};
return (
<CreatableSelect
id={props.id}
instanceId={props.id}
className="cs w-100"
components={components}
inputValue={props.inputValue}
isClearable
isMulti
menuIsOpen={false}
onChange={handleChange}
onInputChange={handleInputChange}
onKeyDown={handleKeyDown}
placeholder={props.placeholder}
value={props.value}
/>
);
};
export default MultiSelectTextInput;
I am setting up an admin portal, the admin portal can have an unlimited amount of steps input. Each step contains a title and a section of rich text in the form of an HTML string. At the moment my code works but I get this error when I use the arrows to increment the number of steps or if I input 10 or more steps into my input field.
Uncaught Error: You are passing the delta object from the onChange event back as value. You most probably want editor.getContents()
Looks like a React-Quill specific error but something makes me think the error is a side effect of bad code somewhere on my part.
Here is my code:
export const NewsArticlesPage = () => {
const [numberOfSteps, setNumberOfSteps] = useState(0)
//#ts-ignore
const StepsMap = Array.apply(null, { length: numberOfSteps })
let richText: any = { ...StepsMap }
let title: any = { ...StepsMap }
const updateTileArray = (index: number, titleEventData: string) => {
const titleData = { ...title }
title = { ...titleData, [index]: titleEventData }
console.log(title[index])
}
const updateRichTextArray = (index: number, richTextEventData: string) => {
const richTextData = { ...richText }
richText = { ...richTextData, [index]: richTextEventData }
console.log(richText[index])
}
const updateNewsArticleJson = () => {
console.log(richText)
console.log(title)
}
return (
<NewsArticlesWrapper>
<TextField
type="number"
label="Number of steps"
value={numberOfSteps}
InputProps={{ inputProps: { min: 0 } }}
onChange={(e) => setNumberOfSteps(Number(e.target.value))}
/>
{StepsMap.map((n, index) => (
<>
<Typography key={'heading' + index}>Step: {index + 1}</Typography>
<TextField
key={'title' + index}
type="text"
label="Title"
value={title[index]}
onChange={(titleEventData) =>
updateTileArray(index, titleEventData.target.value)
}
/>
<ReactQuill
key={'quil' + index}
theme="snow"
value={richText[index]}
modules={modules}
onChange={(richTextEventData) =>
updateRichTextArray(index, richTextEventData)
}
/>
</>
))}
<Button
variant="contained"
colour="primary"
size="medium"
onClick={updateNewsArticleJson}
>
Submit Article
</Button>
</NewsArticlesWrapper>
)
}
I understand the use of type any is bad but my priority is to get this working then I can add the correct types afterward.
i've been trying to update the object inside an array that respresents a react state, the object should be updated when the value of an input is changed, I could find a way myself to update it, but i'm not sure enough that it is the proper way to do it because when i open the react dev tools and go to the components tab and click the component that i'm working on, the state doesn't update immidiatly when typing in the input, and in order to see the change i have to click on another component in the dev tool and then go back to the first component and the change is done.
So I'm basically asking if the way i used to update the state is correct and to get some suggestions about better ways to do it so it updates instantly. Thanks
here is the code
the state:
const [items, setItems] = useState([{ name: "", quantity: "", unit: "" }]);
the change handling function (the function that updates the state):
const nameChange = (e, i) => {
const newItems = items;
newItems[i].name = e.target.value;
setItems(newItems);
console.log(items);
};
the inputs:
{
items.map((item, i) => {
return (
<div key={i} className={`mt3 ${classes.root}`}>
<TextField
onChange={e => nameChange(e, i)}
style={{ width: "30%" }}
id="standard-basic"
label="Item name"
/>
<TextField
style={{ width: "25%" }}
id="standard-basic"
label="quantity"
/>
<TextField
style={{ width: "10%" }}
id="standard-basic"
label="Unit"
/>
</div>
);
});
}
const [items, setItems] = React.useState({
name: '',
quantity: '',
unit: ''
});
const handleChange = prop => event => {
setItems({ ...items, [prop]: event.target.value });
};
on your TextFields:
<TextField onChange={handleChange('name')}/>
<TextField onChange={handleChange('quantity')}/>
Update: if items is an array:
const updateItem = (prop, event, index) => {
const old = items[index];
const updated = { ...old, [prop]: event.target.value }
const clone = [...items];
clone[index] = updated;
setItems(clone);
}
on your TextFields:
{items.map((item, i) => (
<div key={i}>
<TextField onChange={e => updateItem('name', e, i)}/>
<TextField onChange={e => updateItem('quantity', e, i)}/>
<TextField onChange={e => updateItem('unit', e, i)}/>
</div>
))}
I got the issue when state is object. I used redux and mapStateToProps and I can't listen the property in object change cause react can not check deeper.
Solution: follow code:
// in reducer file
case 'SAVE_SERVICE_STATE': {
return {
...state,
service: {...payload},
loading: false,
};
}
//listen service state change
useEffect(() => {
if (props.serviceState) {
// do something
}
}, [props.serviceState]);
hope can help someone.