I am working on the autocomplete functionality and want to work with the multiple tags selected by the users. My question is how I can achieve this considering each tag is an object of the type
type Keyword = {
label: string;
otherval: string;
};
As some attributes for autocomplete I use multiple and freeSolo
<Autocomplete
multiple
freeSolo
id='tags-filled'
options={keywords}
My biggest problem lies in figuring out whether it's possible to parse the user input when he presses enter as an object rather than a string. If I click however to create a new option the object gets created. So at the end I have at
onChange={(event, tagsList) => {
/* setTags(tagsList); */
}}
the tagsList
is a list of Keyword || string
type. I would like to have tagsList
being consistently consisting of Keyword
elements.
The full code so far looks like this
`
<Autocomplete
multiple
freeSolo
id='tags-filled'
options={keywords}
style={{ width: 600 }}
onChange={(event, tagsList) => {
const stringList = tagsList.map((item) => {
if (typeof item === 'string') {
return item;
}
return item.label;
});
console.log(stringList);
}}
sx={
{ color: BLACK, background: BLUE }
}
getOptionLabel={(option) => {
// Value selected with enter, right from the input
if (typeof option === 'string') {
return option;
}
// Add "xxx" option created dynamically
/* if (option.inputValue) {
return option.inputValue;
} */
// Regular option
return option.label;
}}
filterOptions={(options, params) => {
const filtered = filter(options, params);
// Input value passed as a string argument
const { inputValue } = params;
// Suggest the creation of a new value
const isExisting = options.some((option) => inputValue === option.label);
if (inputValue !== '' && !isExisting) {
filtered.push({
label: inputValue,
topProductPrompt: `"${inputValue}"`,
});
} else {
filtered.push({
label: inputValue,
topProductPrompt: `"${inputValue}"`,
});
}
return filtered;
}}
renderOption={(props, option) => (
<span {...props} style={{ color: BLACK, backgroundColor: WHITE }}>
{option.label}
</span>
)}
renderTags={(tagValues: readonly any[], getTagProps) =>
tagValues.map((option: any, index: number) => (
<Chip
sx={{ color: WHITE, background: BLUE, bgcolor: ROSA }}
variant='outlined'
// By pressing enter string is parsed, otherwise the whole object
label={typeof option === 'string' ? option : option.label}
{...getTagProps({ index })}
/>
))
}
renderInput={(params) => (
<TextField
{...params}
sx={{ color: WHITE, background: BLUE }}
variant='filled'
label=''
style={{ flex: 1, margin: '0 50px 0 0', color: 'white' }}
inputProps={{
style: { color: 'white' },
...params.inputProps,
}}
placeholder='Product keywords'
/>
)}
/>`
I would like to have tagsList being consistently consisting of Keyword elements.
To address this request directly: This can be done. The MUI API docs declare the tagsList
data type as value: T | Array<T>
. This suggests that if your Autocomplete value
property is configured to be an array of <T>
, then Autocomplete should send <T>
or Array<T>
to the tagsList
argument in the onChange callback. Your code is missing the value
property. You could update your component accordingly to make it work.
As for validation, the functions below can validate the input. You can see these functions in action in this demo. Source code for the demo is available on SourceHut. The demo uses an additional property (isValid
) that might require augmenting your Typescript Keyword interface. The demo is in JavaScript, not Typescript.
Some important Autocomplete properties to understand are these:
options={ keywords.map( ( keyword ) => keyword.label ) }
value={ keywords.map( ( keyword ) => keyword.label ) }
value
property assumes that the Keyword label property can be used as a unique identifier for a keyword. If label is not unique the Keyword interface needs to be augmented with an id property, and id should be used instead of label. Such an update would also require an update to the callback in isOptionEqualToValue
onChange={ validateInput }
filterSelectedOptions={ true }
isOptionEqualToValue={ ( option, label ) => option == label }
options
need to be converted or coerced to different data types to be compared to values in value
.The functions below perform the validation. keywords
is assumed to be in scope for the validateValue
and validateInput
functions.
/**
* Check for duplication in the keyword list or the list of user-entered keyword values.
* This function can be modified based on validation requirements
* (e.g. exclude curse words). :-)
*
* @param {string} label - Most recently entered value by the user.
* @param {Object[]} validatedItems - Keyword objects containing recently
* entered values that have already been validated.
*
* @returns {boolean}
*/
function validateValue( label, validatedItems ) {
let isValid = true;
const selectedValues = keywords.map( ( item ) => item.label );
const validatedValues = validatedItems.map( ( item ) => item.label );
const combinedValues = selectedValues.concat( validatedValues );
// const combinedValues = combinedValues.concat( curseWords ); :-)
let validationCollator = new Intl.Collator(
'en',
{
usage: 'search',
numeric: false,
sensitivity: 'base'
}
);
const matches = combinedValues.filter(
( value ) => validationCollator.compare( value, label ) === 0
);
if ( matches.length > 0 ) {
isValid = false;
}
return isValid;
}
/**
* Main validation function.
*
* @param {Object} event - Synthetic React event object.
* @param {string[]} inputs - Strings from text input field.
* @param {string} reason - MUI reason why the onChange event was triggered.
*/
function validateInput( event, inputs, reason ) {
let newKeywordList = [];
if ( 'createOption' == reason || 'selectOption' == reason ) {
let validatedItems = [];
let value = inputs[ inputs.length - 1 ];
if ( /[^À-ÿ0-9a-zA-Z_. -]/.test( value ) || value.length == 0 ) {
// Use your own validation here. This just checks that entered characters are
// within certain character code ranges (by returning true if a character is
// not within the accepted ranges). You can add code here to display user
// feedback if there is user error. Currently, invalid characters
// (e.g. punctuation marks))just silently fail without feedback.
} else {
// Decide what you want to do with invalid entries.
// Currently, invalid entries are retained with isValid set to false.
let isValid = validateValue( value, validatedItems );
validatedItems.push( {
label: value,
otherVal: '', // Enter whatever goes here.
isValid
} );
}
newKeywordList = [].concat( keywords, validatedItems );
} else if ( 'removeOption' == reason ) {
newKeywordList = inputs.map( ( id ) => listById[ id ] );
} else if ( 'clear' == reason ) {
// Noop.
}
setKeywordList( newKeywordList );
// Optional. Call callback function to update parent component for user
// feedback. E.g. update a list page.
// callback( newKeywordList );
}