ericgio / react-bootstrap-typeahead

React typeahead with Bootstrap styling
http://ericgio.github.io/react-bootstrap-typeahead/
MIT License
1.01k stars 407 forks source link

Setting the input value #266

Closed hsuabina closed 4 years ago

hsuabina commented 7 years ago

Is it possible to set the AsyncTypeahead's input value to some arbitrary string? Bear in mind I don't want to set the preselected option(s) as docs suggest, I just want to set the underlying input's value with some string.

To see my use case read the comment below.

Thanks!

hsuabina commented 7 years ago

I'm using the AsyncTypeahead component as a user filter mechanism in a search filter form:

So the when a user starts typing a username in this filter, two things happen triggered by the onSearch hook: a) users (options) are fetch from an endpoint, and fed to the options array... these options will be rendered as suggestions (displaying username and email) b) when users are fetch, also their ids are stored in app's state (outside this component)... they could be seen as the 'selected user id's so far'

If the users clicks on any suggestion provided by AsyncTypeahead, then the onChange hooks triggers and two things happen again: a) the selected user is set as the selected option and AsyncTypeahead does its thing. Here the input value updates with the full username of the selected user (I suspect this is done thanks to the labelKey prop). b) the selected user id is stored in the app's state in the same fashion than earlier... it can be now seen as the 'finally selected user id'

All good so far... the problem now comes if I want to set the value of the input. I know how I can set the default selected options (using selected or defaultSelected props) but I'm not sure how can I set the input's value (with the search query that resulted in those selected options). Is this even possible?

I mean, if I type 'B' in the typeahead and that results in two usernames: ['ABY', 'ABZ'], I don't think there is any possible way of knowing from that result set I used 'B' as search query previously since same results are obtained if you used 'A', 'B' or also 'AB' as search query.

ericgio commented 7 years ago

No, setting the input value to some arbitrary text is not currently possible. See #172 for additional discussion around this issue. I'm having trouble understanding from your description why you need this functionality, though.

hsuabina commented 7 years ago

Thanks for the quick answer. Sorry, not my best description ever. Let me try again.

I have the AT (AsyncTypeahead) component within a form that shows up as a modal window. When you start typing in the AT, I pull and save list of users from an endpoint using the string written in the AT's input (this is done on the onSearch hook). If you don't click on any suggestion and click submit, I send that list with multiple users to some search endpoint, and then close the modal window. If you do click on one of the suggested users provided by the AT then a list containing only that user will be sent when submitting the form (saving this list of one user is done using the onChange hook):

state1

What I want to achieve, is that you can see the string you typed in earlier if you open that modal window again after doing a previous search. So the state of the AT component should be like this (no focus, no dropdown of suggestions being shown, but string is already typed in the input):

state2

If you did select a suggestion in your previous search, then the full name of the user should show up when reopening the modal window (in this case I had selected a user from the suggestions provided):

state3

I have both things available: the list of previously pulled users and the string you typed in the input, but I don't know how to use them so the AT component is shown in the state shown above.

Any ideas will be appreciated. Thanks!

ericgio commented 7 years ago

Yours is a very similar use case to the issue I referenced above. To update my answer a bit, the current API doesn't officially support this. It is possible, however; you can do the following in your parent component:

componentDidMount() {
  this._instance._updateText('Initial value');
}

render() {
  return (
      <Typeahead
        options={options}
        ref={(component) => this._instance = component.getInstance()}
      />
  );
}

Please note that _updateText should be considered a private, undocumented method subject to change without notice at any time. You would be using it at your own risk and your app could break if the internals change.

hsuabina commented 6 years ago

Thanks @ericgio, that did work.

I was trying with passing a crafted array of a single element as defaultSelected (which didn't exists in options) but I think even when it's a private method your solution is way better than what I as doing.

jeffmcaffer commented 6 years ago

FWIW, I am having a similar problem for a different use case. In my case we are doing an autocomplete input for GitHub repos. They have a login and a repo name but I'd like the user to

  1. type part of a login (e.g., 'ericg') and see a set of login options
  2. pick a login
  3. be offered as set of login/repo options

alternatively the user can paste in ericgio/rea (partial path) and get the list of matches.

The logins and repos are supplied by different GitHub API calls and doing the full cross product proactively would be expensive and not useful.

The key challenge is around the /. The user can type it but they should not have to. In the steps above # 2 was a click. After the click the input should look like ericgio/ and the options should be all of ericgio's repos as fully qualified paths.

I tried the internal hack mentioned above but

I get that this scenario is a bit off the beaten track but it is not wholly in left field. I'm not expecting explicit support but some suggestions as to where I'm going wrong would be good.

Aside, I did manage to get this working in react-select but am preferring this component for the bodyContainer support and a couple other things. Would be great if I could get this last piece of the puzzle put in.

jeffmcaffer commented 6 years ago

FWIW, I made some progress but still using internal implementation details. Here are the essentials.

In render capture the component for future reference

    render() {
      return (<AsyncTypeahead
        ref={component => this._typeahead = component ? component.getInstance() : this._typeahead}
        onChange={this.onChange}
        ...

Then in the same component, when the user selects something onChange is called and you can figure out if the input and selection should change.

  onChange(values) {
    ... logic to figure out if we are done with one segment of the input path ...
    // if so, take the current value, append / and then clear the selection 
    this._typeahead._updateText(value + '/')
    this._typeahead._updateSelected([])
  }

so @ericgio like I said, not looking for explicit support per se but the idea of manipulating the input and the selection programmatically is pretty powerful. I don't know enough about all the other scenarios to propose concrete API/design changes that would be compatible. Happy to help work through ideas though.

ericgio commented 6 years ago

@jeffmcaffer: What you're essentially requesting here is for the text input itself to be a controlled component, separate from the overall typeahead. While that's a reasonable request, it's going to be tough to implement the way the component is currently written. There are a bunch of downstream effects whenever the input value changes, so I'm guessing a lot of the internals would need to be re-written to allow what you want, unfortunately.

Note that you can control selections using the selected prop. You shouldn't need to use _updateSelected.

jeffmcaffer commented 6 years ago

thanks @ericgio . yeah, I can see that it could be far reaching. Do you see any issues with the approach that I mentioned above (https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-359224294). Other than the obvious that it relies on some internal implementation details.

as simplified version of this is to allow the component to be initialized with an empty selection and a given text value. I vaguely recall seeing some related discussion in another issue. Effectively in my case, when someone selects the GitHub or I am accepting that selection but immediately resetting the typeahead to have no selection, some initial text in the input and a new set of options

ericgio commented 6 years ago

Do you see any issues with the approach that I mentioned above

As noted in my comment, you should be able to reset the selection using the selected prop rather than _updateSelected. You'll just need to make sure that you call _updateText after you set the selected value. That should be easy to do by passing a callback to setState:

onChange(values) {
  ... logic to figure out if we are done with one segment of the input path ...
  // if so, take the current value, append / and then clear the selection 
  this.setState({selected: []}, () => {
    this._typeahead._updateText(value + '/')
  })
}

[A] simplified version of this is to allow the component to be initialized with an empty selection

Yeah, I've thought a little about having the input work as an uncontrolled component by adding a defaultText or initialText prop to allow this and I don't think it would be too difficult to implement. Not sure that would totally address your case, since you'd only be able to set the value on the initial mount. You might need a workaround like unmounting then re-mounting the typeahead, or conditionally mounting a second typeahead.

jeffmcaffer commented 6 years ago

That did not work for me. I updated my component to manage selected by maintaining state.selected and passing that into Typeahead as a prop. In onChange it is doing the right selected state management and running the code suggested above with the callback. All the calls seem to happen but the input text is not updated in the UI. The input stays at (say) ericgio, not ericgio/.

Note also that onSearch is not getting triggered.

Perhaps some it is getting debounced or something. I can see _updateText being called with ericgio/ as the value but yet that is not getting rendered.

Your help is appreciated. In the end, the code I put in my comment above is working and while it is a little unsettling that it is using internals and is working "unexpectedly", that it not the end of the world right now.

BTW, if you want to see our code in all its glory, check out https://github.com/clearlydefined/website/blob/master/src/components/GitHubSelector.js

Not super easy to run as is without getting and running the server part...

ericgio commented 6 years ago

Re-opening this issue for more discussion since it has now come up a few times. To recap a bit:

ericgio commented 6 years ago

defaultInputValue added in v2.3.0. This allows the input to function as an uncontrolled component, much the way defaultValue would work with a normal text input.

jeffmcaffer commented 6 years ago

Thanks @ericgio, that addresses a new use case I just came across. Very timely. Should I be able to use that for my other scenarios or wanting to "reset" the value? (see above)

ericgio commented 6 years ago

@jeffmcaffer: You might be able to get something working, I'm not sure. You'd have to unmount and re-mount the component, though.

alasdairhurst commented 6 years ago

I was running into this issue when i wasn't able to clear a form with these typeaheads in. Since my typeahead is wrapped with some fixes/additional/alternative functionality and styles, I added the following to see if i could get an controlled component:

    componentWillReceiveProps(props) {
        const { value } = props;
        if (value !== undefined) {
            // it's controlled
            this.input && this.input.getInstance().setState({ text: value });
        }
    }

and pass the value as the defaultInputValue:

defaultInputValue={defaultInputValue || value}

Surprisingly nothing seems to be broken in terms of events by introducing this prop to my component and setting the state on this one.

ericgio commented 6 years ago

i wasn't able to clear a form with these typeaheads in

@alasdairhurst: you should be able to clear the typeahead by passing an empty array to the selected prop. No need for anything fancy.

this.input && this.input.getInstance().setState({ text: value })

This might work but is obviously highly discouraged.

alasdairhurst commented 6 years ago

Would be nice if it would work with mutated values - https://jsfiddle.net/alasdairhurst/wtLgga55/ I see a problem where onInputChanged is being called when the selected prop changes. This is due to changes outside the component and shouldn't be called if the component itself didn't make the change for the outside world to know. In this case, i update my form's state with a mutated version of the value that was typed with "onInputChanged", but since my form now passes the new value into the component, it calls onInputChanged again infinitely.

Just setting the same value works fine though, which is nice. It would be useful if selected took a string alternatively to an array. The array assumes that it's being used as a multiselect and has no real use when multiple is false. // slightly off topic, but as a follow on: "selected" as a name seems wrong based on the naming of props for react-bootstrap or react in general. Usually this would be "value". onInputChanged would be "onChange", and your current "onChange" would be something like "onOptionAdd" or something indicating the options array has been mutated in some way. Ideally, the props of this component should be a superset of the react-bootstrap multiselect, since this is pretty similar, but with extra features, just like the relationship between a regular input field and the multiselect field. //

ericgio commented 6 years ago

onInputChanged is being called when the selected prop changes. This is due to changes outside the component and shouldn't be called if the component itself didn't make the change for the outside world to know.

The way the component is written, the input displays the selected value. If you change that value, the input changes and onInputChange will therefore be called. What you're asking for is what has been requested by others, which is to be able to change the value of the input itself in a controlled manner.

Probably best to discuss your other comments in a separate issue, as those involve major changes to the API.

alasdairhurst commented 6 years ago

Ok I'll create a couple of issues.

alasdairhurst commented 6 years ago

Ok, I noticed a problem with the suggested way of using selected to have a semi-controlled component. https://jsfiddle.net/alasdairhurst/02ng79d1/

You can see here, if i'm updating selected it in a controlled way, it works fine except for never rendering the hint text.

ericgio commented 6 years ago

if i'm updating selected it in a controlled way, it works fine except for never rendering the hint text

@alasdairhurst you're updating selected using onInputChange, which means it's set on every keystroke. The hint won't show when there's a selection.

This would be the way to do it: https://jsfiddle.net/02ng79d1/11/

ericgio commented 6 years ago

@hectorsabina @jeffmcaffer: Heads-up that _updateText is being removed soon, so you'll need to find a different way to set the input.

To continue the discussion, I understand the use case for setting the initial value (eg: restoring a previous search string when re-mounting the component). However, I'd still like to understand the use case for controlling the input value beyond that, as it creates a fundamental conflict between the input value and the selected value. I'm not convinced there's a truly valid case for it, but I'm willing to be proven wrong.

jeffmcaffer commented 6 years ago

Thanks for the heads up @ericgio . Here is a short GIF video that shows an example. here we offer the user the ability to enter a GitHub org and repo as org/repo. As they type the org, the code autosuggests orgs from GitHub. Once they pick an org from the menu or type a /, the completion choices change to be repos. You can see the code for the GitHub and Maven iterations of this at (respectively):

Would totally love to find that we are off base and can accomplish the same thing some other way.

updatetext

ericgio commented 6 years ago

Thanks for the clear example @jeffmcaffer. Yours feels like a special case to me, in that you're combining two selectors into one. Traditionally, I think you'd see something more like the following, with separate fields:

image

I understand that you're trying to streamline the experience a bit, but I think this is an uncommon scenario. I don't have a great recommendation for an alternative; you could try to replace the initial typeahead with a second one when the repo is selected, and use defaultInputValue to set the initial value. Honestly, it seems like the implementation would be a lot easier (and the UX clearer) if you just split it up into two typeaheads as illustrated above.

jeffmcaffer commented 6 years ago

Understood. We worked through that (two part) UI. For consistency we wanted one as the same typeahead is used for Maven, npm, NuGet, Gems, ... and some setups require two parts, some just one, ... In the end I realize that this is not a mainstream usecase for typeahead. It does feel legit but in the end we'll adapt somehow to whatever happens.

Thanks for the heads up.

RobUnderscore commented 6 years ago

Just an FYI this is also a use case for us. We have a customer, contact, and billing address picker.

A customer has a set of contacts and billing addresses. When the user changes the customer, the contacts/billing address should clear.

Right now we need to use _updateText to do this, but we'll start exploring other options if this is going to be deprecated.

Thanks for the library, it's been very useful and we'll still use it in other areas.

typeahead-example

ericgio commented 6 years ago

@RobCubed it looks like you're just clearing the inputs, which is currently supported. You can either use the public clear method or, if you're using a controlled typeahead, simply pass an empty array to selected.

ericgio commented 6 years ago

Please note that _updateText is now gone as of v3.0.0

johnywith1n commented 6 years ago

I have a use case where I am selecting multiple items. I want to be able to clear the input but keep my selections. The public clear method does that but also removes my selections. Is that something that would fit into your api?

ericgio commented 6 years ago

@johnywith1n: does the multi-select version (ie: setting multiple={true}) not work for your case?

johnywith1n commented 6 years ago

@ericgio I am using multiple={true}. What I'm trying to do is clear the text input without clearing the selections. The public clear method clears both the text input and the selections.

My specific use case is to clear the text input without clearing the selections when someone clicks outside the element so that it does not appear that there's something that might be selected due to the text.

I've resorted to doing

const instance = this.ref.getInstance();
instance.setState({
  activeIndex: -1,
  activeItem: null,
  initialItem: null,
  shownResults: instance.props.maxResults,
  text: '',
});

which is pretty much just the clear method without selected: [], in its state change since _updateText was removed. However, it seems like this might be something that could be useful to other people as well.

ericgio commented 6 years ago

Oh, I see what you're saying. The clear method (as well as the use of clearButton) is primarily intended for clearing selections, hence why the clear button isn't even visible when there are no selections.

Something to understand is that the input isn't intended to be a first-class citizen in this component; it's simply a tool for filtering down the result set.

johnywith1n commented 6 years ago

Sure, but from a user experience point of view, leaving text in the input when they move away from the element is confusing to end users. I'm guessing by your previous response that this is not something you'd be interested in merging?

ericgio commented 6 years ago

leaving text in the input when they move away from the element is confusing to end users

Is it? That's how any standard input element would behave as well. Also, the text was presumably input by the user so they should understand why it's there.

johnywith1n commented 6 years ago

In this specific case, when using multiple, the user may be confused as to whether or not the selection was actually made and believe that the selection was made when in fact it was not. This has already happened.

ericgio commented 6 years ago

The tokenized selection vs. plain text seems like a pretty reasonable UI distinction, IMO. As far as officially supporting modifying the input value, my stance remains: there are very few, if any, valid use cases for it that I've seen; the work involved is substantial; and the resulting developer experience is potentially complicated and fragile.

mariendev commented 6 years ago

My use case for inserting text is as follows: We have a company search to find company records. Our system has an approval flow for custom companies to be linked to existing companies. For speed up we have a button to copy the company name in the search field. This button just needs to insert search text in this component. (I don't know the company data yet since it is retrieved via an async flow)

My workaround for the new version is this:

let event = new Event('input', { bubbles: true });
this.company.getInstance().clear();
this.company.getInstance().setState({text: nameVariable});
this.company.getInstance().props.onInputChange(nameVariable, event);
this.company.getInstance().focus();
cameron-martin commented 5 years ago

To continue the discussion, I understand the use case for setting the initial value (eg: restoring a previous search string when re-mounting the component). However, I'd still like to understand the use case for controlling the input value beyond that, as it creates a fundamental conflict between the input value and the selected value. I'm not convinced there's a truly valid case for it, but I'm willing to be proven wrong.

I want to use the component to produce something like the google search homepage - so it just acts like an input field and the dropdown merely acts as a fast way of changing the input value. I realise that this only really makes sense when the options are strings, because otherwise the types don't match up.

bestuiexperience commented 4 years ago

Is there a way I can get rid of the hint auto-populating on the inputs each time I type? I want the input just to include what the user types.

ericgio commented 4 years ago

@bestuiexperience: that's a separate issue from what's being discussed here. Check out this sandbox for an example of custom input rendering that omits the hint functionality.

FranzForstmayr commented 4 years ago

@mariendev do you have an more in detail example for setting the input value? I use typeahead for a database frontend, when a user wants to change a value i want the previous value pre filled in the form.

ericgio commented 4 years ago

@FranzForstmayr: Check out the defaultInputValue prop, which allows you to set an initial input value when the component mounts.

FranzForstmayr commented 4 years ago

@ericgio Thanks a lot. I already tried this method once, however my data was written to state in ComponentDidMount, so directly after mounting. This was working without TypeAhead, that's why I didn't recognize the issue.

Now it's working like a charm :) Thank you for this package!

ericgio commented 4 years ago

Closing this issue, as there are no plans to allow changing of the input value in a controlled manner.

cameron-martin commented 3 years ago

@ericgio Was my use case above not convincing enough?

ericgio commented 3 years ago

@cameron-martin: Can you explain why setting the input value is needed in your use case? The general case you described above is reasonable, but from your description my guess is what you're looking for is a way to select whatever arbitrary string the user has entered in the input.

cameron-martin commented 3 years ago

It's just that when you start using the component as a "text box but with suggestions" then it's sometimes necessary to update the text input in response to a state change, just as it is with regular text inputs. I have, however, switched to using a native input field with a datalist for this use, which works reasonably.

Orelongz commented 3 years ago

I know this is quite old, but it might be helpful to someone else coming here with a scenario like @cameron-martin's ...

I won't say it's a solution, but it's kind of a workaround a teammate and I found;

Basically, since the value for the labelKey is what gets displayed as the input when one of the results is clicked, we can set the labelKey as seen in https://github.com/ericgio/react-bootstrap-typeahead/blob/master/docs/Data.md#arrayobject-wcustom-labelkey, then have all options return the search term as that labelKey.

So with a search term as dav, options returned from your API could be something like

const options = [
  {id: 1, searchTerm: 'dav', label: 'John Davis'},
  {id: 2, searchTerm: 'dav', label: 'David Miles'},
  {id: 3, searchTerm: 'dav', label: 'Charles Davison'},
  {id: 4, searchTerm: 'dav', label: 'Herbie Lance'},
];
<Typeahead
  options=options
  labelKey="searchTerm"
  renderMenu={…display the menu as you would like...}
/>

With this, after selection, dav should remain as the input field

eugeneborodkin commented 3 years ago

Is defaultSelected the modern equivalent of defaultInputValue ?