I'm using the material ui autocomplete component but I noticed that when the chips (tags) are deleted directly from the (x) button on the chip the autocomplete's onchange function is not triggered. Any ideas how I can get the onchange to trigger when a tag is deleted directly from the chip component?
Bellow is my code:
My component which uses autocomplete
export default function FormSearchInput( props ) {
const classes = FormStyle();
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
return (
<Grid item xs = {props.xs} className = {classes.formItem}>
<Autocomplete
className = {props.className}
size = {props.size}
limitTags = {4}
multiple
options = {props.options}
disableCloseOnSelect
getOptionLabel = {( option ) => option}
defaultValue = {props.selectedOptions}
renderOption = {( option, {selected} ) => (
<React.Fragment>
<Checkbox
icon = {icon}
checkedIcon = {checkedIcon}
style = {{ marginRight: 8 }}
checked = {selected}
onChange = {props.onChange( option, selected )}
/>
{option}
</React.Fragment>
)}
renderInput = {( params ) => (
<TextField {...params} variant = "outlined" label = {props.label}/>
)}
/>
</Grid>
)
}
My onchange handler which i pass to my formsearchcomponent:
function handleCollaboratorsChange( option, selected ) {
console.log("triggering")
let tempCollaborators = collaborators
if( selected && !tempCollaborators.includes(option) ) {
// If collaborator not in list add to list
tempCollaborators.push( option )
} else if( !selected && tempCollaborators.includes(option) ) {
// If collaborator in list remove from list
tempCollaborators.splice( tempCollaborators.indexOf(option), 1 );
}
setCollaborators( tempCollaborators )
}
Capture the "reason" in the onchange.
In the following example, I've an autocomplete that allows the user to add new options and display them as chips.
// HANDLE: ON CHANGE
const on_change = (
event: React.SyntheticEvent,
newValue: any,
reason: string,
details: any,
) => {
const selected_item = newValue[newValue.length - 1]
switch (reason) {
case 'removeOption':
case 'remove-option':
if (details.option.name) {
// remove an existing option
remove_tag(details.option.name)
} else {
// remove a new created option
remove_tag(details.option.inputValue)
}
break
case 'createOption':
case 'selectOption':
if (typeof selected_item === 'string') {
if (can_add()) create_tag(newValue)
} else if (selected_item && selected_item.inputValue) {
// Create a new value from the user input
if (can_add()) create_tag(selected_item.inputValue)
} else {
if (can_add()) {
if (!tags.includes(selected_item)) set_tag([...tags, selected_item])
}
}
break
}
}
And define the component like this:
<Autocomplete
multiple={true}
autoHighlight={true}
limitTags={tags_limit}
id="cmb_tags"
options={full_tags}
getOptionLabel={on_get_option_label}
defaultValue={tags}
freeSolo
filterOptions={on_filter}
selectOnFocus
noOptionsText={i18n.t('sinopciones')}
onChange={on_change}
renderInput={(params) => (
<TextField
{...params}
variant="standard"
placeholder={placeholder}
/>
)}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox
icon={icon}
checkedIcon={checkedIcon}
style={{ marginRight: 8 }}
checked={selected}
/>
{option.name}
</li>
)}
/>
Let me know if this helps you.
Add the onChange property...
<Autocomplete
className = {props.className}
size = {props.size}
limitTags = {4}
multiple
options = {props.options}
disableCloseOnSelect
getOptionLabel = {( option ) => option}
defaultValue = {props.selectedOptions}
**onChange = {(event, newValue) => { handleCollaboratorsChange(newValue); }}**
renderOption = {( option, {selected} ) => (
Related
This is my current code, what I want here is after selecting on Select option (CHIP) and if the user type on the textfield I want to clear what the user selected on CHIP, What should I do to get what i want functionality?
const names = [
'Oliver Hansen',
'Van Henry',
'April Tucker',
];
function getStyles(name, personName, theme) {
return {
fontWeight:
personName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
export default function MultipleSelectChip() {
const theme = useTheme();
const [personName, setPersonName] = React.useState([]);
const handleChange = (event) => {
const {
target: { value },
} = event;
setPersonName(
// On autofill we get a stringified value.
typeof value === 'string' ? value.split(',') : value
);
};
const handleChangeTextField = (event) => {
setPersonName(null);
};
return (
<div>
<FormControl sx={{ m: 1, width: 300 }}>
<InputLabel id="demo-multiple-chip-label">Chip</InputLabel>
<Select
labelId="demo-multiple-chip-label"
id="demo-multiple-chip"
multiple
value={personName}
onChange={handleChange}
input={<OutlinedInput id="select-multiple-chip" label="Chip" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
MenuProps={MenuProps}
>
{names.map((name) => (
<MenuItem
key={name}
value={name}
style={getStyles(name, personName, theme)}
>
{name}
</MenuItem>
))}
</Select>
<TextField
variant="outlined"
label="Type anything to remove the value of Chip"
onChange={handleChangeTextField} />
</FormControl>
</div>
This is my current code, what I want here is after selecting on Select option (CHIP) and if the user type on the textfield I want to clear what the user selected on CHIP, What should I do to get what i want functionality?
I would set your textfield to be controlled (ie backed by a state variable) and add an effect hook to watch it.
When it receives a value, clear the selected names by setting personNames back to an empty array.
const [text, setText] = useState("");
useEffect(() => {
if (text) {
setPersonName([]);
}
}, [text]);
const handleChangeTextField = ({ target: { value } }) => {
setText(value);
};
<TextField
value={text}
variant="outlined"
label="Type anything to remove the value of Chip"
onChange={handleChangeTextField}
/>
You might also want to clear the text field when selecting names by adding this into handleChange...
setText("");
I noticed that "onAdd" property is removed from the updated version of Material-UI, MUI. The only property that is a function is "onDelete" and "onClick". I want to create new chips depending on user input tags. Is there any equivalent way of doing so?
You can use input fields to trigger the creation of new chips. This jsFiddle demo (click "Run" to start the demo) has a button that creates chips.
The important code in that demo relating to your question is below under index.jsx. The key items are:
createNewChip function,
chipName TextField, and
Create New Chip button.
The createNewChip function acts as an event listener for the onClick event of the Create New Chip button. createNewChip takes the text value from the chipName TextField and adds it to the list variable which is managed by React.useState.
list is an array of chip data where each element is an object that looks something like this:
{
id: '11bf5b37-e0b8-42e0-8dcf-dc8c4aefc000',
value: 'A Wonderful Chip',
isValid: true
}
Hopefully, this helps to get you started on a solution.
index.jsx
...
export default function ChipMaker() {
const [ list, setList ] = React.useState( [] );
const selectedItems = list.filter( ( item ) => item.isValid );
const selectedLengthIndex = selectedItems.length - 1;
let listById = {};
for ( let item of list ) {
listById[ item.id ] = item;
}
...
function createNewChip() {
let chipName = document.getElementById( 'chipName' ).value;
let newList = list.concat( {
id: uuidv4(),
value: chipName,
isValid: true
} );
setList( newList );
}
...
return (
<Stack spacing={3} sx={{ width: 500 }}>
<Autocomplete
multiple
id="tags-filled"
filterSelectedOptions={ true }
options={ list.map(( item ) => item.id) }
value={ list.map(( item ) => item.id) }
freeSolo
renderTags={( listIds, getTagProps) =>
listIds.map(( id, index) => (
<Chip
key={ index }
variant="outlined"
label={ listById[ id ].value }
sx={ {
color: ( theme ) => {
let chipColor = '#fff';
if ( typeof( listById[ id ] ) == 'object' ) {
chipColor = listById[ id ].isValid
? theme.palette.common.white
: theme.palette.common.white
}
return chipColor;
},
backgroundColor: ( theme ) => {
let chipColor = '#fff';
if ( typeof( listById[ id ] ) == 'object' ) {
chipColor = listById[ id ].isValid
? theme.palette.primary.main
: theme.palette.error.main
}
return chipColor;
},
[`& .MuiSvgIcon-root.MuiSvgIcon-fontSizeMedium.MuiChip-deleteIcon.MuiChip-deleteIconMedium.MuiChip-deleteIconColorDefault.MuiChip-deleteIconOutlinedColorDefault`]: {
fill: ( theme ) => theme.palette.grey[200]
}
} }
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => (
<TextField
{...params}
variant="filled"
label="Material-UI Chip Input Test"
placeholder="Favorites"
/>
)}
onChange={ validateInput }
/>
<div>
{ selectedItems.map( ( item, index ) => {
let comma = null;
if ( selectedLengthIndex != index ) {
comma = (<span key={ 'idx'+index }>, </span>);
}
return (
item.isValid
? <span key={ index }>{ item.value }{ comma }</span>
: null
);
} ) }
</div>
<TextField
id='chipName'
name='chipName'
className={ 'test' }
type='text'
label={ 'Chip name' }
fullWidth={ false }
variant='standard'
inputProps={ { maxLength: 20 } }
helperText={ 'Enter chip name' }
InputLabelProps={ {
'variant': 'standard',
'color': 'primary',
'disableAnimation': false
} }
FormHelperTextProps={ { 'variant': 'standard' } }
error={ false }
defaultValue={ '' }
placeholder={ 'New chip name' }
color={ 'info' }
/>
<Button
variant={ 'contained' }
onClick={ createNewChip }
sx={ {
width: '200px'
} }
>
{ 'Create new chip' }
</Button>
</Stack>
);
}
/**
* Inject component into DOM
*/
root.render(
<ChipMaker />
);
Actually, I've just come to find out myself that MUI has a component called, "Autocomplete" which is the equivalent of making new chips (and adding tags from user input) with the "ChipInput" component from the older material-UI.
This is the component for Dropdown:
<DropDownField
name={formElement.name}
label={formElement.name}
value={formik.values[formElement.name] || ''}
dropDownItems={formElement.name === 'Loan Details' ? loanEnum : formElement.enum}
className={classes.flexGrow}
onChange={dropDownChangeHandler}
error={formik.touched[formElement.name] && Boolean(formik.errors[formElement.name])}
helperText={formik.touched[formElement.name] && formik.errors[formElement.name]}
/>
and the DropDownField component:
<FormControl fullWidth>
{/* eslint-disable-next-line */}
<InputLabel id="simple-select-label">{props.label}</InputLabel>
<Select labelId="simple-select-label" id="simple-select" {...props}>
{/* eslint-disable-next-line */}
{props.dropDownItems.map((element, index) => (
<MenuItem key={element + index} value={element}>
{element}
</MenuItem>
))}
</Select>
</FormControl>
onChange function:
const dropDownChangeHandler = async (e) => {
const loanDetails = await LoanDetailsService.search(formik.values['Employee Number']);
formik.handleChange(e);
console.log(loanDetails);
formik.setFieldValue('Loan Type', loanDetails[index]['Loan Type']);
};
Now in the onChange function how do i get the index of the dropdown items?
I think you could try adding a data attribute to your menu items, e.g.:
{props.dropDownItems.map((element, index) => (
<MenuItem key={element + index} value={element} data-index={index}>
{element}
</MenuItem>
))}
And then getting that index from e.currentTarget.dataset.index or by grabbing the second argument (child) sent to the event handler as detailed in the onChange section on this page.
e.g. something like:
const dropDownChangeHandler = async (e) => {
const index = +e.currentTarget.dataset.index;
const loanDetails = await LoanDetailsService.search(formik.values['Employee Number']);
formik.handleChange(e);
console.log(loanDetails);
formik.setFieldValue('Loan Type', loanDetails[index]['Loan Type']);
};
or
const dropDownChangeHandler = async (e, child) => {
const index = child.props['data-index'];
const loanDetails = await LoanDetailsService.search(formik.values['Employee Number']);
formik.handleChange(e);
console.log(loanDetails);
formik.setFieldValue('Loan Type', loanDetails[index]['Loan Type']);
};
You are mapping over the elements, the map method gives you the indices of the element, you could bubble the index of the given selected element via handler using the onClick prop
{props.dropDownItems.map((element, index) => (
<MenuItem key={element + index} value={element} onClick={()=> handleItemClick(index)} >
{element}
</MenuItem>
))}
The handleItemClick code will be as in the following
const handleItemClick = (idx) => {
console.log(idx);
}
How to hide the lists of cities when the textfield is empty and when the user start typing the lists will show.
https://codesandbox.io/s/loving-platform-t2cyb?file=/src/LocationWidget.js
const openSearch = () => {
setViewLocationList(true);
startSearch();
};
const stopSearch = () => {
setSearchParameter('')
setViewLocationList(false);
};
<TextField
variant="outlined"
placeholder="Search Locations"
onFocus={openSearch}
onChange={filterResults}
value={searchParameter}
classes={{notchedOutline:classes.input}}
InputProps={{
endAdornment: (
<IconButton onClick={stopSearch} edge="end">
<ClearIcon />
</IconButton>
),
classes:{notchedOutline:classes.noBorder},
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Box>
<Collapse in={viewLocationList} sx={{ my: '2px' }}>
<Box className="rounded-scrollbar widget-result-container">
{filteredLocations.map((location, index) => (
<LocationWidgetItem
key={index}
location={location}
onClickLocation={setActiveLocation}
/>
))}
</Box>
</Collapse>
You have to take state variable in which you have to store whatever user is typing:
const [text,setText] = useState("");
and in your filterLocations you have to update its value
const filterLocations = (txt) => {
setText(txt.target.value);
let filteredLocations = locations.filter((e) =>
e.name.toLowerCase().includes(txt.target.value.toLowerCase())
);
setSearchParameter(filteredLocations);
};
And Finally in your render, render ul conditioanlly
{ !!text && <ul>
{searchParameter.map((location) => (
<li key={location.name}>{location.name}</li>
))}
</ul>}
https://codesandbox.io/s/dreamy-roentgen-0jnoi?file=/src/LocationWidget.js
please find below code.
export default function LocationWidget({ locations }) {
const [searchParameter, setSearchParameter] = useState(locations);
const [inputText, setInputText] = useState(null);
const filterLocations = (txt) => {
setInputText(txt.target.value);
let filteredLocations = locations.filter((e) =>
e.name.toLowerCase().includes(txt.target.value.toLowerCase())
);
setSearchParameter(filteredLocations);
};
return (
<>
<input type="text" onChange={filterLocations} />
{inputText && <ul>
{searchParameter.map((location) => (
<li key={location.name}>{location.name}</li>
))}
</ul>}
</>
);
}
You'd like to set an 'input' eventListener on the input field. And check if the inputField length > 0, then set the setViewLocationList(true); and if the inputField length === 0, then set the setViewLocationList(false).
I have a custom component that takes in values from an API and then displays them to a user, however within this component I give a 'required' flag to give an asterisk to the label, however I only want one field as seen below to have an asterisk not both as is currently happening.
<Grid item xs={12} sm={6}>
<SearchUsers
name="primaryOfficerId"
label="PO Responsible"
id="primaryOfficerId"
onSelect={change.bind(null, 'primaryOfficerId')}
error={touched.primaryOfficerId && Boolean(errors.primaryOfficerId)}
/>
</Grid>
<Grid item xs={12} sm={6}>
<SearchUsers
name="supportOfficerId"
label="Support Officer"
id="supportOfficerId"
onSelect={change.bind(null, 'supportOfficerId')}
/>
</Grid>
And now my custom component
const Search = (props) => {
const [data, setData] = useState([]);
const [select, setSelect] = useState(0);
const { type: TYPE, name: NAME, label: LABEL, onSelect, filter } = props;
const applyFilter = (data) => {
let result = data;
if (filter) {
result = filter(data);
}
return result;
};
useEffect(() => {
getLookupData(TYPE)
.then((response) => {
setData(response);
})
.catch((error) => {
if (error === HttpStatus.NOT_FOUND) {
setData([]);
} else {
throw error;
}
});
}, [TYPE]);
const options = applyFilter(data).map((item) => (
<MenuItem value={item.id} key={item.id}>
{item[NAME]}
</MenuItem>
));
const handleChange = (event) => {
setSelect(event.target.value);
onSelect && onSelect(event);
};
const { classes } = props;
return (
<FormControl required className={classes.formControl} id={NAME} error={props.error}>
<FormControlLabel control={<InputLabel htmlFor={NAME}>{LABEL}</InputLabel>} />
<Select
name={TYPE}
value={select}
onChange={handleChange}
disabled={props.disabled || options.length === 0}
input={<Input name={TYPE} id={NAME} />}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{options}
</Select>
</FormControl>
);
};
Below you can see an image of my problem, both PO, and So responsible have an asterisk. I need only PO to have this asterisk but my component does not currently allow for individuals
Simply make your component customizable by passing it a required prop as boolean then in your component make it dynamic :
<FormControl required={props.required}>
// ...
</FormControl>
So now you can use it with an asterisk <SearchUsers required /> or without <SearchUsers required={false} />
Add additional prop required to your Search component and then use it as a prop value for FormControl:
<FormControl required={props.required} />