Clearing the "cache" of computed properties upon each reload - vue.js

I have this code:
TEMPLATE:
<b-dropdown class="dropdown-small dropdown-channel" text="Axes">
<b-dropdown-item v-for="(item, index) in chartAxes" :key="index" #click="displayAxis(item)">
{{ item }}
</b-dropdown-item>
</b-dropdown>
COMPUTED:
chartAxes(): string[] {
const axes = this.chart?.axes?.items
?.filter((axis: Tee.IAxis) => axis.title.text != "")
?.map((axis: Tee.IAxis) => axis.title.text)
if (axes === undefined) {
return [];
}
return axes;
},
What this does is show the different lines in a graph in a dropdown (and one can choose a specific line to populate the left-hand axis with).
The issue I am facing is every time I load a new graph, the available lines are just accumulated in the dropdown. What I want to do is to clear it and re-populate it with the new lines. Tried to push this into an array and then clear that, but doesn’t work.

Related

Vuetify: How to show overlay with spinner while v-data-table renders (how to wait for a component to finish rendering)

I have a Vuetify v-data-table with about a 1000 rows in it. Rendering it for the first time and when text searching on it, the render takes a few seconds. During this time I want to overlay it with a scrim + spinner to indicate things are happening.
The overlay:
<v-overlay
:value="loading"
opacity="0.7"
absolute
v-mutate="onMutate"
>
<v-progress-circular indeterminate size="32" color="blue"></v-progress-circular>
</v-overlay>
The v-data-table:
<v-data-table
:headers="cHeaders"
:items="cLimitedData"
disable-pagination
hide-default-footer
:search="searchIntercepted"
#current-items="tstCurrentItems"
>
The computed variable cLimitedData:
cLimitedData() {
if (this.indNoLimit) {
return this.data
} else {
return this.data.slice(0,this.dtRowTo)
}
},
I watch the search variable, and when it changes, I set loading to true to activate the overlay.
watch: {
search() {
if (!this.indNoLimit) {
// remove limit, this will cause cLimitedData to return all rows
this.loading = true
// -> moved to onMutate
//this.$nextTick(function () {
// this.indNoLimit = true
//})
} else {
this.searchIntercepted = this.search
}
},
However, the overlay doesn't activate until after the v-data-table had finished rendering. I've tried a million things, one of them is to put a v-mutate="onMutate" on the overlay, and only when it fired, would I this.indNoLimit = true to set things in motion, but that is still not good enough to have the scrim start before the v-data-table begins reloading itself.
onMutate(thing1) {
console.log('#onMutate',thing1)
this.$nextTick(function () {
this.indNoLimit = true
this.searchIntercepted = this.search
})
},
I also found that the next tick in #current-items fairly reliably marked the end of the render of the v-data-table, thus the deactivation of the scrim is probably going to be ok:
tstCurrentItems(thing1) {
console.log('#current-items',thing1)
this.$nextTick(function () {
console.log('#current-items NEXT')
this.loading = false
})
I believe my question should be: how can I detect/wait for components to have rendered (the v-overlay+v-progress-circular) before making changes to other components (the v-data-table).
Note: To solve the initial wait time of loading of the table, I found a way to progressively load it by inserting div-markers that trigger a v-intersect. However, this does not solve the situation when a user searches the whole data set when only the first 50 rows are loaded.
EDIT: Tried to start the update of the table after the overlay has been activated using https://github.com/twickstrom/vue-force-next-tick, but still no luck. It almost looks like vue tries to aggregate changes to the DOM instead of executing them one by one.
I have not figured out why the v-data-table seems to block/freeze, but here is a solution that can streamline any large v-data-table:
The v-data-table:
<v-data-table
:headers="cHeaders"
:items="data"
:items-per-page="dtRowTo"
hide-default-footer
hide-default-header
:search="search"
item-key="id"
>
<template
v-slot:item="{item, index, isSelected, select, headers}"
>
<tr>
<td
:colspan="headers.length"
>
Stuff
<div v-if="(index+1)%25==0" v-intersect.quiet.once="onIntersect">{{index+1}}</div>
</td>
</tr>
</template>
</v-data-table>
Add a div, or any other element, to intersect with at given intervals. Every time we intersect with it, we're going to increase the page size.
Variables:
dtRowTo: 50, // initial nr of rows
dtRowIncr: 50, // increments
The onIntersect:
onIntersect (entries, observer) {
let index = entries[0].target.textContent
if (index == this.dtRowTo) {
this.dtRowTo += this.dtRowIncr
}
},
Unfortunately you cannot add the index as an argument like v-intersect.quiet.once="onIntersect(index)", as this executes the function before you intersect with it (not sure why), so we will have to take the index out of the textContext.
Basically what you do is you increase the page size every time you're at the bottom. Search will still operate on the whole dataset.
What does not work (which I found out the hard way), is incrementally increasing the items, like the computed function below. As search needs the entire dataset to be present at :items, this won't work:
<v-data-table
:items="cLimitedData"
computed: {
cLimitedData() {
return this.data.slice(0,this.dtRowTo)
},
}
Doing so is fine (I guess?) as long as you don't need search or anything that operates on the entire data set.

List of components not updating

I have this template displaying a list of components:
<template>
<v-container id="container">
<RaceItem v-for="(item, index) in items" :key="item + index" />
(...)
When the array "items" is updated (shift() or push() a new item), the list displayed is not updated.
I'm sure I'm missing something on how Vue works... but could anyone explain what's wrong?
The key attribute expects number | string | boolean | symbol. It is recommended to use primitives as keys.
The simplest usage is to use a primitive unique property of each element to track them:
<RaceItem v-for="item in items" :key="item.id" />
... assuming your items have unique ids.
If you use the index as key, every time you update the array you'll have to replace it with an updated version of itself (i.e: this.items = [...items]; - where items contains the mutation).
Here's how your methods would have to look in that case:
methods: {
applyChangeToItem(item, change) {
// apply change to item and return the modified item
},
updateItem(index, change) {
this.$set(
this.items,
index,
this.applyChangeToItem(this.items[index], change)
);
},
addItem(item) {
this.items = [...this.items, item];
},
removeItem(index) {
this.items = [...this.items.filter((_, i) => index !== i)];
}
}
Can you try something like this
Put componentKey on v-container and use forceRender() after your push is done

How to keep track of an array change in Vue.js when the index is a dynamic value?

I am building an app using Node.js and Vue.
My DATA for the component is the following:
data() {
return {
campaign: {
buses: [],
weeks: [
{
oneWayBuses: [],
returnBuses: []
}
]
},
busesMap: {
// id is the bus ID. Value is the index in the campaign.buses array.
},
};
},
I fill the buses and weeks array in MOUNTED section in two separate methods after getting the data from the server:
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
So the idea is that my busesMap looks like busesId keys and index values:
busesMap = {
'k3jbjdlkk': 0,
'xjkxh834b': 1,
'hkf37sndd': 2
}
However, when I try to iterate over weeks, v-if does not update so no bus info is shown:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[busesMap.busId]">
<strong>{{ campaign.buses[busesMap.busId].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap.busId].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
On the other side, if I shorten the v-if condition to campaign.buses, then I get into the condition but campaign.buses[busesMap.busId] is still undefined, so I get an ERROR trying to display busLabel and oneWayDepartureDate
I've read vue in depth documentation, but couldn't come up with a resolution.
Any gotchas you can find out?
Try this:
async mounted(){
await responseForWeeks
await responseForBuses
responseForWeeks => {
responseForWeeks.forEach(
week => this.campaign.weeks.push(week);
)
}
// this is partial since it is what you provided
responseForBuses => {
responseForBuses.forEach(
bus => this.campaign.buses.push(bus);
// Here I also fill the busesMap to link each week to its bus index in the array of buses
this.busesMap[bus.id] = this.campaign.buses.length - 1;
)
}
}
Basically you want to make sure that before your component loads your data is in place. You can also create computed properties which will force re rendering if dependencies are changed and they are in the dom.
Actually, the problem was indeed in the HTML.
When trying to access the object keys, better use [] intead of a dot .
Final HTML result would be as follows:
<ul>
<li
v-for="(busId, index) in week.oneWayBuses"
:key="index"
:item="busId"
>
<span v-if="campaign.buses[[busesMap[busId]]]">
<strong>{{ campaign.buses[busesMap[busId]].busLabel }}</strong>
leaves on the
<span>{{ campaign.buses[busesMap[busId]].oneWayDepartureDate.toDate() | formatDate }}</span>
</span>
</li>
</ul>
What was happening is that previously campaign.buses[busesMap.busId] did not exist, thus not rendering anything. Solved to campaign.buses[busesMap[busId]]. Also used claudators for the displayed moustache sintach.
Hope it helps someone else messing with Objects!

Datatable vuetify select multiple rows (Shift+Click)

I am using Datatable from Vuetify 1.5.x
Have enabled the checkboxes so that multiple rows can be selected, I would like to be able to select with Shift + Click so that I don't have to click on each checkbox exact same as Gmail works.
It wouldn't be difficult to do if I had a row id that changes by the sort or if the rows array was reordered when the data table is sorted. But none of these seem to work.
Has anyone achieve this with vuetify datatable?
<template v-slot:items="props">
<tr :active="props.selected" #click="selectRow(props);">
<td>
<v-layout>
<v-flex>
<v-checkbox
:input-value="props.selected"
primary
hide-details
:class="{ 'red--text': props.item.was_modified_recently == 1 }"
></v-checkbox>
</v-flex>
</td>
</tr>
</template>
Vuetify documentation example
I actually had to solve this today.
My solution relied on using the item-selected hook and method that performs the bulk selection.
methods: {
bulkSelect({ item: b, value }) {
const { selectedRows, current, shiftKeyOn } = this;
if (selectedRows.length == 1 && value == true && shiftKeyOn) {
const [a] = selectedRows;
let start = current.findIndex((item) => item == a);
let end = current.findIndex((item) => item == b);
if (start - end > 0) {
let temp = start;
start = end;
end = temp;
}
for (let i = start; i <= end; i++) {
selectedRows.push(current[i]);
}
}
},
}
So that's the meat of it. There's other housekeeping details like keeping track of when shift is being held down, and preventing the browser from highlighting the text of the table when holding down shift and clicking the second row.
I made a codepen showing this functioning here.
https://codepen.io/ryancwynar/pen/jOWoXZw
In the new version of vuetify i.e. 2.0
You question is solved easily the following way
I have put the answer in the following Stack Overflow question link
Vuetify added prepend-item slot that lets you add a custom item before listing items by which we can add "select all"
Please check the example in codepen on pretend-item for select all checkboxes
I've got the same case as you faced.
First of all, you got to add another events (keyboard events) on the <tr>
tag and pass the props as argument.
And then use slice to the items array for determined range of the selected items you want.
In my case, i've stored the selected array inside vuex store.
hoped i can helped ;)
<template v-slot:items="props">
<tr :active="props.selected" #click.shift="useShift(props)" #click="selectRow(props)">
<td>
<v-layout>
<v-flex>
<v-checkbox
:input-value="props.selected"
primary
hide-details
:class="{ 'red--text': props.item.was_modified_recently == 1 }"
></v-checkbox>
</v-flex>
</td>
</tr>
</template>
export default {
methods:{
...mapMutations(["setSelected"]),
useShift({ index }) {
this.setSelected(
this.items.slice(
this.items.findIndex(el => {
return this.selected.find(v => v.track.id == el.track.id);
}),
index
)
);
},
}
}

Vue + Vue-Paginate: Array will not refresh once empty

I am using vue-paginate in my app and I've noticed that once my array is empty, refreshing its value to an array with contents does not display.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
>
<transition-group name="zoom">
<div v-for="recipe in paginated('recipes')" :key="recipe.id">
<recipe class=""
:recipe="recipe"
:ref="recipe.id"
></recipe>
</div>
</transition-group>
</paginate>
This is how things get displayed, and my recipe array changes depending on a search. If I type in "b" into my search, results for banana, and bbq would show. If I typed "ba" the result for bbq is removed, and once I backspace the search to "b" it would re-appear as expected.
If I type "bx" every result is removed and when I backspace the search to "b", no results re-appear.
Any idea why this might happen?
UPDATE
When I inspect the component in chrome I see:
currentPage:-1
pageItemsCount:"-15-0 of 222"
Even though the list prop is:
list:Array[222]
Paginate needs a key in order to know when to re-render after the collection it's looking at reaches a length of zero. If you add a key to the paginate element, things should function as expected.
<paginate
name="recipes"
:list="recipes"
:per="16"
class="p-0"
:key="recipes ? recipes.length : 0" // You need some key that will update when the filtered result updates
>
See "Filtering the paginated list" is not working on vue-paginate node for a slightly more in depth answer.
I found a hacky workaround that fixed it for my app. First, I added a ref to my <paginate></paginate> component ref="paginator". Then I created a computed property:
emptyArray () {
return store.state.recipes.length == 0
}
then I created a watcher that looks for a change from length == 0 to length != 0:
watch: {
emptyArray: function(newVal, oldVal) {
if ( newVal === false && oldVal === true ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator.goToPage(page)
}
}, 100)
}
}
}
The timeout was necessary otherwise the component thought there was no page 1.
Using :key in the element has certain bugs. It will not work properly if you have multiple search on the table. In that case input will lose focus by typing single character. Here is the better alternative:
computed:{
searchFilter() {
if(this.search){
//Your Search condition
}
}
},
watch:{
searchFilter(newVal,oldVal){
if ( newVal.length !==0 && oldVal.length ===0 ) {
setTimeout(() => {
if (this.$refs.paginator) {
this.$refs.paginator[0].goToPage(1)
}
}, 50)
}
}
},