Search code examples
reactjsmaterial-uiautocomplete

MUI Autocomplete pass input value as an object


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'
            />
          )}
        />`

Solution

  • Update

    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.

    Autocomplete properties

    Some important Autocomplete properties to understand are these:

    options

    options={ keywords.map( ( keyword ) => keyword.label ) }
    
    • Generate the strings to be rendered in the select drop-down list.

    value

    value={ keywords.map( ( keyword ) => keyword.label ) }
    
    • The callback for the 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

    onChange={ validateInput }
    
    • Send the selected or user-entered value to a callback for validation.

    filterSelectedOptions

    filterSelectedOptions={ true }
    
    • Omit selected options from the drop-down list. (Optional).

    isOptionEqualToValue

    isOptionEqualToValue={ ( option, label ) => option == label }
    
    • Comparator function used to determine if the value in the text field represents one of the Keyword objects. Include if array elements in options need to be converted or coerced to different data types to be compared to values in value.

    Validation

    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 ); 
    }