With react-select, select multiple items matching search simultaneously - javascript

I am using react-select to display a searchable drop-down list of items, of which the user can select multiple items. My list is quite long and the user will often wish to multi-select many items which match the same filter string, which is a bit of a tedious procedure because each time you select an item the dropdown disappears and you need to re-type the search.
For example, the following sandbox has a react-select which lists lots of apples and cheeses. In order to select all the Apples, one would have to keep typing "Apple" and choosing one apple at a time.
https://codesandbox.io/s/2l99lry5p
Coming from desktop UI background, I naturally want to be able to type a search query and press Ctrl-A to select all of the matching search results and add them to my list, or Ctrl-Click to cherry pick multiple items from the matching set. But as far as I can tell there's no support for any hotkey like this in react-select.
Does the react-select API have any way that I can implement a "select all" hotkey which would select everything that matches the current search filter (or even an explicit "select all matches" button on the page would be fine)? I cannot see any programmatic way to get access to the set of objects which match the filter. Is this something that I would need to fork react-select to implement or is it possible to do this via the existing API somehow?

React Select has built-in props that can be used to prevent the menu from closing on select and persist the search string.
First prevent the menu from closing on select by setting closeMenuOnSelect to false.
Then onInputChange store the search string in state if the action equals 'input-change'.
Setting inputValue to this.state.value will persist the search string in the input field.
class Foo extends Component {
constructor(props) {
super(props);
this.state = {
value: ''
};
}
handleInputChange = (value, e) => {
if (e.action === 'input-change') {
this.setState({value});
}
}
render() {
return (
<Select
isMulti
name="colors"
options={options.map(x => MakeOption(x))}
className="basic-multi-select"
classNamePrefix="select"
/* added these props */
closeMenuOnSelect={false}
onInputChange={this.handleInputChange}
inputValue={this.state.value}
/>
)
}
}
Updated sandbox: https://codesandbox.io/s/yvmzx6pn6z

I hacked up something that kind of does what I want but is quite ugly:
https://codesandbox.io/s/j7453qrmv
To use:
Try searching "apple", then press "Add all matching items to selection"
The approach:
As #wdm mentioned, there's a onInputChanged you can hook in to.
In onInputChanged, get the matching items store them in the state of the component
I add a button near the Select which allows the user to choose to copy the matching set of items into another state variable chosenItems
The react-select Select component has a value property that you can provide to programmatically choose the items. I pass state.chosenItems in to this.
This works but there were many things that make this a pain:
The onInputChanged handler gets called before the items matching the filter appear. I attempted to grab the matching items by DOM queries but it did not work because onInputChanged is too early. So rather than relying on react-select's filtering logic, I'm replicating the filtering logic in the onInputChanged handler. This is not great as there could be a discrepancy between my filtering logic and the displayed list of matching items.
Whenever you click after typing a search, the react-select clears the search, which invokes the onInputChanged event again. So by clicking on the custom "Add All Matching Items" button, it removes the filter, clearing the list, invoking onInputChanged and setting the state with a new list of matching items. To deal with this I needed to have a previousMatchingOptions state variable which keeps track of the matching items from the previous call to onInputChanged. This seems like a terrible hack.
Likewise, I attempted to hide/show my "Select All Matching Items" button based on whether there were currently more than one item that matches the search, but I was similarly thwarted by timing issues. I attempted to hack around this but kept getting caught up in corner cases.
The UI I came up with for "Select All Matching Items" doesn't feel integrated with the react-select very well. It would be nicer if it was part of their component rather than beside it.
By using the values property of the Select component, you are bypassing the component's internal management of its state, so the normal way of adding, removing, and clearing items does not work without reimplementing all that in a custom onChange handler which modifies the state.chosen which is passed to values. Managing this myself seems also less than desirable.
So, I have a solution, but if someone has something has a suggestion that is much cleaner and/or simpler I would be happy to hear it!
It seems like forking the control and doing these changes internal to the component might be the better way to go.
In my onInputChanged I attempted to get the matching search results directly from the DOM using some getElementsByClassName hackery, though this approach did not work because the onInputChanged

A very simple way of implementing a "Select All" option is overriding React-Select component with a custom component. For that you first need to import it as
import { default as ReactSelect } from 'react-select';
then create a custom component which defines a new Property named "allowSelectAll", and selects all the options when this property is set to 'true'.
const Select = props => {
if (props.allowSelectAll) {
if (props.value && (props.value.length === props.options.length)) {
return (
<ReactSelect
{...props}
value={[props.allOption]}
onChange={selected => props.onChange(selected.slice(1))}
/>
);
}
return (
<ReactSelect
{...props}
options={[props.allOption, ...props.options]}
onChange={selected => {
if (
selected.length > 0 &&
selected[selected.length - 1].value === props.allOption.value
) {
return props.onChange(props.options);
}
return props.onChange(selected);
}}
/>
);
}
return <ReactSelect {...props} />;
};
Select.propTypes = {
options: PropTypes.array,
value: PropTypes.any,
onChange: PropTypes.func,
allowSelectAll: PropTypes.bool,
allOption: PropTypes.shape({
label: PropTypes.string,
value: PropTypes.string
})
};
Select.defaultProps = {
allOption: {
label: "Select all",
value: "*"
}
};
Note: You can simply copy and paste the above given code and it will work absolutely fine.
And once that is done you can simply use the new 'Select' component with 'allowSelectAll' property set to true.
<Select allowSelectAll={true} isMulti={true} isSearchable={true} options={options} />

You can use the filterOption function like this:
<select
options={[{label: 'test', value: 1, customData: 'bla blub test'}]}
filterOption={(option, filter) => {
const { customData } = option.customData;
if(customData.toLowerCase().indexOf(filter.toLowerCase()) >= 0) {
return true;
}
}} />
Hope this will help you :)

Related

ReactJS state value not update correctly on input onChange

In my app, i have Groups, and i have a select input, to change between group lists. My issue is when i change the select input, the state changes the name of the list, but is not the one im selecting. For example, at initial i have:
All Groups
Group #1
Group #2
When i choose Group #1, the state in the console says "All Groups". If i choose Group #2, the state in the console says "Group #1"
Select Input
<select id="selectedPg" name="selectedPg" onChange={event=> {
this.valueToState(event.target)
this.viewProfileGroup();
}}>
viewProfileGroup()
// View Selected Profile Group
viewProfileGroup = ()=> {
const { selectedPg, allProfilesLists } = this.state
this.setState({
profilesLists: allProfilesLists.filter(pg => pg.profileGroup === selectedPg)
})
console.log(this.state.selectedPg)
}
The issue is the setState is not synchronous, so when you call this.viewProfileGroup() and in the method, you are not operating on the latest state.
The solution is simple.
Just pass event.target to the viewProfileGroup function
this.valueToState(event.target)
this.viewProfileGroup(event.target);
PS, maybe in this way you will do not need this.valueToState at all.

React-table: State doesn't change as expected, and table doesn't update

Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
I am using the react-table package (version 7.1.0).
I have a table which displays some invoices like so:
The user should be able to select some or all of these items using the selected checkbox.
The selected field is not part of the data. However, when the user hits a selected checkbox, the field should toggle and an array storing document numbers should be populated.
To store the document numbers, I have a stateful getter and setter:
const [selectedInvoiceIds, setSelectedInvoiceIds] = useState([]);
To populate the field, I am attempting to simply add the document number to the array immutably, from the onChange of the checkbox:
{
Header: "Selected",
accessor: "selected",
Cell: item => {
const { documentNumber } = item.row.values;
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds([...selectedInvoiceIds, documentNumber]);
}}
/>
);
}
}
When a checkbox is clicked for the first time, the selectedInvoiceIds becomes populated:
selectedInvoiceIds: ["706942"]
The problems are:
The table does not update to reflect the state change, despite this prop on the checkbox:
checked={selectedInvoiceIds.includes(documentNumber)}
The selectedInvoiceIds value gets overwritten when another document number is added, instead of being added to, as if the state is re-initialising to [] somewhere in between.
Can it be explained why these state issues are occurring and how to get around it?
I am aware of the useTableState value exposed by react-table, but I don't know how I can apply it to this use case.
Codesandbox example: https://codesandbox.io/s/react-table-state-not-updating-hmquq?file=/src/App.js
There are multiple issues in this code:
// 1) updates to state should should use callback function if it uses prv state
return (
<input
type="checkbox"
checked={selectedInvoiceIds.includes(documentNumber)}
onChange={() => {
setSelectedInvoiceIds(prvSelectedInovicesId => [...prvSelectedInovicesId, documentNumber]);
}}
/>
);
// 2) also columns is using useMemo, however not providing dependencies, so columns are not being updated when the selectedInvociesIds state gets updated
// 3) you are not yet handling the toggle, this mean you are just always just adding to the array and not removing from it
here is a working version code sandbox
https://codesandbox.io/s/react-table-state-not-updating-wkri3?file=/src/App.js
Because of useMemo, add your array to last parameter
const columns = React.useMemo(
// ....
,
[selectedInvoiceIds]
);
And as we need toggle checkboxed it is more reasonable to keep selected ids in object instead of array and update it as this
setSelectedInvoices({
...selectedInovicesIds,
documentNumber: !selectedInovicesIds[documentNumber]}
)
so it will toggle mark

ReactJS - ReactiveSearch - Custom event on suggestion selection

I am building an autocomplete component to search current users and add them to a team. The search works fine except I need to customize a couple things that I can't figure out how to do.
First, this component is modeled after GitHub's invite user modal. You can start typing to search for a current user and it populates any results it finds. However, if it doesn't find any results it only shows one item to invite that user via email. How can I edit the full state of the result list for a DataSearch component? All I can see is to edit the content of each suggestion via the onSuggestion prop. I need to be able to say, "If there are zero results, display this."
Second, when a suggestion from the autocomplete result list is selected, I need to be able to reset the search box because I am populating a list that the user can see. Right now, the search box is populated with the value of the suggestion. So, I am populating the list below of selected results just fine; but, I still need to be able to reset the search box when that result is selected.
Help????
CodeSandbox link
For the first part of the problem, you can use the prop onNoResults on any of the results components to render custom JSX when no results are found. From the docs:
onNoResults String or JSX [optional]
show custom message or component when no results founds.
<ResultList
...
// custom JSX when no results are found
onNoResults={
<section>
<input />
<button>Add</button>
</section>
}
/>
For the second part of the problem, there are two ways IMO you may approach this.
Using a ReactiveComponent which lets you create a custom ReactiveSearch component.
Using customQuery
I'll explain how to approach this using customQuery but it might be better to create a custom component depending on which approach suits your needs best.
In the example I've shared my DataSearch looks like this:
<DataSearch
title="DataSearch"
dataField={["original_title", "original_title.search"]}
categoryField="authors.raw"
componentId="BookSensor"
customQuery={this.customQuery}
defaultSelected={this.state.value}
onValueSelected={this.updateState}
/>
The reason for using a customQuery is to get full control of which query gets applied to retrieve the results. ReactiveSearch is designed to work reactively. When a value is set into the DataSearch, the ResultList would react to this change. Having a customQuery lets us control which query is fired for this change. Also I'm keeping the value of DataSearch in the state so I can clear it up when the query gets fired. Here's what I'm using in the example:
customQuery = (value, props) => {
// find the current query using defaultQuery from DataSearch
const currentQuery = DataSearch.defaultQuery(value, props);
// clear the value in component's state
this.setState({
value: ""
});
if (value.length) {
// store this query
this.prevQuery = currentQuery;
return currentQuery;
}
// if the value is empty, that is it was cleared, return the previous query instead
return this.prevQuery;
};
So whenever the value is cleared, I just return the previous query so the result list shows the correct results for the previous query (before the value was cleared).
Also in order to control the value of DataSearch from my component I'm using the defaultSelected and onValueSelected props. They work quite similar to how you would create a controlled component in react. Docs
Again, it might be better to create a custom component using ReactiveComponent if this approach sounds complicated to customize this flow.
Demo for this answer using customQuery

React + Redux: Changing input focus programatically

I have a react application with a 'yes/no' question interface. I'm using redux to manage state.
The app presents a series of input fields, which are added dynamically.
When a question is answered with a 'y' or 'n' keystroke, I want the next input in the series to get focus automatically -- enabling fast data-entry. This is proving surprisingly difficult!
My redux store contains the current question's index - I want this to translate into focus on that input.
/*Input Component*/
const quizQs = ({
questionArray = ["Q1", "Q2", "Q3", "Q4"]
currentQIndex, //From Store
changeQIndex, //Action
}) => {
const _handleKeyDown = (e) => {
if(e.key == 'y' || e.key == 'n'){
//Dispatches Action that increases current currentQIndex'
}
}
//_handleFocus()... for updating currentQIndex if an input is focused by the user
return (
{questionArray.map((q, index) => {
return(
<input
key={index}
onKeyDown={_handleKeyDown}
onFocus={_handleFocus}
type="text"
placeholder={q}
/>
)
})}
)
}
/*Main Component -- Connected to Store*/
class myQuiz extends React.Component {
constructor(props){
super(props);
}
render(){
return(
<div>
<quizQs
currentQIndex = {this.props.currentQIndex}
changeQIndex = {this.props.changeQIndex}
/>
</div>
)}
}
I have tried setting autoFocus = true, if the store's 'currentQIndex' matches the index of that particular question, in the 'quizQs' component. This method is able to focus the specified field when the page first renders, but the focus does not change when the 'currentQIndex' of the store changes.
As I have searched for an answer, React 'refs' + use of a callback would seem to be the way to go, (https://reactjs.org/docs/refs-and-the-dom.html#the-ref-callback-attribute), but I cannot figure out how to set up such 'focus' callback, that responds to changes in the Redux store.
In addition, in order to use Refs, the component must be set up as a class, not an arrow function. AFAIK, it is not good practice to have multiple classes in one file, and it does not seem appropriate to connect so many different components to a redux store.
I'd appreciate help.
Here is simple example of what you are trying to achieve: https://codesandbox.io/s/z64qw3nkzx
I simplified it a bit, but the point is there.
As .focus() is native method on the DOM element, you need a way of tracking those input elements. For that in React there is ref prop. It accepts a function with has one parameter which is the actual DOM element of the component.
You'll see that I put all of the DOM references into an array:
<input
ref={el => this.questionInputElements[index] = el}
key={index}
// all other props
/>
and on key up* find the next element in the array and focus it:
const nextInput = this.questionInputElements[index + 1];
if (nextInput) {
nextInput.focus();
}
* it needs to be on key up (rather than key down) as it would focus the next field before and print y/n in the next input. Try it for fun :)

React Component Flickers on Rerender

I'm pretty new to react and have been working on this new page for work. Basically, there's a panel with filter options which lets you filter objects by color. Everything works but I'm noticing the entire filter panel flickers when you select a filter.
Here are the areas functions in the filter component I think bear directly on the filter and then the parent component they're inserted into. When I had originally written this, the filter component was also calling re render but I've since refactored so that the parent handles all of that - it was causing other problems with the page's pagination functionality. naturally. and I think that's kind of my problem. the whole thing is getting passed in then rerendered. but I have no idea how to fix it. or what's best.
checks whether previous props are different from props coming in from parent and if so, creates copy of new state, calls a render method with those options. at this point, still in child component.
componentDidUpdate(prevProps, prevState) {
if (prevState.selectedColorKeys.length !== this.state.selectedColorKeys.length ||
prevState.hasMatchingInvitation !== this.state.hasMatchingInvitation) {
const options = Object.assign({}, {
hasMatchingInvitation: this.state.hasMatchingInvitation,
selectedColorKeys: this.state.selectedColorKeys
});
this.props.onFilterChange(options);
}
}
handles active classes and blocks user from selecting same filter twice
isColorSelected(color) {
return this.state.selectedColorKeys.indexOf(color) > -1;
}
calls to remove filter with color name so users can deselect with same filter button or if its a new filter, sets state by adding the color to the array of selected color keys
filterByColor(color) {
if (this.isColorSelected(color.color_name)) {
this.removeFilter(color.color_name);
return;
}
this.setState({
selectedColorKeys:
this.state.selectedColorKeys.concat([color.color_name])
});
}
creating the color panel itself
// color panel
colorOptions.map(color => (
colorPicker.push(
(<li className={['color-filter', this.isColorSelected(color.color_name) ? 'active' : null].join(' ')} key={color.key} ><span className={color.key} onClick={() => { this.filterByColor(color); }} /></li>)
)
));
parent component
callback referencing the filter child with the onFilterChange function
<ThemesFilter onFilterChange={this.onFilterChange} />
onFilterChange(filters) {
const { filterThemes, loadThemes, unloadThemes } = this.props;
unloadThemes();
this.setState({
filterOptions: filters,
offset: 0
}, () => {
filterThemes(this.state.filterOptions.selectedColorKeys, this.state.filterOptions.hasMatchingInvitation);
loadThemes(this.state.offset);
});
}
when I place break points, the general flow seems to be :
filterByColor is triggered in event handler passing in that color
active classes are added to the color, a filter tag for that color is generated and appended
componentDidMount takes in the previous props/state and compares it to the new props/state. if they don't match, i.e state has changed, it creates a copy of that object, assigning the new states of what's changed. passes that as props to onFilterChange, a function in the parent, with those options.
onFilterChange takes those options, calls the action method for getting new themes (the filtering actually happens in the backend, all I really ever need to do is update the page) and passes those forward. its also setting offset to 0, that's for the page's pagination functionality.
It looks like the problem might be around the componentDidUpdate function which, after setting breakpoints and watching it go through the steps from filterByColor to componentDidMount, that componentDidMount loops through twice, checking again if the colorIsSelected, and throughout all that the color panel pauses to re-render and you get a flicker.
Is it possible creating the copy is causing it? since it's being treated, essentially, as a new object that isColorSelected feels necessary to double check? any help you guys have would be much appreciated, this shit is so far over my head I can't see the sun.
Can you change
componentDidUpdate(prevProps, prevState)
with
componentWillUpdate(nextProps, nextState)

Categories

Resources