I have the following component for selecting roles:
export const MultipleSelectChip = ({
options,
label,
error,
onRolesUpdate,
}: Props) => {
const theme = useTheme();
const [selectedOptions, setSelectedOptions] = React.useState<string[]>([]);
const handleChipChange = (
event: SelectChangeEvent<typeof selectedOptions>,
) => {
const {
target: { value },
} = event;
setSelectedOptions(
// On autofill we get a the stringified value.
typeof value === 'string' ? value.split(',') : value,
);
};
return (
<div>
<FormControl sx={{ m: 1, width: 300 }}>
<InputLabel id="multiple-chip-label">{label}</InputLabel>
<Select
required
labelId="multiple-chip-label"
error={error}
id="demo-multiple-chip"
multiple
value={selectedOptions}
onChange={handleChipChange}
input={<OutlinedInput id="select-multiple-chip" label={label} />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
MenuProps={MenuProps}
>
{options.map((propOption) => (
<MenuItem
key={propOption.id}
value={propOption.name}
style={getStyles(propOption, selectedOptions, theme)}
>
{propOption.name}
</MenuItem>
))}
</Select>
<FormHelperText>Here's my helper text</FormHelperText>
</FormControl>
</div>
);
};
For options I have an array of objects with id and name, the thing is that I want to use the names for displaying the chips and the ids to pass them to the parent component for the add request. I don't know how to get de ids, too.
This is the example: https://codesandbox.io/s/6ry5y?file=/demo.tsx but is using an array of strings instead of an array of objects.
This is how 'options' looks like:
const rolesDummy: Role[] = [
{ id: '61fb0f25-34aa-46c6-8683-093254223dcd', name: 'HR' },
{ id: '949b9b1e-d3f8-45cb-a061-08da483bd486', name: 'Interviewer' },
{ id: 'c09ae2d4-1335-4ef0-8d4b-ee9529796b52', name: 'Hiring Manager' },
];
And I need to get back only the selected ids
Thank you!
If you pass the option as an object, you can render each MenuItem with the option.id as a key and the option.name as the label. The MenuItem is identified by an id:
<Select {...}>
{options.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.name}
</MenuItem>
))}
</Select>
To display the name in the selected Chip. Use renderValue, but it only provides you the selected values (array of option.id), so you need to find the option to get the name:
renderValue={(selected) => {
return (
<Box>
{selected.map((value) => {
const option = options.find((o) => o.id === value);
return <Chip key={value} label={option.name} />;
})}
</Box>
);
}}
Now you can get an array of selected ids by adding a change handler:
onChange={e => console.log(e.target.value)}
Related
I have a case where the I have 3 items, and at in the case where is the first item, it should be displaying only the first item, and not allow the user to select 2nd and 3rd, but in case wher isItFirt = false then the user should be able to choose from the list. I wrote the minimal reproducible example as shown below:
import * as React from "react";
import {
Typography,
Button,
Dialog,
Box,
Select,
InputLabel,
FormControl,
MenuItem,
SelectChangeEvent
} from "#mui/material";
enum MyOptions {
FIRST = 1,
SECOND = 2,
THIRD = 3
}
export default function App() {
const [open, setOpen] = React.useState(true);
const [myOptions, setMyOptions] = React.useState(MyOptions.SECOND as number);
const handleChange = (event: SelectChangeEvent) => {
let nr = parseInt(event.target.value, 10);
setMyOptions(nr);
};
const isItFirst: boolean = false;
const handleClose = () => {
setOpen(false);
};
const somethingHappens = () => {
console.log("clicked: ", myOptions);
setOpen(false);
};
React.useEffect(() => {
if (isItFirst) {
setMyOptions(MyOptions.FIRST as number);
}
}, [isItFirst]);
return (
<div>
<Button
variant="contained"
size="small"
onClick={() => {
setOpen(true);
}}
>
Display dialog
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box>
<Typography id="modal-modal-title" variant="h6" component="h4">
Select one of the options
</Typography>
<FormControl>
<InputLabel id="1">Options</InputLabel>
<Select
labelId=""
id=""
value={myOptions}
label="Options"
onChange={(e: any) => handleChange(e)}
>
{isItFirst ? (
<MenuItem value={MyOptions.FIRST}>This is first</MenuItem>
) : (
<div>
<MenuItem value={MyOptions.SECOND} key={MyOptions.SECOND}>
This is second
</MenuItem>
<MenuItem value={MyOptions.THIRD} key={MyOptions.THIRD}>
This is third
</MenuItem>
</div>
)}
</Select>
</FormControl>
</Box>
<Button
variant="contained"
size="small"
onClick={() => {
somethingHappens();
}}
>
Select
</Button>
</Dialog>
</div>
);
}
This is the error output:
MUI: You have provided an out-of-range value `1` for the select component.
Consider providing a value that matches one of the available options or ''.
The available values are "".
And this is the dialog box that is shown in the case when isItFirst === false, I do not understand why it is shown as blank when I set the state of myOptions with the help of useEffect.
According to this document for children prop of Select
The option elements to populate the select with. Can be some MenuItem when native is false and option when native is true.
⚠️The MenuItem elements must be direct descendants when native is false.
So technically, we cannot pass div or any other elements to wrap MenuItem.
For the fix, you can consider to use filter and map with a pre-defined option like below
const options: {value: MyOptions, label: string}[] = [
{value: MyOptions.FIRST, label: "This is first"},
{value: MyOptions.SECOND, label: "This is second"},
{value: MyOptions.THIRD, label: "This is thrid"}
]
Here is how we apply options to Select
<Select
labelId=""
id=""
value={myOptions}
label="Options"
onChange={handleChange}
key="first-select"
>
{options
.filter((option) =>
isItFirst
? option.value === MyOptions.FIRST
: option.value !== MyOptions.FIRST
)
.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</Select>
I have the component below where I'm trying the build functionality to allow a user to update opening times of a store.
I'm passing the original opening times as a prop and creating some state using the props opening times for initial state. I want to use the new state to submit changes but if the user selects cancel the UI updates to reflect the original times.
I have most of the functionality working but for some reason my handler to update the state with the input change also seems to update the props value so it won't go back to the original value.
How can I stop the props updating and ensure only the allOpeningHours state is changed?
VIDEO: https://www.veed.io/view/c40bf9e8-7502-408a-ba6d-fd306dbf4b6f?sharingWidget=true
const EditStudioHours: FC<{ studio: Studio }> = ({ studio }) => {
const { value: edit, toggle: toggleEdit } = useBoolean(false)
const { value: submitting, toggle: toggleSubmitting } = useBoolean(false)
const [allOpeningHours, setAllOpeningHours] = useState([
...studio.openingHours.regularDays,
])
return (
<Box>
<Typography variant='h6' mt={2} gutterBottom>
Set standard hours
</Typography>
<Typography fontWeight='light' fontSize={14}>
Configure the standard operating hours of this studio
</Typography>
<Stack mt={3} spacing={2}>
{studio.openingHours.regularDays.map((hours, i) => (
<DayOfWeek
dow={daysOfWeek[i]}
openingHours={hours}
edit={edit}
i={i}
setAllOpeningHours={setAllOpeningHours}
allOpeningHours={allOpeningHours}
/>
))}
</Stack>
<Button
variant={edit ? 'contained' : 'outlined'}
onClick={() => {
toggleEdit()
}}
fullWidth
sx={{ mt: 2 }}
disabled={!edit ? false : submitting}
>
{submitting ? (
<CircularProgress size={22} />
) : edit ? (
'Submit changes'
) : (
'Edit'
)}
</Button>
{edit && (
<Button
onClick={toggleEdit}
variant={'outlined'}
sx={{ mt: 1 }}
fullWidth
>
Cancel
</Button>
)}
</Box>
)
}
export default EditStudioHours
const DayOfWeek: FC<{
openingHours: { start: number; end: number }
dow: string
edit: boolean
i: number
setAllOpeningHours: any
allOpeningHours: any
}> = ({ openingHours, dow, edit, i, setAllOpeningHours, allOpeningHours }) => {
const [open, setOpen] = useState(openingHours.end !== openingHours.start)
const handleOpenClose = () => {
open &&
setAllOpeningHours((ps: any) => {
const newHours = [...ps]
newHours[i].start = 0
newHours[i].end = 0
return newHours
})
setOpen((ps) => !ps)
}
const handleStart = (e: any) => {
setAllOpeningHours((prevState: any) => {
const newHours = [...prevState]
newHours[i].start = e.target.value
return newHours
})
}
const handleEnd = (e: any) => {
setAllOpeningHours((ps: any) => {
const newHours = [...ps]
newHours[i].end = e.target.value
return newHours
})
}
return (
<Box display='flex' alignItems='center' justifyContent={'space-between'}>
<Box display={'flex'} alignItems='center'>
<Typography width={150}>{dow}</Typography>
<FormGroup>
<FormControlLabel
control={
<Switch
disabled={!edit}
checked={open}
onChange={handleOpenClose}
/>
}
label='Open'
/>
</FormGroup>
</Box>
{open && (
<Box display={'flex'} alignItems='center'>
<TextField
disabled={!edit}
id={`${i}open`}
select
label='Open'
value={edit ? allOpeningHours[i].start : openingHours.start}
type='number'
sx={{ minWidth: 120 }}
size='small'
onChange={handleStart}
>
{openingOptions.map((option: { value: number; label: string }) => (
<MenuItem dense key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
<Typography mx={2}>TO</Typography>
<TextField
disabled={!edit}
id={`${i}close`}
select
label='Close'
value={edit ? allOpeningHours[i].end : openingHours.end}
type='number'
sx={{ minWidth: 120 }}
size='small'
onChange={handleEnd}
>
{openingOptions.map((option: { value: number; label: string }) => (
<MenuItem dense key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</TextField>
</Box>
)}
</Box>
)
}
The problem is that objects within the studio.openingHours.regularDays array still share the same reference, even though you copied the array itself.
When you use something like
newHours[i].start = e.target.value
You're still updating the original objects from props.
You can use Array.prototype.splice() to remove the object at index i and replace it with a new one
const day = newHours[i];
newHours.splice(i, 1, {
...day,
start: e.target.value,
});
Do this in each of your 3 handle functions.
Alternately, break all references when creating local state from props
const [allOpeningHours, setAllOpeningHours] = useState(
studio.openingHours.regularDays.map((day) => ({ ...day }))
);
I am trying to populate an MUI select with choices from an API's JSON response. Currently, all Choices are being pushed into one MenuItem within the Select.
The Choices are likely to change in the future so I would like to avoid hard coding them.
How can I apply a .map to get the MenuItemsto display the JSON Choices separately rather than having them display on the same line.
Here is my JSON, and below is how I am displaying all other data.
{
"question_groups": [
{
"GroupName": "DDD",
"questions": [
{
"Question": "1. Do you want a Drink?",
"QuestionType": "Single Choice",
"Response": null,
"Choices": [
"Yes",
"No"
]
}
],
"SizingId": null
},
}
const SelectQuestion = ({ question }) => {
return (
<Box>
<TextField value={question?.Question || ""} />
<Select label="Question" >
<MenuItem value={question?.Choices || ""}>{question?.Choices || ""}</MenuItem>
</Select>
<Divider />
</Box>
);
};
const TextQuestion = ({ question }) => {
return (
<Box>
<TextField />
<TextField />
<Divider />
</Box>
);
};
const questionComps = questions["question_groups"]?.map((group, i) => {
return group["questions"]?.map((question, i) => {
return question["QuestionType"] === "Text" ? (
<TextQuestion key={`${i}${question.Question}`} question={question} />
) : (
<SelectQuestion key={`${i}${question.Question}`} question={question} />
);
});
});
You should change your SelectQuestion component by mapping through your "Choices" options and render MenuItem-s accordingly.
const SelectQuestion = ({ question }) => {
return (
<Box>
<TextField value={question?.Question || ""} />
<Select label="Question">
{question.Choices.map((choice) => (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
))}
</Select>
<Divider />
</Box>
);
};
Demo
I'm trying to create a Material-UI Autocomplete component that essentially just displays search results to the user. Some of the options' names will be duplicates, but they will all have unique IDs. I receive the following warning:
index.js:1 Warning: Encountered two children with the same key, Name B. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and/or omitted — the behavior is unsupported and could change in a future version.
const SearchField = () => {
const [open, setOpen] = React.useState(false)
const [searchQuery, setSearchQuery] = React.useState('')
const [searchResults, setSearchResults] = React.useState([])
const loading = true //later
const debounced = useDebouncedCallback(
async searchQuery => {
if (searchQuery) {
let result = await doSearch(searchQuery)
if (result.status === 200) {
setSearchResults(result.data)
} else {
console.error(result)
}
}
},
1000
)
const handleInputChange = e => {
if (e.target.value && e.target.value !== searchQuery) {
debounced(e.target.value)
setSearchQuery(e.target.value)
}
}
const options = [{
name: 'Name A',
id: 'entry_0597856'
},{
name: 'Name B',
id: 'entry_3049854'
},{
name: 'Name B',
id: 'entry_3794654'
},{
name: 'Name C',
id: 'entry_9087345'
}]
return (
<Autocomplete
id='search_freesolo'
freeSolo
selectOnFocus
clearOnBlur
handleHomeEndKeys
autoHighlight
onInputChange={handleInputChange}
open={true}
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
loading={loading}
key={option => option.id}
options={options}
getOptionLabel={option => option.name}
renderOption={(props, option) => (
<Box
component='li'
{...props}
>
{option.name}
</Box>
)}
renderInput={params => {
return (
<TextField
{...params}
required
id="search_bar"
label="Search"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress size={18} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
}
/>
)
}
You can define your own renderOption that can return the list item with a correct key value. Your code complains about the duplicated keys because by default, Autocomplete uses the getOptionLabel(option) to retrieve the key:
<Autocomplete
renderOption={(props, option) => {
return (
<li {...props} key={option.id}>
{option.name}
</li>
);
}}
renderInput={(params) => <TextField {...params} label="Movie" />}
/>
If it still doesn't work, check your props order, you need to declare the key prop last, if you put it before the props provided by the callback:
<Box component='li' key={key} {...props}
Then it will be overridden by the props.key from MUI. It should be like this:
<Box component='li' {...props} key={key}
Live Demo
When working with HTML select in React, we tend to use an id or key to track the value selected:
<select value={value} onChange={(event) => setValue(event.target.value)}>
{options.map((option) => (
<option value={option.id}>{option.label}</option>
))}
</select>
I wonder if we can do the same with Material-ui Autocompelete component since in its demo, the value set in state is the whole object instead of the object id.
I tried using its APIs in the following way which make sense to me but it doesn't work as expected:
const fruits = [
{ id: 0, label: "apple" },
{ id: 1, label: "banana" },
{ id: 2, label: "cherries" },
{ id: 3, label: "fig" }
];
function FruitPicker() {
const [value, setValue] = useState(null);
return (
<Autocomplete
id="fruit-picker"
value={value}
onChange={(event, option) => {
setValue(option?.id || null);
}}
options={fruits}
getOptionLabel={(option) => option.label}
getOptionSelected={(option) => option.id === value}
renderInput={(params) => <TextField {...params} label="Fruit" />}
openOnFocus
/>
);
}
I had created this Codesandbox if you want to play around. Thanks.
This is the method that I used.
<Autocomplete
options={fruits}
value={fruits.filter(el => el.id === currentValue)[0]}
getOptionLabel={option => option.label}
onChange={(event, option) => { setValue(option?.id || null); }}
/>
Because you pass the options of array object, so when set value onChange, you must still keep setValue(option), but on getOptionSelected, compare their ids instead
<Autocomplete
value={value}
onChange={(event, option) => {
setValue(option);
}}
options={fruits}
getOptionLabel={(option) => option.label}
getOptionSelected={(option) => option.id === value.id}
renderInput={(params) => <TextField {...params} label="Fruit" />}
openOnFocus
/>