reactjsmaterial-uimui-autocomplete

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

    value

    value={ keywords.map( ( keyword ) => keyword.label ) }
    

    onChange

    onChange={ validateInput }
    

    filterSelectedOptions

    filterSelectedOptions={ true }
    

    isOptionEqualToValue

    isOptionEqualToValue={ ( option, label ) => option == label }
    

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