MUI Chip: onAdd (Material-UI) equivalent function? - javascript

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.

Related

TextField loses focus after inserting one character

I have the following functions which display either a TextField or a Select component based on a JSON value being either Text or Select. When I try to type into a TextField, I can only enter one letter before focus is lost on the TextField. I can then click the TextField again and enter another letter before focus is lost again. There is also a noticeable delay between pressing a key and the corresponding symbol appearing in the TextField.
The complete code can be found here
This is my code of how I get my API response, useEffect, and my TextDetails function which displays the TextFields
const fetchDetails = async () => {
setBusy(true);
setDetails(await fetch(`/fiscalyears/FY2023/intakes/${params.id}/details`).then((response) => response.json()));
setBusy(false);
};
useEffect(() => {
fetchDetails();
}, []);
const TextDetails = ({ head, val, i }) => {
return (
<TextField
value={val || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[i] = {
...update[i],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
variant="outlined"
margin="normal"
label={head}
/>
);
};
const detailsComps = details["fields"]?.map((row, i) => {
return row["FieldType"] === "Text" ||
row["FieldType"] === "Decimal" ||
row["FieldType"] === "Number" ||
row["FieldType"] === "Date" ? (
<TextDetails head={row?.FieldName} val={row?.Value} i={i} />
) : (
<SelectDetails head={row.FieldName} val={row?.Value} choices={row?.Choices} i={i} />
);
});
...
return (
<Box>
{detailsComps}
</Box>
)
Sorry can't see the whole code. But please check whether the whole From is re-rendered every time you enter a value in the field. The reason could be that with setDetails you signalise React that details value has changed and because detailsComps renders from details it is highly likely the from is re-rendered. Hence, focused is lost.
I fixed this issue by moving this functionality into the functions return statement:
...
return (
<Box>
{details["fields"]?.map((row, index) => {
if (row?.FieldType === "Text" || row?.FieldType === "Decimal" || row?.FieldType === "Number") {
return (
<TextField
className="text-field"
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
label={row["FieldName"]}
/>
);
}
if (row?.FieldType === "Date") {
return (
<TextField
type="date"
label={row["FieldName"]}
InputLabelProps={{
shrink: true,
}}
/>
);
} else {
return (
<TextField
value={row?.Value || ""}
onChange={(e) => {
setDetails((prev) => {
const update = [...prev.fields];
update[index] = {
...update[index],
Value: e.target.value,
};
return { ...prev, fields: update };
});
}}
select
label={row?.FieldName}
>
{row?.Choices.map((choice) => (
<MenuItem key={choice} value={choice}>
{choice}
</MenuItem>
))}
</TextField>
);
}
})}
</Box>
)

Finding children elements that are missing unique keys? (reactJS)

I currently have this bit of code, that is working, but it is throwing a Warning: Each child in a list should have a unique "key" prop.
Check the render method of `TopMenuDropdown`. See https://reactjs.org/link/warning-keys for more information.
MenuItem#http://localhost:3000/protis-react/static/js/vendors~main.chunk.js:2815:110
TopMenuDropdown#http://localhost:3000/protis-react/static/js/main.chunk.js:2432:5
div
div
App#http://localhost:3000/protis-react/static/js/main.chunk.js:63:5
Router#http://localhost:3000/protis-react/static/js/vendors~main.chunk.js:174391:30
BrowserRouter#http://localhost:3000/protis-react/static/js/vendors~main.chunk.js:174011:35
at me. I am not even sure where to track down what is missing a key, or where I missed putting it in. Any way for me to track it, or is someone here able to find the missing elements that need the unique keys. I tried to put a key on almost anything I generated originally, but I'm not sure what went wrong where, and this has been bothering me.
import React from 'react';
import {Menu, MenuButton, MenuDivider, MenuItem, SubMenu} from '#szhsin/react-menu';
import '#szhsin/react-menu/dist/index.css'
const tooltipStyle = {
position: "absolute",
pointerEvents: "none",
backgroundColor: "#D7D7A6",
border: "1px solid",
padding: "1px 8px",
whiteSpace: "nowrap",
zIndex: 200
};
class TopMenuDropdown extends React.Component {
state = {
pos: {x: 0, y: 0},
tooltip: '',
show: false
};
createTooltip = (tooltip) => ({
onMouseEnter: ({clientX, clientY}) => {
this.setState(
{
pos: {x: clientX, y: clientY},
tooltip: tooltip,
show: true
}
);
},
onMouseLeave: () => {
this.setState(
{
show: false
}
)
}
});
handleSelect = ({value}) => {
this.props.onClick(value);
}
renderMenuItem(menuAnchor, menuItems, isSubMenu) {
if (isSubMenu) {
return (
<SubMenu key={menuAnchor.id}
label={
<div
key={menuAnchor.id}
{...this.createTooltip(menuAnchor.attributes.title)}
>
{menuAnchor['#text']}
</div>
}
>
{this.menuGeneration(menuItems, menuAnchor)}
</SubMenu>
);
}
return (
<>
<Menu
style={{display: 'flex', float: 'left'}}
key={menuAnchor.id}
menuButton={
<MenuButton
key={menuAnchor.id}
{...this.createTooltip(menuAnchor.attributes.title)}>
{menuAnchor['#text']}
</MenuButton>}
onChange={({open}) => !open && this.setState({show: false})}
>
{this.menuGeneration(menuItems, menuAnchor)}
</Menu>
{this.state.show && (
<div
key={menuAnchor.id}
style={{
...tooltipStyle,
left: this.state.pos.x,
top: this.state.pos.y
}}
>
{this.state.tooltip}
</div>
)}
</>
);
}
menuGeneration(menuItems, menuAnchor) {
if (menuItems === undefined) {
return <></>;
}
if (!Array.isArray(menuItems)) {
menuItems = [menuItems];
}
return (
menuItems.map(({a, attributes, ul}) => {
if (ul !== undefined && ul.li !== undefined) {
return (
this.renderMenuItem(a, ul.li, true)
);
}
if (a === undefined) {
return (
<MenuDivider key={menuAnchor.id} />
)
}
return (
<MenuItem
key={menuAnchor.id}
value={a.attributes.id}
onClick={(id) => this.handleSelect(id)}
{...this.createTooltip(a.attributes.title)}
>
{a['#text']}
</MenuItem>)
}
)
)
}
render() {
if (!this.props.topMenu.hasOwnProperty('ul')) {
return null;
}
const menuItemRendering = this.props.topMenu.ul.li.map(({a, ul}) => {
return this.renderMenuItem(a, ul.li, false);
});
return (
<div style={{display: 'flex'}}>
{menuItemRendering}
</div>
)
}
}
export default TopMenuDropdown;
Issue is in renderMenuItem in the "else" branch when you render Menu and a div into a Fragment. When mapping JSX a React key needs to be on the outermost returned mapped element, the Fragment in this case.
renderMenuItem(menuAnchor, menuItems, isSubMenu) {
if (isSubMenu) {
return (
<SubMenu
...
>
{this.menuGeneration(menuItems, menuAnchor)}
</SubMenu>
);
}
return (
<Fragment key={menuAnchor.id}> // <-- add missing React key here
<Menu
...
>
{this.menuGeneration(menuItems, menuAnchor)}
</Menu>
{this.state.show && (
<div
...
>
{this.state.tooltip}
</div>
)}
</Fragment>
);
}
Try replacing key={menuAnchor.id} with key={a.id} for items directly in menuGeneration:
if (a === undefined) {
return <MenuDivider key={a.id} />
}
return (
<MenuItem
key={a.id}
value={a.attributes.id}
onClick={(id) => this.handleSelect(id)}
{...this.createTooltip(a.attributes.title)}
>
{a['#text']}
</MenuItem>
)

MUI autocomplete onchange not triggering on chip delete

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} ) => (

Retrieving the key for the entry from material-ui autocomplete at onSelect, instead of value

I'm using React with Material-ui and the Autocomplete component documented here - https://material-ui.com/components/autocomplete/ with downshift.
<Downshift id="downshift-options">
{({
clearSelection,
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
highlightedIndex,
inputValue,
isOpen,
openMenu,
selectedItem,
}) => {
const {onSelect, onBlur, onChange, onFocus, ...inputProps} = getInputProps({
onChange: event => {
if (event.target.value === '') {
clearSelection();
}
},
onSelect: event => {
if (event.target.id) {
this.props.onSelect(event.target.value);
}
},
onFocus: openMenu,
placeholder: 'Type to search',
});
return (
<div className={classes.container}>
{renderInput({
fullWidth: true,
classes,
label: "Assigned Rider",
InputLabelProps: getLabelProps({shrink: true}),
InputProps: {onBlur, onChange, onFocus, onSelect},
inputProps,
})}
<div {...getMenuProps()}>
{isOpen ? (
<Paper className={classes.paper} square>
{getSuggestions(this.props.suggestions, inputValue, {showEmpty: true}).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({item: suggestion.label}),
highlightedIndex,
selectedItem,
}),
)}
</Paper>
) : null}
</div>
</div>
);
}}
</Downshift>
With onSelect, I can retrieve the value of the selection. I'd like to be able to retrieve the key instead.
function renderSuggestion(suggestionProps) {
const { suggestion, index, itemProps, highlightedIndex, selectedItem } = suggestionProps;
const isHighlighted = highlightedIndex === index;
const isSelected = (selectedItem || '').indexOf(suggestion.label) > -1;
return (
<MenuItem
{...itemProps}
key={suggestion.uuid}
value={suggestion.uuid}
selected={isHighlighted}
component="div"
style={{
fontWeight: isSelected ? 500 : 400,
}}
>
{suggestion.label}
</MenuItem>
);
}
Here I set the uuid as the key for each selection.
My ultimate aim is to be able to make a selection and retrieve a uuid instead of the value itself.
Although I can use the value returned to match against a list of items, I want to be sure that if there end up being any duplicate entries, it doesn't cause problems.
Link to my full code for the component is here - https://github.com/theocranmore/bloodbike/blob/master/react_app/src/components/UsersSelect.js#L143
Thank you!
I don't know why you need an id but for my own getting the object itself will suffice.
<Autocomplete .....
onChange={(event, newValue) => {
console.log(newValue); //this will give you the selected value dictionary (source)
}}
You can retrieve the entire value and then access your desired key (option.id):
const options = [
{ id: 0, value: "foo" },
{ id: 1, value: "goo" },
];
<Autocomplete
options={options}
onChange={(event, option) => {
console.log(option.id); // 1
}}
renderInput={(params) => <TextField {...params} />}
/>;

after entering the value in the textbox if I hit enter, should see a new chip with the entered value in it

when I click textbox text a menu opens, in that textbox I need to enter some value in the textbox.
after entering the value in the textbox if I hit enter, I should see a new chip with the entered value in it.
so I thought I will create an Onchange event and pass the values to the chip component
but the values are not getting passed.
I think I need to pass values an object to that array.
chipData.push(handleTextBox);
console.log("handleTextBox after push chipData--->", chipData);
can you tell me how to fix it.
providing my code snippet and sandbox below.
https://codesandbox.io/s/material-demo-99sf8
demo.js
const [chipData, setChipData] = React.useState([
{ key: 0, label: "Angular" },
{ key: 1, label: "jQuery" },
{ key: 2, label: "Polymer" },
{ key: 3, label: "React" },
{ key: 4, label: "Vue.js" }
]);
const [hideChip, setHideChip] = React.useState([false]);
const handleClick = event => {
setAnchorEl({ type: "icon", target: event.currentTarget });
};
const handleClickFilter = event => {
setAnchorEl({ type: "textbox", target: event.currentTarget });
};
const handleTextBox = event => {
console.log("handleTextBox event--->", event);
console.log("handleTextBox event.target.value--->", event.target.value);
};
console.log("handleTextBox handleTextBox--->", handleTextBox);
console.log("handleTextBox chipData--->", chipData);
chipData.push(handleTextBox);
console.log("handleTextBox after push chipData--->", chipData);
<Menu
id="simple-menu"
anchorEl={
anchorEl && anchorEl.type === "textbox" && anchorEl.target
}
open={Boolean(anchorEl && anchorEl.type === "textbox")}
onClose={handleClose}
>
<MenuItem>
<form
className={classes.container}
noValidate
autoComplete="off"
>
<TextField
id="standard-name"
label="Name"
className={classes.textField}
onChange={handleTextBox}
// value={values.name}
// onChange={handleChange('name')}
margin="normal"
/>
</form>
</MenuItem>
</Menu>
<Paper className={classes.root}>
{chipData.map(data => {
let icon;
console.log("setHideChip--->", setHideChip);
if (data.label === "React") {
icon = <TagFacesIcon />;
}
return (
<Chip
key={data.key}
icon={icon}
label={data.label}
onDelete={handleDelete(data)}
className={classes.chip}
/>
);
})}
</Paper>
try with this code .
<TextField
fullWidth={true}
id="standard-name"
label="Tags"
className={classes.textField}
onChange={handleChange('tag')}
onKeyDown={(e) => pushToarray(e)}
margin="dense"
/>
on entering you should see tag being added
function pushToarray(e){
if(e.key == 'Enter'){
e.preventDefault();
setValues({ tags : [...values.tags ,e.target.value] ,
tag : ''})
}
}
and normal handleChange of text change in the box :
const handleChange = name => event => {
setValues({ ...values, [name]: event.target.value });
console.log(values)
};
Initialize state like this I have initialized with empty array you can use keys:
const [values, setValues] = React.useState({
tag :'',
tags: [],
})
here is your handleDelete function to remove chips :
function handleDelete(item){
var rm = values.tags.indexOf(item);
values.tags.splice( rm, 1)
setValues( { tags : values.tags})
}
this is how your chip component should be
<Paper className={classes.root}>
{
values.tags && values.tags.map( (tag ,id) =>(
<Chip color="default" className={classes.chip} onDelete={() => handleDelete(tag)} key={id} label={tag}/>
))
}

Categories

Resources