mui / material-ui

Material UI: Comprehensive React component library that implements Google's Material Design. Free forever.
https://mui.com/material-ui/
MIT License
93.76k stars 32.24k forks source link

[Autocomplete] Add ability to append to value instead of replacing it #19267

Closed jdoklovic closed 4 years ago

jdoklovic commented 4 years ago

Summary 💡

I have an autocomplete that parses the input and provides different sets of options depending where you are in the text. Think about this like autocompleting a "query" field:

  1. user types "proj" and gets options: [project, projectPermission] 1.1 user selects "project" and "project" is the current input value.
  2. user types and gets options: [=,!=] 2.1 user selects "=" and the value is updated to "project ="
  3. user types m and gets options: [myProject,mystery,mattsProject,...] 3.1 user selects "myProject" and value is now "project = myProject"

Currently I have the options displaying properly with free solo and it "works" if I ignore the options and just keep typing, but as soon as I click on an option it replaces the entire value with the option clicked.

Currently I'm just using the Autocomplete component so I'm not sure if there's a way to swipe the onClick handler of the options and get it to do what I want and maintain all the other functionality. I haven't tried useAutocomplete yet, so I'm not sure if that would help.

Examples 🌈

essentially I want to build something like this: jql-editor

mikkopursuittechnology commented 4 years ago

@jdoklovic Have you tried using multiple?

support[bot] commented 4 years ago

👋 Thanks for using Material-UI!

We use GitHub issues exclusively as a bug and feature requests tracker, however, this issue appears to be a support request.

For support, please check out https://material-ui.com/getting-started/support/. Thanks!

If you have a question on StackOverflow, you are welcome to link to it here, it might help others. If your issue is subsequently confirmed as a bug, and the report follows the issue template, it can be reopened.

oliviertassinari commented 4 years ago

@jdoklovic It's supported, you would need to:

  1. Configure getOptionLabel to render the correct input value (the full string)
  2. Configure renderOption to render the correct option value (the incremental change string)

@mikkopursuittechnology Thanks for the interest in the topic :). I believe multiple isn't necessary.

jdoklovic commented 4 years ago

@oliviertassinari I'm not sure I'm following this. perhaps I'm misunderstanding what getOptionLabel and renderOption are used for. I assumed getOptionLabel was to display the labels in the drop down and renderOption was to render the option in the dropdown.

It seems like you're saying getOptionLabel is used to determine the new value for the input?

jdoklovic commented 4 years ago

@oliviertassinari Sorry, one last question... I have this somewhat working, but was just wondering if it's expected that getOptionLabel is called multiple times for the same option?

I'm seeing that when I click an option, getOptionLabel gets called 3 times.

nobodyme commented 3 years ago

I was also looking to make something like this.

nobodyme commented 3 years ago
class ExpressionBuilder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: this.props.value ? this.props.value : '',
      fakeValue: ''
    }

    this.filterOptions = this.filterOptions.bind(this);
    this.suggestItems = this.suggestItems.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  suggestItems(value) {
    /**
     * return items to suggest, based upon context
     */
    return ['blah', 'nomore'];
  }

  getCurrentWord(text) {
    const words = text.split(" ");
    return words[words.length - 1];
  }

  filterOptions(options, { inputValue }) {
    const items = this.suggestItems(inputValue);
    let currentWord = this.getCurrentWord(inputValue);
    const matched_chars = items.filter((char) => {
      return char.indexOf(currentWord.toLowerCase()) !== -1;
    });
    return matched_chars;
  }

  handleChange(event) {
    const value = event.target.value;
    this.setState({
      inputValue: value
    });

    /**
     * Small hack to reset the internal state of the material autocompleter,
     * to allow it to suggest the previously suggested value,
     * if the input in totally wiped, and also reset highlight
     */
    if (value === '') {
      this.setState({
        fakeValue: ''
      });
    }
  }

  render() {
    return (
      <Autocomplete
        id="expression-builder"
        disableClearable
        options={filters}
        autoHighlight={true}
        value={this.state.fakeValue}
        inputValue={this.state.inputValue}
        filterOptions={this.filterOptions}
        onChange={(event, newInputValue) => {
          const words = this.state.inputValue.split(" ");
          words.pop();
          words.push(newInputValue);
          const newWord = words.join(" ");
          this.setState({
            inputValue: newWord
          });
        }}
        freeSolo={true}
        style={{ width: 300 }}
        renderInput={(params) => {
          return <TextField className="expressionBuilderField" {...params} variant="outlined" onChange={this.handleChange}/>
        }}
      />
    );
  }
}

export default ExpressionBuilder;

This implementation worked for me!

Nikita-Filonov commented 2 years ago
class ExpressionBuilder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: this.props.value ? this.props.value : '',
      fakeValue: ''
    }

    this.filterOptions = this.filterOptions.bind(this);
    this.suggestItems = this.suggestItems.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  suggestItems(value) {
    /**
     * return items to suggest, based upon context
     */
    return ['blah', 'nomore'];
  }

  getCurrentWord(text) {
    const words = text.split(" ");
    return words[words.length - 1];
  }

  filterOptions(options, { inputValue }) {
    const items = this.suggestItems(inputValue);
    let currentWord = this.getCurrentWord(inputValue);
    const matched_chars = items.filter((char) => {
      return char.indexOf(currentWord.toLowerCase()) !== -1;
    });
    return matched_chars;
  }

  handleChange(event) {
    const value = event.target.value;
    this.setState({
      inputValue: value
    });

    /**
     * Small hack to reset the internal state of the material autocompleter,
     * to allow it to suggest the previously suggested value,
     * if the input in totally wiped, and also reset highlight
     */
    if (value === '') {
      this.setState({
        fakeValue: ''
      });
    }
  }

  render() {
    return (
      <Autocomplete
        id="expression-builder"
        disableClearable
        options={filters}
        autoHighlight={true}
        value={this.state.fakeValue}
        inputValue={this.state.inputValue}
        filterOptions={this.filterOptions}
        onChange={(event, newInputValue) => {
          const words = this.state.inputValue.split(" ");
          words.pop();
          words.push(newInputValue);
          const newWord = words.join(" ");
          this.setState({
            inputValue: newWord
          });
        }}
        freeSolo={true}
        style={{ width: 300 }}
        renderInput={(params) => {
          return <TextField className="expressionBuilderField" {...params} variant="outlined" onChange={this.handleChange}/>
        }}
      />
    );
  }
}

export default ExpressionBuilder;

This implementation worked for me!

Nice solution, this exactly what I needed. Thank you