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

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! 🤓

Related

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.

How to select efficiently from a long list of options in 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

Vue Object isn't returning updated value despite seeing the value updated

I'm writing tests for Vue.js and I'm trying to write the test to ensure that when some props are changed for pagination, the resulting values within the component are updated properly.
So when I console.log the component, I see the correctly updated values, but then when I try to literally grab that attribute it gives me the old and outdated value. Look at rangeList in the following screenshot to see what I mean:
Here is my code so that you see how what is generating this output.
pagComp.$refs.component.limit = 10;
pagComp.$refs.component.pages = 145;
console.log(pagComp.$refs.component);
console.log('RangList: ' + pagComp.$refs.component.rangeList.length);
Here is the code that modifies rangeList:
createListOfRanges() {
let range = [];
for(let i = 0; i < this.pages; i++) {
range.push(i);
}
this.rangeList = [];
while(range.length > 0) {
this.rangeList.push(range.splice(0, this.rangeLength));
}
this.correctLastRange();
},
Finally, there are two places this function is called: when the component is being created, and when the pages attribute changes. Here is the watcher:
watch: {
pages(val) {
this.createListOfRanges();
}
},
I see some issues with this part of your code:
while(range.length > 0) {
this.rangeList.push(range.splice(0, this.rangeLength));
}
range.splice(..) returns an array, which is getting pushed into this.rangeList
Forget about that for a minute. Look at the following example:
x = [1, 2, 3, 4]
x.splice(0, 2) // result: [1, 2]
As you can see above, splice returns an array, not an element. Now, in the same example above:
x = [1, 2, 3, 4]
y = [10, 11]
y.push(x.splice(0, 2))
Check the value of y. It will be [10, 11, [1, 2] ]. It is an array within another array. It does not look very meaningful here. You can do the above x and y experiments directly in your developer console.
In your case, your x happens to be the local range array within createListOfRanges method, and your y is this.rangeList that belongs to your Vue component.
Can you check your app logic at around that area, and see if that is really what you want to do?
For debugging Vue.js apps, you also need Vue devtools: https://github.com/vuejs/vue-devtools - it is much better than console.log()
While #Mani is right on the line of code giving you issues is your push to rangeList.
createListOfRanges() {
let range = [];
for(let i = 0; i < this.pages; i++) {
range.push(i);
}
this.rangeList = [];
while(range.length > 0) {
this.rangeList.push(range.splice(0, this.rangeLength));
}
this.correctLastRange();
},
pushing the result of the splice just makes a single element with all the elements of range in it.
try changing it to
this.rangeList.push(range.shift());
Though your function could be simplified by pushing the for(let i = 0; i < this.pages; i++) { i value directly to rangeList unless that's a simplification.
This JSFiddle shows what I'm talking about.
I appreciate the answers above, however they aren't what the issue was.
The problem was with Vue's lifecycle. I'm not 100% sure why, but when the page and limit variables are changed it takes another moment for the page watcher (shown above) to get executed and update the component. So thus it wouldn't show up in my tests. So what I did was use nextTick like so, which fixed the problem.
pagVue.limit = 10; // limit and pages change together every time automatically
pagVue.pages = 145;
Vue.nextTick(() => {
expect(pagination.rangeList.length).toBe(25);
})

Object collection optimisation

I'm struggling with something I'm sure I should be able to do quite quick with Linq-to-Obj
Have 27 flowers, need a collection of Flower[] containing 5 items split over, roughly 6 records;
List<Flowers[]> should contain 6 Flowers[] entities, and each Flowers[] array item should contain 5 flower objects.
I currently have something like:
List<Flowers[]> flowers;
int counter = 0;
List<Flowers>.ForEach(delegate (Flower item) {
if (counter <= 5){
// add flowers to array, add array to list
}
});
I'm trying to optimise this as it's bulky.
[Update]
I can probably to an array push on objects, removing the items I'v already run through, but is there not an easier way?
var ITEMS_IN_GROUP = 5;
var result = list.Select((Item, Index) => new { Item, Index })
.GroupBy(x => x.Index / ITEMS_IN_GROUP)
.Select(g => g.Select(x=>x.Item).ToArray())
.ToList();
You may also want to try morelinq's Batch