How do I lazy load item lists on Vuejs and Vuetify's lazyload? - vue.js

Im trying to make an infinite scroll list but it's really not lazy loading, been stuck with this for hours, the whole list will come in.
.....
<v-col
v-for="(post, i) in posts"
:key="i"
cols="12"
>
<v-lazy
v-model="isActive"
:options="{
threshold: .5
}"
transition="fade-transition"
>
{{Content here}}
</....
API used for test : https://jsonplaceholder.typicode.com/posts

There is a new virtual-scroller component, but it doesn't work with responsive content like grid rows/cols. Instead use v-lazy...
I discovered that the columns need to have defined min-height (approx. to the expected height of the cards) in order for the v-lazy intersection observer to work. Use something like a v-sheet or v-responsive to set the min-height and contain the cards.
Also bind the v-model of the v-lazy to each post (ie: post.isActive), instead of a global isActive var...
<v-col lg="3" md="4" sm="6" cols="12" v-for="(post, index) in posts">
<v-sheet min-height="250" class="fill-height" color="transparent">
<v-lazy
v-model="post.isActive" :options="{
threshold: .5
}"
class="fill-height">
<v-card class="fill-height" hover>
<v-card-text>
<v-row :key="index" #click="">
<v-col sm="10" cols="12" class="text-sm-left text-center">
#{{ (index+1) }}
<h2 v-html="post.title"></h2>
<div v-html="post.body"></div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-lazy>
</v-sheet>
</v-col>
Demo: https://codeply.com/p/eOZKk873AJ

I can suggest another solution with v-intersect, which works perfect for me.
Sorry, the snippet may be not working as composed of my code, but the idea should be pretty clear
<template>
<v-list class="overflow-y-auto" max-height="500">
<v-list-item v-for="item in items">
{{ item.name }}
</v-list-item>
<v-skeleton-loader v-if="moreDataToAvailable" v-intersect="loadNextPage" type="list-item#5" />
</v-list>
</template>
<script lang="ts">
import Vue from 'vue'
const pageSize = 10
export default Vue.extend({
data(): any {
return {
pageLoaded: 0,
totalCount: 100,//fetch from API
items: []
}
},
computed: {
moreDataToAvailable (): boolean {
return Math.ceil(this.totalCount / pageSize) - 1 > this.pageLoaded
}
},
methods {
async loadNextPage (entries: IntersectionObserverEntry[]) {
if (entries[0].isIntersecting && this.moreDataToAvailable) {
const nextPage = this.pageLoaded + 1
const loaded = await this.loadPage(nextPage) //API call
loaded.data.forEach((item: any) => this.items.push(item))
this.totalCount = loaded.totalCount
this.pageLoaded = nextPage
}
},
}
})
</script>

Related

V-select issue in Vuetify 3

I'm using Vuetify 3.0.0-beta.0 ~ for my project (because it is the only version that supports vue3), and having a bit weird issue
I want to implement the same thing as described there https://codepen.io/reijnemans/pen/vYNadMo?editors=1010 with v-select involved, so I was needed to use Vuetify
copied snippet
<v-select
:items="items"
label="Standard"
>
<template v-slot:selection="{ item, index }">
<img :src="item.image">{{ item.name }}</template>
</template>
<template v-slot:item="{ item }">
<img :src="item.image">{{ item.name }}</template>
</v-select>
My Component:
<template>
<div class="resourceSelectors">
<v-col cols="10" lg="4" class="mx-auto">
<div class="text-center">
<h2 class="indigo--text" style="margin-bottom: 30px">Some Test H2</h2>
</div>
<v-col class="d-flex" cols="12" sm="6">
<v-select
:items="items"
label="Standard">
<template v-slot:selection="{ item }">
<img :src="item.image">{{ item.name }}
</template>
<template v-slot:item="{ item }">
<img :src="item.image">{{ item.name }}
</template>
</v-select>
</v-col>
</v-col>
</div>
</template>
<script>
import { mapState } from "vuex";
/* eslint-disable */
export default {
name: "testComponent",
data() {
return {
// hardware Configuration Validation Rules
items: [
{ name: 'Foo', image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'},
{ name: 'Bar', image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'},
{ name: 'Hoo', image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'},
{ name: 'Coo', image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'}],
}
}}
When I'm trying to run the above component I always get this weird error Failed setting prop "type" on <select>: value text is invalid. TypeError: Cannot set property type of #<HTMLSelectElement> which has only a getter,
Did anyone faced similar issue before?
In Vuetify 3, you need some workarounds to style the items in v-select, because the item slot resets the entire styling.
You should use the menu-props, with it you can pass props through to the v-menu component. It accepts an object with anything from /api/v-menu. This allows you to close the field on click.
In the item slot, you should use a v-list-item with an #click property to set the model.
I made an example here with a selection of symbols:
<script setup>
const symbols = [
'ab-testing',
'abacus',
'account',
'account-alert',
]
const form = { symbol: '', }
</script>
<template>
<v-select
v-model="form.symbol"
:items="symbols"
label="Symbol"
:prepend-inner-icon="'mdi-'+form.symbol"
:menu-props="{
closeOnClick: true,
closeOnContentClick: true,
}"
>
<template v-slot:selection="{ item, index }">
{{ item.value }}
</template>
<template v-slot:item="{ item, index }">
<v-list-item
:title="item.title"
:prepend-icon="'mdi-'+item.title"
#click="form.symbol = item.title"
>
</v-list-item>
</template>
</v-select>
</template>
I hope it helps you.
I couldn't find correct solution but I just wanted to share what I did about scoped slot. I think we should use item.raw to access name and image. And the next problem is how to make it clickable to trigger select event that I didn't know yet :(
const { createApp } = Vue
const { createVuetify } = Vuetify
const vuetify = createVuetify()
const app = createApp({
data() {
return {
value: null,
items: [
{
name: 'Foo',
image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'
},
{
name: 'Bar',
image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'
},
{
name: 'Hoo',
image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'
},
{
name: 'Coo',
image: 'https://www.gravatar.com/avatar/b17065ea1655f1e3283aac8d8fc16019?s=48&d=identicon&r=PG'
}
]
}
}
});
app.use(vuetify).mount('#app');
<link href="https://cdn.jsdelivr.net/npm/vuetify#3.0.0-beta.9/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#3.0.0-beta.9/dist/vuetify.min.js"></script>
<div id="app">
<div class="resourceSelectors">
<v-col cols="10" lg="4" class="mx-auto">
<div class="text-center">
<h2 class="indigo--text" style="margin-bottom: 30px">Some Test H2</h2>
</div>
<v-col class="d-flex" cols="12" sm="6">
<v-select
v-model="value"
:items="items"
item-title="name"
item-value="name"
label="Standard">
<template v-slot:item="{item}">
<v-list-item
:prepend-avatar="item.raw.image"
:title="item.raw.name"
/>
</template>
</v-select>
</v-col>
</v-col>
</div>
</div>

Nuxt, Vuetify skeleton loader while image loads

I cannot find one proper example of loading an image with a skeleton loader. I'm using nuxt + Vuetify and I'm trying to use a simple image with a skeleton loader.
Here is my code.
<template>
<v-skeleton-loader v-if="loading" :loading="loading" type="image">
<v-card
v-show="loaded"
class="ma-auto elevation-4"
shaped
color="darkgreen"
width="500"
flat
>
<v-img
src="/products/em-lucky-combo.jpg"
max-width="500"
#load="hasLoaded"
>
</v-img>
</v-card>
</v-skeleton-loader>
</template>
my method
<script>
export default {
methods: {
hasLoaded() {
console.log('Image finished loading')
this.loading = false
this.loaded = true
},
},
}
</script>
But as I said the method never gets called when it is inside the v-card tag or directly in the skeleton loader.
I have tried using #load, I have tried using mounted hooks like below.
<script>
export default {
mounted() {
const readyHandler = () => {
if (document.readyState === 'complete') {
console.log('Document Rendered')
this.loading = false
this.loaded = true
document.removeEventListener('readystatechange', readyHandler)
}
}
document.addEventListener('readystatechange', readyHandler)
readyHandler() // in case the component has been instantiated lately after loading
},
}
</script>
But nothing seems to work properly or elegantly. Is it just not possible? The moment the <v-img> tag is inside the skeleton loader or inside a v-card inside a skeleton loader it never gets rendered no matter what I do. One suggestion was to use a slot. I'm guessing it has something to do with slots but I do not understand how to use these.
I tried doing something like this.
<template v-slot:default>
<v-card
class="ma-auto elevation-4"
shaped
color="darkgreen"
width="500"
transition="fade-transition"
flat
>
<v-img
src="/products/em-lucky-combo.jpg"
max-width="500"
transition="fade-transition"
#load="imageLoaded"
></v-img>
</v-card>
</template>
I tried using the code below.
How to make v-skeleton loader inside v-for in Vuetify
<v-img>
<template #placeholder>
<v-sheet>
<v-skeleton-loader />
</v-sheet>
</template>
</v-img>
At the end, OP had enough of using just some dimensions and a lazy loader image. Since images were local, there was no need for a skeleton.
<template>
<v-card class="ma-auto elevation-4" flat shaped width="500">
<v-img
aspect-ratio="1"
class="grey lighten-2"
lazy-src=""
max-height="350"
max-width="500"
src="/"
transition="fade-transition"
>
<template v-slot:placeholder>
<v-row
align="center"
class="fill-height ma-0"
justify="center"
>
<v-progress-circular
color="grey lighten-5"
indeterminate
></v-progress-circular>
</v-row>
</template>
</v-img>
</v-card>
</template>
Thanks to #kissu I realised my approach was wrong and unnecessary . I ended up giving my images some set dimensions and using a lazy image loader with a placeholder slot which works just fine. As he mentioned my images are loaded locally so it wont really work in my case.
What I did in the end.
<template>
<v-card class="ma-auto elevation-4" flat shaped width="500">
<v-img
aspect-ratio="1"
class="grey lighten-2"
lazy-src=""
max-height="350"
max-width="500"
src="/"
transition="fade-transition"
>
<template v-slot:placeholder>
<v-row
align="center"
class="fill-height ma-0"
justify="center"
>
<v-progress-circular
color="grey lighten-5"
indeterminate
></v-progress-circular>
</v-row>
</template>
</v-img>
</v-card>
</template>

displaying highlighted list items in vuetify 2.x

I'm writing my first app in vuetify 2.x. The app is displaying a list of items which are objects with a text (item.t) string field and a checked (item.c) boolean field.
I would like to display checked items in the current theme color and the unchecked items in the opposite theme color (highlighted). It thus depends on the value of the item.c field value.
I assume that changing the them of the list item will kind of reverse the colors of its content. Black <-> white.
How could I do that ?
This is my list component:
<template>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.r">
<v-list-item-content class="font-weight-medium">
<v-layout>
<v-row align="center">
<v-col cols="2">
<v-row no-gutters justify="end">
{{ item.n }}
</v-row>
</v-col>
<v-col cols="10" class="px-0">
<v-row no-gutters>{{ item.t }}</v-row>
</v-col>
</v-row>
</v-layout>
</v-list-item-content>
</v-list-item>
<v-divider
v-if="index < items.length - 1"
:key="`divider-${index}`"
></v-divider>
</template>
</v-list>
</template>
<script>
export default {
name: "itemList",
computed: {
items() {
return this.$store.getters.currentListItems;
},
},
};
</script>
I tried many things without success and couldn't find an example how to do that.
Edit: since the items contains just text and no icons, maybe it's enough the change the background and text color. The nice thing of theme is that it also reverse icons.
You can do it by different ways by v-menu by v-select or v-combo-box
and here you can use option multiple
https://vuetifyjs.com/en/components/combobox/#dense
but i think you need to use combo-box
and here you have slots
freedom of thought
I finally solved it. It's a bit hacky but it does the job.
The solution is to use a style computed with a method receiving the item object as argument. The hacky part is the way I change the style.
The highlighted text and background colors swap with the dark or light setting. Switching the them switches all the colors.
<template>
<v-list dense>
<template v-for="(item, index) in items">
<v-list-item :key="item.r" v-bind:style="highlighted(item)">
<v-list-item-content class="font-weight-medium">
<v-layout>
<v-row align="center">
<v-col cols="2">
<v-row no-gutters justify="end">
{{ item.n }}
</v-row>
</v-col>
<v-col cols="9" class="px-0">
<v-row no-gutters>{{ item.t }}</v-row>
</v-col>
</v-row>
</v-layout>
</v-list-item-content>
</v-list-item>
<v-divider
v-if="index < items.length - 1"
:key="`divider-${index}`"
></v-divider>
</template>
</v-list>
</template>
<script>
export default {
name: "itemList",
computed: {
items() {
return this.$store.getters.currentListItems;
},
},
methods: {
highlighted(item) {
let backColor = "white";
let textColor = "#1E1E1E";
if (
(this.$vuetify.theme.dark && item.c) ||
(!this.$vuetify.theme.dark && !item.c)
) {
[textColor, backColor] = [backColor, textColor];
}
return {
"background-color": backColor,
color: textColor,
};
},
},
};
</script>

Can you please help me with my on step behind issue in my vue code?

I got a question about my vue code I'm making a filter dropdown but when I input a key to trigger the key down event for filtering the arr it's changing the dom each time after the second event (one step behind).
Here is the code pen :
https://codepen.io/dyonvangerwen/pen/zYvjMdY
it's only keeping the values in the arr that are matching the input
template:
<div id="app">
<v-app id="inspire">
<v-form>
<v-container>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-text-field
v-model="inputValue"
label="Filled"
placeholder="Placeholder"
filled
v-on:keydown="tester"
></v-text-field>
<v-card
class="mx-auto"
max-width="400"
tile
>
<v-list-ite >
<v-list-item-content v-for=" item in itemsInDropdown" :key="item">
<v-list-item-title>{{item}}</v-list-item-title>
</v-list-item-content>
</v-list-ite>
</v-card>
</v-col>
</v-row>
</v-container>
</v-form>
</v-app>
</div>
script:
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
inputValue:'',
itemsInDropdown:['a','b','c','d','e','ab','cd','ea']
}),
methods:{
tester:function(){
this.itemsInDropdown = this.itemsInDropdown.filter((x)=>{
if(x.includes(this.inputValue)){
return true
}
else{return false}
})
}
}
})
It is better to use computed in this case:
Replace the method with this:
computed:{
itemsInDropdownFiltered:function(){
return this.itemsInDropdown.filter((x)=>{
return x.includes(this.inputValue);
});
}
}
Change the array to be rendered from itemsInDropdown to itemsInDropdownFiltered as follows:
<v-list-item-content v-for="item in itemsInDropdownFiltered" :key="item">

Extracting the information in a prop in a Vue child component

I'm passing a object as a prop to a child component but I can't reference one of its elements (user_id) to use in a method in that child component.
My code (slightly abbreviated) is:
<template>
<div class="back">
<v-app id="inspire">
<v-content>
<v-container fluid>
<v-card flat>
<v-card-title>
<div>
<div class="headline">
{{data.title}}
</div>
<span class="grey--text">{{data.user}} said {{data.created_at}}</span>
</div>
<v-spacer></v-spacer>
<v-badge color="deep-orange accent-3" left overlap>
<span slot="badge">7</span>
<v-icon color="grey lighten-1" large>
insert_comment
</v-icon>
</v-badge>
<!--<v-btn color="deep-orange accent-3">5 replies</v-btn>-->
</v-card-title>
<v-card-text v-html="data.body"></v-card-text>
<v-card-actions v-if="own">
<v-btn icon small>
<v-icon color="deep-orange accent-3">edit</v-icon>
</v-btn>
<v-btn icon small>
<v-icon color="red">delete</v-icon>
</v-btn>
</v-card-actions>
</v-card>
<return-button></return-button>
</v-container>
</v-content>
</v-app>
</div>
</template>
<script>
export default {
name: "ShowQuestion",
props: ['data'],
data() {
return {
own: this.Own(),
}
},
methods: {
Own: function () {
return this.UserID() == this.user_id <---HERE BE DRAGONS! (reported as 'undefined')
},
UserID: function () {
... returns the 'sub' from the JWT token
return sub;
}
},
}
</script>
While the correct information is being displayed in the template, I also need to be able to compare the user's ID from the token with that contained in the prop (i.e. data.user_id). My reading suggests the solution will involve converting the object to an array, but that's beyond my current skill level too. I'd appreciate some pointers to a solution.
Thanks,
Tom
If you can render data.user_id in your template you can use it anywhere, but I'd probably do something like this to solve your problem:
props: ['data']
data() {
return {
}
},
computed: {
UserId() {
return //however you get your id
}
},
Then your v-if could just be this:
<v-card-actions v-if="UserId === data.user_id">