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

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.

Related

Svelte {#if variable} block does not react to variable updates within the block

I would like to populate a table with visible rows in Svelte.
My current attempt relies on a {#if variable} test, where the rendered row updates the variable. Unfortunately, the test does not appear to react to changes to the variable. Perhaps this is as designed but the documentation does not appear to address this. Essentially:
<table>
<tbody>
{#each rows as row}
{#if renderIt==true}
<tr use:updateRenderIt>
<td>cell</td>
</tr>
{/if}
{/each}
</tbody>
</table>
I think my understanding of the timing is lacking :(. Perhaps the {#if} block cannot react to each renderIt change. There are quite a few examples of {#if} blocks, but none appear to rely on a variable which is changed within the block.
There is a running example in the Svelte playground. The console divider can be moved vertically to change the viewport dimensions.
If someone knows of a way to achieve this it would be appreciated! I can do it in traditional Javascript, but my Svelte expertise is limited :).
What I'm assuming you want is to have a state on each row when it is visible.
To do so you will need to store some data with your row, so instead of your row being a list of numbers and a single boolean to say if all rows are visible or not, it will be a list of objets that have a property visible:
let rows = [];
for (let i = 0; i < 100; i++) {
rows.push({
index: i,
visible: false,
});
};
Next, to capture when visibility changes on those rows, use Intersection Observer API:
let observer = new IntersectionObserver(
(entries, observer) => {
console.log(entries);
}
);
And use a svelte action to add that observer to elements:
<script>
...
let intersect = (element) => {
observer.observe(element);
};
</script>
<table>
<tbody>
{#each rows as row (row.index)}
<tr
use:intersect>
<td>{row.visible}</td>
</tr>
{/each}
</tbody>
</table>
To pass the intersecting state back to the element throw a custom event on it:
let observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
entry.target.dispatchEvent(
new CustomEvent("intersect", { detail: entry.isIntersecting })
);
});
}
);
And finally capture that event and modify the state:
<tr use:intersect
on:intersect={(event) => (row.visible = event.detail)} >
<td>{row.visible}</td>
</tr>
To render rows up to how many can fit on screen you could make the defaut state visible: true, and then wrap the element with an {#if row.visible}<tr .... </tr>{/if}. After the first event you would then remove the observer from the element using observer.unobserve by either updating the svelte action or in the observer hook.

how to use vue-glide-js events on nuxt and load more slides on last one

i'm new on all of these so i need help. first of all how vue-glide-js events work to begin with. its documentation just listed them without examples.
And second and more important, i wanna send an axios request at the end of my carousel and load more items. i couldn't work with events so used active slide and watched it and on last slide sent and updated my data but slide still shows the previous one. what should i do?
it is the simplified version of my code
<template>
<div>
<vue-glide v-bind="carouselOptions" v-model="active">
<vue-glide-slide v-for="i in array" :key="i">
Slide {{ i }}
</vue-glide-slide>
</vue-glide>
<p>{{active}}</p>
</div>
</template>
<script>
export default {
data(){
return{
carouselOptions:{
direction: process.env.SITE_DIRECTION,
autoplay: false,
perView: this.$device.isMobileOrTablet ? 4 : 8,
peek: { before: 0, after: 50 }
},
array: [1,2,3,4,5,6,7],
active: 0
}
},
watch:{
active(){
console.log('active = ' + this.active)
if(this.active > this.array.length - 3){
this.array= [1,2,3,4,5,6,7,8,9,10,11,12,13,14]
}
}
},
mounted(){
//
}
}
</script>
as you can see i manually added more items to my array but glide still show 7 items. i know it has something to do with mounting but dont know what to do.
and one more thing. if there is any better carousel that support rtl and breakpoint (items per view on different width) i would be happy to know. tanx
To use call a method at the end of slides just using #glide:run-end:
<template>
<div>
<vue-glide v-bind="carouselOptions" v-model="active" #glide:run-end="myMethod">
<vue-glide-slide v-for="i in array" :key="i">
Slide {{ i }}
</vue-glide-slide>
</vue-glide>
</div>
</template>
to show the new slides, it's a bit tricky! as far as i know the only way is to refresh (reload) the component! so put a key on vue-glide and change it after each new added slides. the only problem is, if there is images on slides, old ones also will be load again. (it's a suggestion and didn't tried my self yet, but maybe with a caching system, it may be solved)

For each results in v-for loop how can I nest another v-for loop using a parameter from the results of the first loop

Using a v-for loop in Vue js. I am looping through the readingTasks data object which correctly produces two results from the data below.
readingTasks:Array[2]
0:Object
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
url:"#"
1:Object
enabled:true
newunit:-1
task:"The part 3 training units"
unit:-1
unit_task_id:28
url:"#"
The bit I am unsure about is how for each result, how do I run another Axios database call that shows if the reading Task is complete or not. For example for the first record, the complete status should be true (unit_task_id:27) and the second record should be false.
userTasks:Array[1]
0:Object
complete:true
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
unit_task_user_id:21
<ul>
<li v-for="task in readingTasks">
{{task.task}}
//trying to call a function that does an Axios call passing in parameters from readingTasks
{{getUserTaskByUnit(task.unit, task.unit_task_id)}}
<template v-for="usertask in userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
//javascript if its useful
data: {
readingTasks: [],
userTasks: []
},
mounted() {
this.lastUnit();
},
methods: {
//functons
lastUnit: function() {
this.tasks();
},
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
self.readingTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {
});
},
getUserTaskByUnit: function(unit, unitTaskId) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + unit + "&unitTaskId=" + unitTaskId).then(function(response) {
self.userTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {});
}
This code seems close to doing the correct thing, however {{usertask.complete}} flickers between true and false for both sets of results. Like it is stuck in a loop.
I would expect the first result to show True here and the second result to show False.
The part 3 guide - true
The part 3 training units - false
There are a few problems here.
The template has a dependency on userTasks, so every time userTasks changes it will cause the component to re-render, running the template again.
Every time the template runs it calls getUserTaskByUnit for both tasks. That will, asynchronously, update userTasks. When userTasks is updated it will trigger a re-render, which will call getUserTaskByUnit again, going round and round in an infinite loop.
Worse than just being an infinite loop, each time it renders it will trigger two requests, each of which will trigger another re-rendering. The number of requests will balloon exponentially.
When those requests do return you're then storing them in userTasks. But both responses are being stored in exactly the same place, so you'll only ever see the results of one request in the UI.
The first thing you'll need is a better data structure for storing the responses in getUserTaskByUnit. The simplest place to store them would be on the tasks in readingTask. That might look something like this:
// Note the whole task is now being passed to getUserTaskByUnit
getUserTaskByUnit: function(task) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + task.unit + "&unitTaskId=" + task.unit_task_id).then(function(response) {
task.userTasks = response.data;
})
...
}
The call to getUserTaskByUnit needs moving out of the template. Moving it into the tasks method seems as good a place as any. There are also a few changes required to get it to work with the new version of getUserTaskByUnit:
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
var readingTasks = response.data;
// Pre-populate userTasks so it will be reactive
readingTasks.forEach(function(task) {
task.userTasks = [];
});
// This must come after userTasks is pre-populated
self.readingTasks = readingTasks;
readingTasks.forEach(function(task) {
// Passing task to getUserTaskByUnit, not unit and unit_task_id
self.getUserTaskByUnit(task);
});
})
...
Then within the template we'd need to loop over task.userTasks:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in task.userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
There are alternative data structures we could use depending on what other requirements you have. For example, you could retain a separate userTasks object to hold the userTasks but for that to work it would need to be a nested data structure rather than just an array. You'd need to key it by unit and then unitTaskId. The result in the template would be something like this:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in userTasks[task.unit][task.unit_task_id]">
{{usertask.complete}}
</template>
</li>
</ul>
Much like with the earlier solution you would need to pre-populate the userTasks with empty values when readingTasks first loads to ensure the values are reactive and also to avoid the template blowing up at the undefined entries. Alternatively you could use $set and suitable v-if checks respectively.
This is all quite fiddly. It may be that you can simplify it a little based on your knowledge of the system. For example, it may be possible to form compound string keys for userTasks rather than using two levels of nesting. Or it might be that unit is a prop that can be considered constant and doesn't need including in that data structure.
Your userTasks is a view property and gets overwritten upon every call to getUserTaskByUnit (i.e. for each item in readingTasks). What you instead want is a nested structure. You should call getUserTaskByUnit in a loop as soon as readingTasks got loaded, i.e. after the line self.readingTasks = response.data;, and store the response as a property for every readingTask object.

Vue Draggable with touch - drop doesn't trigger

I have been working on a website that has to function on both desktop and tablets. Part of the website is having three columns and being able to drag orders from column to column. Sometimes on drop, the user has to answer a few questions or change some of the data of that specific order. This happens in a pop-up window that is triggered by an #drop function (for example #drop="approved()". The method approved() then checks the status of the dropped order and shows the pop-up window).
When I am on desktop, everything works just fine. But when I switch to iPad Pro in the developer tools, nothing happens. I implemented Vue Draggable, which says to work with touch devices. In their examples I can't find anything about touch events or adding new handles for touch, so I don't know what to do now.
The dragging works just fine with touch devices, it's just the #drop function that doesn't trigger.
The dropzone (it includes a component that contains the draggables and a lot of if-statements):
<div class="col-md-4 border" #dragover.prevent #drop="approved()">
<Wachtrij class="fullHeight" :data2="opdrachtenData2"></Wachtrij>
</div>
The method:
export default {
methods: {
...
approved() {
console.log("Function approved() is being executed.")
if (this.draggingOrder.status === 5) {
this.popupGekeurd = true;
}
else if (this.draggingOrder.status === 6) {
this.popupTochGoed = true;
}
else if ([40, 52, 42,41,49,55,54].indexOf(this.draggingOrder.status) !== -1) {
this.back = true;
}
},
...
}
}
The problem seems to be that you are using native events, while the touch implementation does not (always?) use these events. It is intended that you use a draggable component with one of the events outlined in the documentation. In your case the start and end events look promising. This event has a few properties (docs), some of them being to and from.
Let's assume that we have the following code:
<draggable v-for="(zone, index) in zones" v-model="zones[index]" :class="['dropzone', `zone-${index}`]" :key="`dropzone-${index}`" :options="options" #start="start" #end="end">
<div v-for="item in zones[index]" class="dropitem" :key="`dropitem-${item.id}`">
{{ item.title }}
</div>
</draggable>
This creates a few zones, each filled with their own items. Each array item of zones is changed based on where you move each item. You can then use start to have information on when you start moving an item, and end to have information on when you stop moving an item, and where that item came from and where it ended up. The following methods show off what you can do with that in this case:
methods: {
start (event) {
console.log('start', event);
},
end (event) {
console.log('end', event);
const { from, to } = event;
if (to.className.match(/\bzone-2\b/)) {
console.log('Zone 2 has something added!')
}
if (from.className.match(/\bzone-0\b/)) {
console.log('Zone 0 had something removed!');
}
}
}
We make our dropzones with a class zone-0, zone-1 or zone-2 in this case, so we can use the class name to determine which dropzone we ended up in.
An alternative way to determine which zone was changed is to simply use a watcher. Since zones changes based on where you move items, you can simply watch a particular dropzone for changes and do things based on that.
watch: {
'zones.1': {
handler (oldZone, newZone) {
if (Array.isArray(oldZone) && Array.isArray(newZone) && oldZone.length !== newZone.length) {
console.log('Zone 1 was changed from', oldZone, 'to', newZone);
}
}
}
}
A full example can be found on codesandbox.

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)
}
}
},