I want to be able to not allow a user to add an input value to Autocomplete's values array if an error exists with that value, but I also would like to preserve the input value currently being typed. Is there a way to cancel the event?
<Autocomplete
multiple
options={[]}
freeSolo
onChange={(event, value, reason) => {
if (reason === 'create-option' && error) {
// prevent this event from going through
}
}}
...
/>
Appreciate the help!
This isn't a solution to the question asked above per se, but solves my problem.
const [inputValue, setInputValue] = useState('');
return (
<Autocomplete
multiple
options={[]}
freeSolo
renderTags={(values: string[], getTagProps) => {
return values.map((option: string, index: number) => {
if (!option) return;
return (
<Chip
variant="outlined"
label={option}
{...getTagProps({ index })}
/>
);
});
}}
inputValue={inputValue}
onChange={(event: any, values: any, reason: string) => {
if (reason === 'create-option' && form.getState().errors.verses) {
setInputValue(values[values.length - 1]);
values[values.length - 1] = null;
} else {
setInputValue('');
}
}}
/>
If you set inputValue on Autocomplete and then set the persisted value to null, you can preserve the input value without saving the undesired value so the user won't have to start typing all over again. If the value is valid, then set the input value to empty string.
Related
I have built MUI grouped labels, but the moment I try to add the element into the state, the application crashes. Here's the minimal code.
const options = labels.map(option => {
return {
type: JSON.parse(localStorage.getItem("recentTags") as string).includes(option) ? "RECENT" : "ALL ",
labelText: option
};
});
<Autocomplete
multiple
disableClearable
filterSelectedOptions
groupBy={option => option.type}
isOptionEqualToValue={(option, value) => {
const labelText = value.labelText ? value.labelText : value;
return option.labelText.toUpperCase() === labelText.toUpperCase();
}}
value={value}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(_event, newValue) => {
console.log(newValue.map(el => el));
const latestLabel: any = newValue.slice(-1).pop();
const prevLabels = JSON.parse(localStorage.getItem("recentTags") as string);
if (!prevLabels.includes(latestLabel.labelText) && prevLabels.length < 5) {
localStorage.setItem("recentTags", JSON.stringify([...prevLabels, latestLabel.labelText]));
}
const newLabel = newValue.filter((x) => !labels.includes(x))[0];
setValue(newValue);
onSaveEcardLabels!(newValue, ecardItem.id);
if (!!newLabel) {
labels.push(newLabel.labelText);
}
}}
/>
I am storing recently used tags in localstorage, but I am confused in it's logic as well, I am not able to replace any newly recently used tag in local storage. I believe the problem lies in onChange event.
While onChange in newValue I get previous index and current Object, I would like only single array with index i.e. labelText.
The problem, when filtering a specific value from my Autocomplete, it indeed filters it but also the search capability no longer works on the autocomplete.
Here is the code:
<Autocomplete
fullWidth
// needed to remove mui error related to groupBy
options={sortedDimensionTypes}
// filter out the empty option
filterOptions={(options) => options.filter((option) => option.id !== "-1")}
getOptionLabel={(option) => option?.data?.label}
isOptionEqualToValue={(option, value) => option?.id === value?.id}
groupBy={(option) => optionsGroupLabel(option.data.type)}
size="small"
onChange={(_, newValue, reason) => {
switch (reason) {
case "clear":
setFilters((prevFilters) => {
const newFilters = prevFilters.slice();
newFilters.splice(currentIndex, 1, getBaseAttributionFilter());
return newFilters;
});
handleSelectOption(getBaseMetadataOption());
break;
case "selectOption":
setFilters((prevFilters) => {
const newFilters = prevFilters.slice();
const fltr = filterBase(newValue as MetadataOption);
newFilters.splice(currentIndex, 1, fltr);
return newFilters;
});
handleSelectOption(newValue as MetadataOption);
break;
}
}}
openOnFocus
// disable clearable when no option selected.
disableClearable={selectedType.id === "-1"}
renderInput={(params: AutocompleteRenderInputParams) => (
<TextField {...params} label={attributionText.DIMENSION_TYPE} variant="outlined" margin="dense" />
)}
value={selectedType}
/>
Expected behavior: it would simply filter out the option without an id (where the id equals -1) and allow searching on the rest.
Actual Behavior: typing a search does nothing.
Here is a simple recreation with a basic example from the MUI examples.
https://stackblitz.com/edit/react-yzmpeh?file=demo.tsx
I have an asynchronous Autocomplete component that works fine so far.
Hopefully the simplified code is understandable enough:
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<T[]>();
const onSearch = (search: string) => {
fetchOptions(search).then(setOptions);
};
return (
<Autocomplete<T>
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(event, value) => {
props.onChange(value as T);
}}
getOptionSelected={props.getOptionSelected}
getOptionLabel={props.getOptionLabel}
options={options}
value={(props.value as NonNullable<T>) || undefined}
renderInput={(params) => (
<TextField
{...params}
onChange={(event) => onSearch(event.currentTarget.value)}
/>
)}
/>
);
}
The component above works easily: when the user clicks on the input, the Autocomplete component displays an empty input field where the user can type in a value to search for. After the input has changed, the options are refetched to show matching results.
Now I want to add support for shortcodes: when the user types qq, the search term should be replaced by something, just like if the user would have typed something himself.
However, I found no way to update the value of the rendered TextField programmatically. Even if I set value directly on the TextField, it won't show my value but only the users input.
So, any ideas how to solve this problem?
Thank you very much.
What I've tried so far was to simply update the input within onKeyUp:
// ...
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
const value = event.currentTarget.value;
if(value === 'qq') {
event.currentTarget.value = 'something';
}
},
}}
/>
)}
With the code above I can see the something for a short time, but it gets replaced by the initial user input very soon.
Autocomplete is useful for setting the value of a single-line textbox in one of two types of scenarios: combobox and free solo.
combobox - The value for the textbox must be chosen from a predefined set.
You are using it so it not allowing you to add free text (onblur it replaced)
Answer: To take control of get and set value programmatically.
you need a state variable.
Check here codesandbox code sample taken from official doc
Your code with my comment:-
export function AsyncAutocomplete<T>(props: AsyncAutocompleteProps<T>) {
... //code removed for brevity
//This is a state variable to get and set text value programmatically.
const [value, setValue] = React.useState({name: (props.value as NonNullable<T>) || undefined});
return (
<Autocomplete<T>
... //code removed for brevity
//set value
value={value}
//get value
onChange={(event, newValue) => setValue(newValue)}
renderInput={(params) => (
<TextInput
{...params}
label={props.label}
onChange={(event) => onSearchChange(event.currentTarget.value)}
InputProps={{
...params.InputProps,
onKeyUp: (event) => {
//get value
const value = event.currentTarget.value;
//if qq then set 'something'
if (value === "qq") {
setValue({ name: "something" });
}
//otherwise set user free input text
else {
setValue({ name: value });
}
},
}}
/>
)}
/>
);
}
I am using mui checkbox component and this is how I'm displaying it:
<FormControl component="fieldset">
<FormGroup aria-label="position" column>
{sortedData.map((obj) => {
return (
<FormControlLabel
value="type"
control={<Checkbox color="primary"/>}
label={obj}
name={obj}
onChange={handleTypeCheckboxChange}
labelPlacement="end"
/>
);
})}
</FormGroup>
</FormControl>
Now on this function handleTypeCheckboxChange I want to add or remove the values of the checkboxes when the user checks or unchecks.
This is how I tried to do but it isn't working. It gives me NaN:
const [typeValue, setTypeValue] = useState([]) // the state array where I wanna store the checkbox checked values
const handleTypeCheckboxChange = (event) => {
let typeValuesArray = []
let index
if (event.target.checked) {
setTypeCheckboxCount(prevState => prevState + 1)
typeValuesArray.push(+(event.target.name))
} else {
setTypeCheckboxCount(prevState => prevState - 1)
index = typeValuesArray.indexOf(+(event.target.name))
typeValuesArray.splice(index, 1)
}
setTypeValue(typeValuesArray)
}
How can I resolve this?
Remove the + sign from push and indexOf methods argument.
Because:
+'anystring' = NaN
(Posted solution on behalf of the question author, to move the answer to the answer space).
This was solved. I changed my code to this:
const handleTypeCheckboxChange = (event, index) => {
if (event.target.checked) {
setTypeCheckboxCount(prevState => prevState + 1)
setTypeValue([...typeValue, event.target.name])
} else {
setTypeCheckboxCount(prevState => prevState - 1)
setTypeValue(
typeValue.filter( v => v !== event.target.name)
)
}
}
When a users drags and drops an excel or csv file on me I am dynamically creating material-ui chips from the columns of that file. I am showing them the columns and allowing the user to click the column they want then I build list for them based on the data that is associated with that column. Everything is working correctly except for when the user clicks the chip I want the chip to turn green and add a checkmark to indicate that is the chip selected. They can select many chips so if they select another one I want that chip to turn green. They can also hit the x and deselect the chip at which point i want the chip to go back to the original color and check to go away.
return (
<div>
{
props.dropdownkeys.map((value, index) => (
<Chip label={value}
icon={<CheckIcon key={index} id={`check_${value}`} color='primary' visibility={clickedChip ? 'visible' : 'hidden'}/>}
key={index}
onClick={() => addChip(value, index)}
onDelete={() => deselectChip(value, index)}
size='small'
id={`chip_${value}`}
ref={setChipRef}
className={clickedChip ? classes.chipSelected : classes.chip }
/>
))
}
</div>
);
this is my dynamically created chips i am trying to trigger based off a useState hook to change clickedchip
const [clickedChip, setClickedChip] = useState(false);
this is my addchip functionality
function addChip(value: number | string, index: number) {
debugger;
const chipClicked = getClickedChip(index);
if (chipClicked !== null) {
// chipClicked[0].style.backgroundColor = 'green';
// (chipClicked[0].firstChild as HTMLElement).style.color = 'red';
// (chipClicked[0].firstChild as HTMLElement).attributes[6].value = 'visible';
}
const getCols = props.excelDocumentObj.filter((column) => {
if (column[value] !== undefined) {return column[value];} else { return null;}
});
if (props.storeString !== '') {
props.setStoreString(',');
}
const rowLen = getCols.length;
for (const [i, storeNum] of getCols.entries()) {
if(typeof storeNum[value] === 'number') {
if(rowLen !== i + 1) {
props.setStoreString(`${storeNum[value] },`);
}
else {
props.setStoreString(`${storeNum[value]}`);
}
}
}
creatorStore.handleStoreListFromExcel(props.storeString);
}
the problem i ma having I am changing all of the chips in the renderer not the selected chip like I want. I do not want to access the chip through the dom because basically I was told not to do this on my pull request. I am new to react so I am having issues doing this simple task and any help would be appreciated.
i fixed this instead of using a boolean value i set an active index value like this
const [activeIndex, setActiveIndex] = useState(-1);
then in my render I changed it to this
return (
<div>
{
props.dropdownkeys.map((value, index) => (
<Chip label={value}
icon={<CheckIcon key={index} id={`check_${value}`} color='primary' visibility={index === activeIndex ? 'visible' : 'hidden'}/>}
key={index}
onClick={() => addChip(value, index)}
onDelete={() => deselectChip(value, index)}
size='small'
id={`chip_${value}`}
ref={setChipRef}
className={index === activeIndex ? classes.chipSelected : classes.chip }
/>
))
}
</div>
);
then in my addclick I called the function to update
function addChip(value: number | string, index: number) {
debugger;
setClickedChip(index, true);
const getCols = props.excelDocumentObj.filter((column) => {
if (column[value] !== undefined) {return column[value];} else { return null;}
});
if (props.storeString !== '') {
props.setStoreString(',');
}
const rowLen = getCols.length;
for (const [i, storeNum] of getCols.entries()) {
if(typeof storeNum[value] === 'number') {
if(rowLen !== i + 1) {
props.setStoreString(`${storeNum[value] },`);
}
else {
props.setStoreString(`${storeNum[value]}`);
}
}
}
creatorStore.handleStoreListFromExcel(props.storeString);
}
which in turn calls this function
function setClickedChip(index: number, value: boolean) {
if (value) {
setActiveIndex(index);
}
else {
setActiveIndex(-1);
}
}
then my deslect is this
function deselectChip(value: number | string, index: number) {
setClickedChip(index, false);
let deleteString = props.storeString;
const getCols = props.excelDocumentObj.filter((column) => {
if (column[value] !== undefined) {return column[value];} else {return null;}
});
for(const column of getCols) {
deleteString = removeValue(deleteString, column[value], ',');
}
props.deleteFromStoreString(deleteString);
creatorStore.handleStoreListFromExcel(props.storeString);
}
It appears that the easiest way to solve this problem would be to add a property to the props.dropdownkeys called "selected". This would be a Boolean. Note you may have to add this wherever “props.dropdownkeys” is defined in the parent component.
Then your code would look like the following:
return (
<div>
{
props.dropdownkeys.map((value, index) => (
<Chip label={value}
icon={<CheckIcon key={index} id={`check_${value}`} color='primary' visibility={value.selected ? 'visible' : 'hidden'}/>}
key={index}
onClick={() => addChip(value, index)}
onDelete={() => deselectChip(value, index)}
size='small'
id={`chip_${value}`}
ref={setChipRef}
className={value.selected ? classes.chipSelected : classes.chip }
/>
))
}
</div>
);
Note that with this approach you will need to add a function to toggle the selected property in the parent component where you initialized the props.dropdownkeys. Assuming you call it toggleChip(), you can then call this with the onClick event handler. Something like
onClick={() => props.toggleChip(value, index)}
You can also change your clickedChip state variable to be an array that is mapped to each chip with values of either true or false.
There is a logical error here. You are using one boolean variable (clickedChip) for all the chips. So, if any one chip is selected, clickedChip becomes true and className={clickedChip ? classes.chipSelected : classes.chip} becomes true for all.
You need to know uniquely which chip was selected.
So, what you need to do is:
- Initially, initialize clickedChip to null
- In your clickedChip variable, store the unique value (such as some id or key) related to a particular chip only
- In your className or icon condition, match that particular chip's unique value to the saved value and apply the logic to show green or making icon visible
- On clicking cross or deselecting, set clickedChip to null
The code will transform something like this (note: I am not exactly aware of the hooks syntax so have explained by setState):
state = { clickedChip: null }
setClickedChip = (uniqueChipId) => {
this.setState({ clickedChip: uniqueChipId })
}
render() {
...
return (
<div>
{
props.dropdownkeys.map((value, index) => (
<Chip label={value}
icon={<CheckIcon key={index} id={`check_${value}`} color='primary' visibility={clickedChip === `chip_${value}` ? 'visible' : 'hidden'}/>}
key={index}
onClick={() => addChip(value, index)}
onDelete={() => deselectChip(value, index)}
size='small'
id={`chip_${value}`}
ref={setChipRef}
className={clickedChip === `chip_${value}` ? classes.chipSelected : classes.chip }
/>
))
}
</div>
);
...
chip_${value} and uniqueChipId are the unique chip Ids
Hope it helps. Revert for any doubts/clarifications.