How to select efficiently from a long list of options in react-select - react-select

My use case is to allow the user to select a ticker from a long list of about 8000 companies. I fetch all the companies when the component mounts, so I don't really need the async feature of react-select. The problem really is displaying and scrolling through the 8000 items (as described in several open issues like this one).
My thought is why display 8000 entries when the user can't do anything meaningful with such a big list anyway. Instead why not show a maximum of 5 matches. As the user types more, the matches keep getting better. Specifically:
When the input is blank, show no options
When the input is a single character, there will still be hundreds of matches, but show only the first 5
As the user keeps on typing, the number of matches will reduce, but still limited to 5. However they will be more relavant.
I am not seeing this solution mentioned anywhere, so was wondering if it makes sense. Also wanted to find out what's the best way to implement it with react-select. I have tried the following two approaches - can you think of a better way:
Approach 1: Use Async React Select
Although I don't need async fetching, I can use this feature to filter down the options. It seems to work very well:
const filterCompanies = (value: string) => {
const inputValue = value.trim().toLowerCase();
const inputLength = inputValue.length;
let count = 0;
return inputLength === 0
? []
: companies.filter(company => {
const keep =
count < 5 &&
(company.ticker.toLowerCase().indexOf(inputValue) >= 0 ||
company.name.toLowerCase().indexOf(inputValue) >= 0);
if (keep) {
count += 1;
}
return keep;
});
};
const promiseOptions = (inputValue: string) =>
Promise.resolve(filterCompanies(inputValue));
return (
<AsyncSelect<Company>
loadOptions={promiseOptions}
value={selectedCompany}
getOptionLabel={option => `${option.ticker} - ${option.name}`}
getOptionValue={option => option.ticker}
isClearable={true}
isSearchable={true}
onChange={handleChange}
/>
);
Approach 2: Use filterOption
Here I am using the filterOption to directly filter down the list. However it does not work very well - the filterOption function is very myopic - it gets only one candidate option at a time and needs to decide if that matches or not. Using this approach I cannot tell whether I have crossed the limit of showing 5 options or not. Net result: with blank input I am showing all 8000 options, as user starts typing, the number of options is reduced but still pretty large - so the sluggishness is still there. I would have thought that filterOption would be the more direct approach for my use case but it turns out that it is not as good as the async approach. Am I missing something?
const filterOption = (candidate: Option, input: string) => {
const { ticker, name } = candidate.data;
const inputVal = input.toLowerCase();
return (
ticker.toLowerCase().indexOf(inputVal) >= 0 ||
name.toLowerCase().indexOf(inputVal) >= 0
);
};
return (
<ReactSelect
options={companies}
value={selectedCompany}
filterOption={filterOption}
getOptionLabel={option => `${option.ticker} - ${option.name}`}
getOptionValue={option => option.ticker}
isClearable={true}
isSearchable={true}
onChange={handleChange}
/>
);

you can try using react-window to replace the menulist component
ref : https://github.com/JedWatson/react-select/issues/3128#issuecomment-431397942

Related

How can I save part of a string in an alias using Cypress?

I'm trying to save just a number from a string I get from a paragraph but when I try to asign an alias to it and then check the value it returns undefined. I've tried a few solutions I found but none of those seem to work for me. These are two ways I tried (I tried another one similar to the second one but using split, had same result). The console.log inside of the 'then' doesn't show in the console, and when I try the alias after the code is when I get undefined.
cy.get('p')
.eq(1)
.should('have.text', '/[0-9]+/g')
.as('solNumber')
cy.get('p')
.eq(1)
.invoke('text')
.then((text)=>{
var fullText = text;
var pattern = /[0-9]+/g;
var number = fullText.match(pattern);
console.log(number);
})
.as('solNumber')
Please convert with + operator and return the numeric value if you want numeric type to be stored.
cy.get('p').eq(1)
.invoke('text')
.then(fullText => {
const number = fullText.match(/[0-9]+/);
return +number // text to numeric
})
.as('solNumber')
cy.get('#solNumber')
.should('eq', 42) // numeric type
});
Running your 2nd code on this,
<p>21</p>
<p>42</p>
gives the correct outcome
cy.get('p')
.eq(1)
.invoke('text')
.then((text)=>{
var fullText = text;
var pattern = /[0-9]+/g;
var number = fullText.match(pattern);
console.log(number); // logs 42
})
.as('solNumber')
cy.get('#solNumber')
.should('eq', '42') // passes
So, you need to inspect the DOM, it looks like it's not what you expect.
The first attempt you were passing a jquery element to the .should() and although some chainers change the subject yours did not so it saved the jquery element as solNumber.
The second attempt invokes the .text() which was passed to the .then() it logs the number correctly. However, you did not return anything at the end of the .then() block, therefore, solNumber should hold the entire paragraph.
This should help you out to extract the specific number and save it as an alias.
cy.get('p')
.invoke('text')
.invoke('trim')
.then(paragraph => {
const matcher = /some/
expect(paragraph).to.match(matcher) // check number is there
const indexOfText = paragraph.match(matcher) // get index of match text
return paragraph.substring(indexOfText.index, indexOfText.index + indexOfText[0].length) // return substring
})
.as('savedText')
cy.get('#savedText')
.then(cy.log) // will print out the number you seek

Correct search statement when applying filters

I have created a search function for my React-native app, however, I am not quite sure how to create the correct search statement. What I have now is as follows. You can open a filter screen where you can type a few search criteria (for vehicles) so make, model, color, license plate.
After saving the filters you are re-directed to a result page. On this page, I populate a const with Redux data (the vehicle database) and then filter this data before showing it in flatlist.
const vehicles = useSelector(state => state.uploadedVehicles.vehicles)
const filters = props.navigation.getParam('savedFilters')
const filteredVehicles = vehicles.filter(vehicle =>
vehicle.make === filters.makeFilter ||
vehicle.model === filters.modelFilter ||
vehicle.color === filters.licenseplateFilter ||
vehicle
.licenseplate === filters.licenseplateFilter
)
...return Flatlist with filteredVehicles here...
If I set a filter for a particular Make, only vehicles from this make are found. If I set a filter for a model, only vehicles from this model are found. However, if I set a filter for two statements it now shows vehicles with one matching search criteria. If I would search for a blue Dodge I would find vehicles matching the make Dodge, but also every blue vehicle that is uploaded.
How can I expand my search function so It will show vehicles matching 1 filter, but if 2 or more filters are added it will combine these filters to a more specific search function?
I like to take another approach to this, also using Redux. Here I show you an example code:
case SAVE_FILTERS: {
const appliedFilters = state.filters; // Here you have your filters that have to be
initialized in your state. They also have to be turned to true or false but you can
do it in another function (See next one)
// Here we check every condition
const updatedData = state.data.filter(data => {
if (appliedFilters.filter1 && !data.filter1) {
return false;
}
if (appliedFilters.filter2 && !data.filter2) {
return false;
}
if (appliedFilters.filter3 && !data.filter3) {
return false;
} else {
return true;
}
});
// and now you will return an updated array of data only for the applied filters
return {
...state,
displayData: updatedLocations,
};
The trick is to check every single condition inside of the action. In this particular case, we are checking by filters that are true or not, but you can expand to other conditionals.
The flow for the code above is:
If we have a filter applied AND the data HAS that filter, then we pass the conditional. WE do this for all conditionals. If we pass all of them, return true which means it will be added to the display data and the user will see it.
I hope it helps.
Maybe this isn't the most beautiful way of getting the filter to work. But I didn't quite get my filter working with MIPB his response bit it did push me in the right direction.
I am passing the filters in appliedFilters. Then I constantly checking every filter with the part of the vehicle that is filtered for.
Starting with the make of the vehicle. If the make filter is nog set (so "") I just return the vehicles array which contains every vehicle, else I return every vehicle that is matched with the appliedFilters.makeFilters.
This new makeFilterArray is checked with the modelFilter. If this is not set just set it to the makeFilter array to continue checking other filters, if it is set check the makeFilterArray for the matching model.
Maybe not the best/most elegant solution, but with my limited knowledge I got it working! :-)
case FILTER_VEHICLES:
const appliedFilters = action.setFilters;
console.log(appliedFilters)
console.log(appliedFilters.makeFilter)
console.log(appliedFilters.modelFilter)
console.log(appliedFilters.licenseplateFilter)
console.log(appliedFilters.colorFilter)
const makeFilterArray = appliedFilters.makeFilter === "" ? vehicles : state.vehicles.filter(vehicle => vehicle.make === appliedFilters.makeFilter)
const modelFilterArray = appliedFilters.modelFilter === "" ? makeFilterArray : makeFilterArray.filter(vehicle => vehicle.model === appliedFilters.modelFilter)
const licenseplateFilterArray = appliedFilters.licenseplateFilter === "" ? modelFilterArray : modelFilterArray.filter(vehicle => vehicle.licenplate === appliedFilters.licenseplate)
const filteredArray = appliedFilters.colorFilter === "" ? licenseplateFilterArray : licenseplateFilterArray.filter(vehicle => vehicle.color === appliedFilters.color)
// and now you will return an updated array of data only for the applied filters
return {
...state,
filteredVehicles: filteredArray
};

How to create v-autocomplete which first shows items startsWith and then shows items indexOf

I would like to create a vuetify autocomplete with a custom filter that first shows the hits that start with the searchtext and then show the hits that not start with the searchtext, but have the searchtext somewhere in the middle.
I now have a custom filter like this, but this filter is not prioritizing words that start with the searchtext:
customFilter(item, queryText) {
const textOne = item.description.toLowerCase();
const textTwo = item.code.toLowerCase();
const searchText = queryText.toLowerCase();
return (
textOne.indexOf(searchText) > -1 || textTwo.indexOf(searchText) > -1
);
}
},
Codepen (Type 'ma' for example)
I believe you need to sort it manually, filter returns only true/false whether item is a match.
// https://stackoverflow.com/a/36114029/1981247
var _sortByTerm = function (data, term) {
return data.sort(function (a, b) {
// cast to lowercase
return a.toLowerCase().indexOf(term) < b.toLowerCase().indexOf(term) ? -1 : 1;
});
};
Then pass sorted array to items prop
<v-autocomplete :items="computedItems"
...
computed: {
computedItems() {
return _sortByTerm(this.states, this.search.toLowerCase())
},
},
Note this is just to get you started, and you might need to change code a bit according to the data (and filters) you are using, e.g. _sortByTerm works only on array of strings, but in the link there is a solution for sorting arrays of objects also.

Can We Get Vue to 'Detect Changes' to an Array If We Use `Filter()`?

First, I had this: parts = parts.filter(part => part.id !== change.doc.id);
So, data is an Array and it gets 'clobbered' with a new 'filtered' Array.
Vue didn't seem keen on detecting the change and updating my DOM.
So, I saw this. Specifically: To deal with caveat 2, you can use splice:
I refactored (or is it 'de-factored' b/c my code 'grew'?) to this:
// Get index of part removed
const index = parts.forEach((part, i) => {
if (part.id === change.doc.id) {
return i;
}
});
parts.splice(index, 1);
She works...but really? Do I have to do this way? 😬
parts.splice(parts.findIndex(part => part.id === change.doc.id), 1);
I got the splice in there while still keeping as 1 line, so m happy! 🤓

Validation .passes if null/empty/length == 0 doesn't fire

Validation doesn't seem to be called when the property has no value.
This is the code I'm using to try and make it work:
.ensure('baseContent.SetNamePrint').passes((name) =>
{
var webNameLength = this.baseContent.SetNameWeb.length;
var printNameLength = name.length;
console.log(webNameLength);
console.log(printNameLength);
if ((webNameLength > 1 && webNameLength < 51) || (printNameLength > 1 && printNameLength < 51)) {
return true;
}
return false;
}).withMessage('Web Name or Print Name is Required')
The passes only fires when the value of the property changes to something with a length, when it's empty (a blank string) nothing happens.
What I need is for the .passes() to be called every time there is a change to the value not just when there is a change and it has a value.
You need to additionally constraint the target property with isNotEmpty.
.ensure('baseContent.SetNamePrint').isNotEmpty().passes(...
From the documentation:
The isNotEmpty rule is always checked first before any other
validation rule. This means that without the isNotEmpty rule, the
.hasMinLength(5) rule would still consider a value of '' as valid
because the field is allowed to be empty.
PS: I heard that aurelia-validation is under rewriting. Perhaps that's why I can't find the documentation from the master branch anymore, but in another branch here
To get what I wanted I ended up with the following code.
this.validator = this.validation.on(this)
.ensure('SetNameWeb', (config) => {config.computedFrom(['SetNamePrint', 'SetNameWeb'])})
.if(() => { return this.HasImageEitherPrintNameOrWebName === false })
.isNotEmpty().withMessage('or "Print Name" is required')
.hasLengthBetween(0, 50)
.endIf()
.ensure('SetNamePrint', (config) => {config.computedFrom(['SetNameWeb', 'SetNamePrint'])})
.if(() => { return this.HasImageEitherPrintNameOrWebName === false })
.isNotEmpty().withMessage('or "Web Name" is required')
.hasLengthBetween(0, 50)
.endIf()
This gave me the functionality I needed with both of the fields being updated with each other.
However there was a bug in the aurelia code with the way it handles computedFrom that needed fixing to get this to work.
This problem was with aurelia-validation#0.6.8 though and there is now a new version which works in a completely different way so my recommendation is to update if you're having this problem.