Does this jsFiddle demo meet your requirements? (Click "Run" to start the demo.) If not, please let me know what is missing.
Notes
The demo accepts only comma-separated integers (or a single integer) as input. No spaces are permitted.
Duplicate values are displayed with a red chip and not included in the result set. So far duplicate values are the only validation criterion causing values to fail where the failure is shown to the user via red chips.
Non numerical input fails silently.
index.html
<!DOCTYPE html>
<html>
<head>
<title>Material-UI Chip Input Field Test</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="testContainer" class="testContainer"></div>
<script src="./index.js"></script>
<noscript> Please enable javascript to view the site </noscript>
</body>
</html>
index.jsx
import * as React from 'react';
import { createRoot } from 'react-dom/client';
const container = document.getElementById( 'testContainer' );
const root = createRoot( container );
import Chip from '@mui/material/Chip';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import { v4 as uuidv4 } from 'uuid';
/**
* Sorting and comparator functions adapted from Material-UI "Sorting & selecting"
* Table component demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/tables/EnhancedTable.tsx
*
*/
function descendingComparator( a, b, orderBy ) {
if ( typeof( a[ orderBy ] ) == 'undefined' ) {
a[ orderBy ] = 0;
}
if ( typeof( b[ orderBy ] ) == 'undefined' ) {
b[ orderBy ] = 0;
}
if ( parseInt( b[ orderBy ] ) < parseInt( a[ orderBy ] ) ) {
return -1;
}
if ( parseInt( b[ orderBy ] ) > parseInt( a[ orderBy ] ) ) {
return 1;
}
return 0;
}
/**
* Sorting and comparator functions adapted from Material-UI "Sorting & selecting"
* Table component demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/tables/EnhancedTable.tsx
*
*/
function getComparator( order, orderBy ) {
return order === 'desc'
? ( a, b ) => descendingComparator( a, b, orderBy )
: ( a, b ) => -descendingComparator( a, b, orderBy );
}
/**
* Sorting and comparator functions adapted from Material-UI "Sorting & selecting"
* Table component demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/tables/EnhancedTable.tsx
*
*/
function stableSort( array, comparator ) {
const stabilizedThis = array.map( ( el, index ) => [ el, index ] );
stabilizedThis.sort( ( a, b ) => {
const order = comparator( a[ 0 ], b[ 0 ] );
if ( order !== 0 ) {
return order;
}
return a[ 1 ] - b[ 1 ];
});
return stabilizedThis.map( ( el ) => el[ 0 ] );
}
/**
* Sorting and comparator functions adapted from Material-UI "Sorting & selecting"
* Table component demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/tables/EnhancedTable.tsx
*
*/
const getSortedListAsArray = ( list ) => {
let comparator = getComparator( 'asc', 'value' );
let sortedReminders = stableSort( list, comparator );
return sortedReminders;
}
export default function Tags() {
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;
}
/**
* Sorting and comparator functions adapted from Material-UI "Sorting & selecting"
* Table component demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/tables/EnhancedTable.tsx
*
*/
function updateList( items ) {
const selectedValues = list.map( ( item ) => item.value );
let newSelected = list.slice();
for ( let item of items ) {
let index = parseInt( item.value );
let selectedIndex = -1;
if ( typeof( list ) != 'undefined' && list != null ) {
selectedIndex = selectedValues.indexOf( index );
}
if ( selectedIndex === -1 ) {
newSelected = newSelected.concat( item );
} else if ( selectedIndex === 0 ) {
newSelected = newSelected.concat( newSelected.slice( 1 ) );
} else if ( selectedIndex === list.length - 1 ) {
newSelected = newSelected.concat( newSelected.slice( 0, -1 ) );
} else if ( selectedIndex > 0 ) {
newSelected = newSelected.concat(
newSelected.slice( 0, selectedIndex ),
newSelected.slice( selectedIndex + 1 )
);
}
}
newSelected = getSortedListAsArray( newSelected );
setList( newSelected );
}
function validateValue( value, validatedItems ) {
let isValid = false;
const selectedValues = list.map( ( item ) => item.value );
const validatedValues = validatedItems.map( ( item ) => item.value );
if ( selectedValues.indexOf( value ) === -1 && validatedValues.indexOf( value ) === -1 ) {
isValid = true;
}
return isValid;
}
function validateInput( event, inputs, reason ) {
if ( 'createOption' == reason ) {
let validatedItems = [];
let values = inputs[ inputs.length - 1 ].split( ',' );
for ( let value of values ) {
if ( /[^0-9]+/.test( value ) || value.length == 0 ) {
continue;
} else {
let isValid = validateValue( value, validatedItems );
validatedItems.push( {
id: uuidv4(),
value,
isValid
} );
}
}
updateList( validatedItems );
} else if ( 'removeOption' == reason ) {
let newList = inputs.map( ( id ) => listById[ id ] );
setList( newList );
} else if ( 'clear' == reason ) {
setList( [] );
}
}
/**
* Return call adapted from Material-UI "Multiple values" Autocomplete demo.
* @see https://mui.com/material-ui/react-table/#sorting-amp-selecting
* Code: @see https://github.com/mui/material-ui/blob/v5.9.2/docs/data/material/components/autocomplete/Tags.tsx
*
*/
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="e.g. 12,73,902,23,41"
helperText="Enter comma separated positive integers (no spaces)"
/>
)}
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>
</Stack>
);
}
/**
* Inject component into DOM
*/
root.render(
<Tags />
);