v-for delay dom patching when data update - vue.js

I'm noticing that the v-for I'm using to render some images inside a component, when data are updated using the event bus,I will have a little delay in DOM content replacing. Is there a way to solve this little problem?
NB: I can't replicate the data passed because it's a 50 elements list of images urls and are provided by an external api service.
<template>
<div class="row m-0 pl-5 pr-5">
<div class="col-4 col-img hide p-0" v-for="(img, idx) in feed" :key="idx" v-observe-visibility="visibilityChanged">
<img class="img-fluid w-100 h-100 ig-image" :src="img.url">
<div class="card-img-overlay text-center">
<p class="text-white">{{ img.likes }} <i class="fas fa-heart"></i> {{ img.comments }} <i class="fas fa-comment"></i></p>
</div>
<a class="stretched-link" href="#image-modal" data-toggle="modal" v-on:click.prevent="zoomImage(img.url)"></a>
</div>
</div>
</template>
<script>
import { EventBus } from '#/standalone/event-bus'
export default {
name: 'UserMedia',
data() {
return {
feed: null,
imgUrl: null
}
},
mounted() {
EventBus.$on('profile-media', (media) => {
this.$set(this, 'feed', media)
})
},
methods: {
zoomImage(url) {
this.$set(this, 'imgUrl', url)
},
visibilityChanged(isVisible, el) {
if(isVisible){
el.target.classList.remove('hide')
el.target.classList.add('show')
}
}
}
}
</script>
<style scoped>
.col-img {
height: 420px;
}
</style>

Since if you change the data 'feed', it will take time to load the images but inorder to load small size images prior to heavy size for good user experience you can use some very good npm packages:
npm i vue-lazyload (I have used it and recommend this one)
npm i v-lazy-image (I haven't used yet but you can explore this as well)

Related

How to make transition opacity work when element is removed?

Hi I made the following notification component for my vue app where I am looping through errors and success messages from vuex store. I am removing them after 3 seconds from the array. However this means the transition does not gets applied since the element gets removed from the DOM. How can I make that work? Please help me.
<template>
<div
id="toast-container"
class="fixed z-50 top-20 right-3"
>
<div
v-for="(error, index) in errors"
:key="error+index"
:class="`${error ? 'opacity-1 visible' : 'opacity-0 invisible'}
toast toast-error flex items-center transition-opacity`"
>
<img
svg-inline
src="#/assets/icons/alert_triangle.svg"
alt="alert icon"
>
<div class="pl-2">
<div class="toast-title">
Der er sket en fejl!
</div>
<div class="toast-message">
{{ error }}
</div>
</div>
</div>
<div
v-for="(message, index) in successMessages"
:key="message+index"
:class="`${message ? 'opacity-1 visible' : 'opacity-0 invisible'}
toast toast-success flex items-center`"
>
<img
svg-inline
src="#/assets/icons/shield_check.svg"
alt="alert icon"
>
<div class="pl-2">
<div class="toast-title">
Succes
</div>
<div class="toast-message">
{{ message }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Notifications',
computed: {
errors() {
return this.$store.state.global.errors
},
successMessages() {
return this.$store.state.global.successMessages
},
},
watch: {
errors: {
handler() {
setTimeout(() => {
this.removeError(0)
}, 3000)
},
deep: true,
},
successMessages: {
handler() {
setTimeout(() => {
this.removeSuccessMessage(0)
}, 3000)
},
deep: true,
},
},
methods: {
removeError(index) {
this.$store.commit('removeError', index)
},
removeSuccessMessage(index) {
this.$store.commit('removeSuccessMessage', index)
},
},
}
</script>
Have a look at https://vuejs.org/guide/built-ins/transition-group.html which is designed for this exact use case. Basically wrapping the whole v-for block with <TransitionGroup> and defining proper CSS classes is all you need to do, <TransitionGroup> will take care of animating the element and removal from DOM after animation is done, you just need to add/remove items from state.

import dynamically fontawesome icons in Vue

I have a problem rendering icons dynamically. I use v-for to get all the data from the object array. Also, I have a second array where I save the name of the icons I worked with. However, when the first array is looping, the second array (icons) doesn't move.
I tried to create a method that maps the data from the first and second array to create a new array. But nothing happens.
My code:
Component.vue
<template>
<div class="items">
<div class="item" v-for="(param, index) in params" :key="index">
<font-awesome-icon :icon="['fab', 'temp']" :temp="getIcon" :key="index" class="fab fa" />
<h3 class="skills-title">{{ param.name }}.</h3>
<p style="display: none">{{ param.description }}.</p>
</div>
</div>
</template>
<script>
export default {
name: "PxSkillCard",
data() {
return {
params: [],
icons: ["laravel", "wordpress-simple"],
};
},
methods: {
getIcon() {
let temp = this.params.map((aux, index) => {
return [aux, this.icons[index]];
});
},
},
};
</script>
And I separated the fontawesome file in a apart module
fontawesome.js
import Vue from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faLaravel,
faWordpressSimple
} from "#fortawesome/free-brands-svg-icons";
import { faPlus } from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(
faLaravel,
faWordpressSimple
);
Vue.component("font-awesome-icon", FontAwesomeIcon);
The final result is:
What about with my code (or my logic)?
You are already looping through everything in your template, there's no need to loop again in your function.
Something like this should work.
<template>
<div class="items">
<div class="item" v-for="(param, index) in params" :key="index">
<font-awesome-icon :icon="['fab', icons[index]]" :key="index" class="fab fa" />
<h3 class="skills-title">{{ param.name }}.</h3>
<p style="display: none">{{ param.description }}.</p>
</div>
</div>
</template>
<script>
export default {
name: "PxSkillCard",
data() {
return {
params: [],
icons: ["laravel", "wordpress-simple"],
};
},
};
</script>
This assume, both arrays are the same size and the data in params and icons are in the correct order.

Removing specific object from array keeps removing last item

Here is what I have and I will explain it as much as I can:
I have a modal inside my HTML code as shown below:
<div id="favorites-modal-edit" class="modal">
<div class="modal-background"></div>
<div class="modal-card px-4">
<header class="modal-card-head">
<p class="modal-card-title">Favorites</p>
<button class="delete" aria-label="close"></button>
</header>
<section class="modal-card-body">
<div class="container">
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" />
</div>
</div>
</section>
<footer class="modal-card-foot">
<button class="button" #click="addItem">
Add Item
</button>
</footer>
</div>
</div>
The id="favorites-modal-edit" is the Vue.js app, then I have the <favorites-edit-component /> vue.js component.
Here is the JS code that I have:
I have my favorites_list generated which is an array of objects as shown below:
const favorites_list = [
{
id: 1,
name: 'Horse',
url: 'www.example.com',
},
{
id: 2,
name: 'Sheep',
url: 'www.example2.com',
},
{
id: 3,
name: 'Octopus',
url: 'www.example2.com',
},
{
id: 4,
name: 'Deer',
url: 'www.example2.com',
},
{
id: 5,
name: 'Hamster',
url: 'www.example2.com',
},
];
Then, I have my vue.js component, which is the favorites-edit-component that takes in the #click="removeItem(this.index) which is coming back as undefined on the index.
Vue.component('favorites-edit-component', {
template: `
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="removeItem(this.index)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
`,
props: {
favorite: Object
},
methods: {
removeItem: function(index) {
this.$parent.removeItem(index);
},
}
});
Then I have the vue.js app that is the parent as shown below:
new Vue({
el: '#favorites-modal-edit',
// Return the data in a function instead of a single object
data: function() {
return {
favorites_list
};
},
methods: {
addItem: function() {
console.log('Added item');
},
removeItem: function(index) {
console.log(index);
console.log(this.favorites_list);
this.favorites_list.splice(this.favorites_list.indexOf(index), 1);
},
},
});
The problem:
For some reason, each time I go to delete a item from the list, it's deleting the last item in the list and I don't know why it's doing it, check out what is happening:
This is the guide that I am following:
How to remove an item from an array in Vue.js
The item keeps coming back as undefined each time the remoteItem() function is triggered as shown below:
All help is appreciated!
There is an error in your favorites-edit-component template, actually in vue template, when you want to use prop, data, computed, mehods,..., dont't use this
=> there is an error here: #click="removeItem(this.index)"
=> in addition, where is index declared ? data ? prop ?
you're calling this.$parent.removeItem(index); then in removeItem you're doing this.favorites_list.splice(this.favorites_list.indexOf(index), 1); this means that you want to remove the value equal to index in you array no the value positioned at the index
=> this.favorites_list[index] != this.favorites_list[this.favorites_list.indexOf(index)]
In addition, I would suggest you to modify the favorites-edit-component component to use event so it can be more reusable:
favorites-edit-component:
<template>
<div class="column is-half">
<button class="button is-fullwidth is-danger is-outlined mb-0">
<span>{{ favorite.name }}</span>
<span class="icon is-small favorite-delete" #click="$emit('removeItem', favorite.id)">
<i class="fas fa-times"></i>
</span>
</button>
</div>
</template>
and in the parent component:
<template>
...
<div id="favorites-modal-edit-wrapper" class="columns is-multiline buttons">
<favorites-edit-component
v-for="favorite in favorites_list"
:key="favorite.id"
:favorite="favorite"
#removeItem="removeItem($event)"
/>
</div>
...
</template>
<script>
export default {
data: function () {
return {
favorites_list: [],
};
},
methods: {
...
removeItem(id) {
this.favorites_list = this.favorites_list.filter((favorite) => favorite.id !== id);
}
...
},
};
I would restructure your code a bit.
In your favorites-edit-component
change your removeItem method to be
removeItem() {
this.$emit('delete');
},
Then, where you are using your component (in the template of the parent)
Add an event catcher to catch the emitted "delete" event from the child.
<favorites-edit-component v-for="(favorite, index) in favorites_list" :key="favorite.id" :favorite="favorite" #delete="removeItem(index)"/>
The problem you have right now, is that you are trying to refer to "this.index" inside your child component, but the child component does not know what index it is being rendered as, unless you specifically pass it down to the child as a prop.
Also, if you pass the index down as a prop, you must refer to it as "index" and not "this.index" while in the template.

infinite loop when create infinite scroll using vue.js and laravel

good day; kindly need your support to finalize this issue i try to make infinite scrolle using laravel and vue.js and my proplem is get in finite loop to set request to data base and mu applocation hang up this is my code x component
<template>
<div class="container" style="margin-top:50px;">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header"><strong> Laravel Vue JS Infinite Scroll - ItSolutionStuff.com</strong></div>
<div class="card-body">
<div>
<p v-for="item in list">
<a v-bind:href="'https://itsolutionstuff.com/post/'+item.slug" target="_blank">{{item.title}}</a>
</p>
<infinite-loading #distance="1" #infinite="infiniteHandler"></infinite-loading>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
alert()
console.log('Component mounted.')
},
data() {
return {
list: [],
page: 1,
};
},
methods: {
infiniteHandler($state) {
let vm = this;
this.$http.get('/Services?page='+this.page)
.then(response => {
return response.json();
}).then(data => {
$.each(data.data, function(key, value) {
vm.list.push(value);
});
$state.loaded();
});
this.page = this.page + 1;
},
},
}
</script>
this is my route
Route::get('/Services', 'ServicesController#Services');
Problem 1
You are binding to the distance property wrong.
Solution
Instead of <infinite-loading #distance="1" #infinite="infiniteHandler"></infinite-loading>
it should be
<infinite-loading :distance="1" #infinite="infiniteHandler"></infinite-loading>
Problem 2
In the code, this.page is being incremented before $http.get is resolved.
This may result in unintentional side effects.
Solution
As per the example in docs vue-infinite-loading hacker news example you should be incrementing the page after data is loaded.

Show on several elements in the same component in vuejs

Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}
EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>
This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.