Closed hsuabina closed 4 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.
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.
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):
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):
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):
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!
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.
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.
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
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
if (!multiple && _this.state.hasSelection) {
in _handleSearch
.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.
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.
@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
.
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
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.
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...
Re-opening this issue for more discussion since it has now come up a few times. To recap a bit:
defaultInputValue
prop allowing developers to at least set an initial value, which would allow for an uncontrolled input. This would be fairly easy to do.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.
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)
@jeffmcaffer: You might be able to get something working, I'm not sure. You'd have to unmount and re-mount the component, though.
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.
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.
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. //
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.
Ok I'll create a couple of issues.
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.
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/
@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.
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.
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:
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.
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.
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.
@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
.
Please note that _updateText
is now gone as of v3.0.0
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?
@johnywith1n: does the multi-select version (ie: setting multiple={true}
) not work for your case?
@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.
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.
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?
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.
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.
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.
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();
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.
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.
@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.
@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.
@FranzForstmayr: Check out the defaultInputValue
prop, which allows you to set an initial input value when the component mounts.
@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!
Closing this issue, as there are no plans to allow changing of the input value in a controlled manner.
@ericgio Was my use case above not convincing enough?
@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.
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.
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
Is defaultSelected
the modern equivalent of defaultInputValue
?
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!