Init Vue Awesome swiper on product card hover - vue.js

ideally I need vue awesome swiper to init on product card hover, so until the user hovers, catalog loads only one img per product instead of multiple.
HTML
<div
#mouseover="handleMouseOver"
#mouseleave="handleMouseLeave"
/>
<div
v-if="media.length > 1"
ref="swiper"
v-swiper:mySwiper="swiperOption"
>
<div class="swiper-wrapper">
<div
v-for="image in images"
:key="image.id"
class="swiper-slide image__wrapper"
>
<img
:src="image.attributes.src"
:width="imgWidth"
:height="imgHeight"
:alt="imgAlt"
>
</div>
</div>
</div>
</div>
Component
data: () => ({
swiperOption: {
loop: true,
slidesPerView: 1,
centeredSlides: true,
spaceBetween: 30,
},
}),
methods: {
slideStart() {
if (this.$refs.swiper) {
this.mySwiper.activeIndex = 2;
this.mySwiper.autoplay.start();
}
},
slideStop() {
if (this.$refs.swiper) {
this.mySwiper.autoplay.stop();
this.mySwiper.activeIndex = 1;
}
},
handleMouseOver() {
this.isHovered = true;
this.slideStart();
},
handleMouseLeave() {
this.isHovered = false;
this.slideStop();
},
},
What I've tried and what problems encountered:
At first, I've added isHovered condition to v-if and used element in v-else, however after hover swiper refuses to autoplay (but reacts on activeIndex change)
After that I've tried adding init:false to swiperOption and this.$mySwiper.init() on hover, but it crashes whenever I'm trying to leave the page:
Would appreciate any ideas.

Solved by creating a parent div, and moving 'v-if' logic with hover condition there.

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.

Vue.js transition and GSAP3 stagger usage

I'm using Vue.js and gsap for listing card elements but it is not animating one by one.
Here it is Vue.js codes:
<transition-group appear #before-enter="beforeEnter" #enter="enter" :css="false">
<div class="card" v-for="card in cards" :key="card.id">
<span>{{ card.name }}</span>
</div>
</transition-group>
vue js and gsap parts:
beforeEnter(el) {
console.log(el);
gsap.from(el, {
duration: 1
opacity:0,
ease: "power2.inOut",
stagger: {
grid: "auto",
from: "start",
amount: 1.5
}
})
},
enter(el, done) {
gsap.to(el, {
duration: 1,
opacity: 1
})
}
Animation is working but all listed at the same time. I want to be listed one by one

How you do you call a method once rendering is done in Vue?

I have am building a Vue app that includes a QuillJS editor in a tab. I have a simple setTab(tabName) Vue method that shows/hides tabs with the v-if directive.
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
}
}
}
My tab is basically like this:
<div id="composer" v-if="tabName === 'compose'">
<!-- toolbar container -->
<div id="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div id="editor">
<p>Hello World!</p>
</div>
</div>
Currently, I'm getting an error because the #editor element does not yet exist when I am calling new Quill(...). How do I delay that QuillJS initialization on the page so that it doesn't happen until after the #editor is already there?
Use mounted hook.
mounted: function () {
// Code that will run only after the
// entire view has been rendered
}
Use this.$nextTick() to defer a callback to be executed after the next DOM update cycle (e.g., after changing a data property that causes a render-update).
For example, you could do this:
methods: {
setTab: function (tabName) {
this.view = tabName;
if(tabName === 'compose') {
this.$nextTick(() => {
var editor = new Quill('#editor', {
modules: { toolbar: '#toolbar' },
theme: 'snow'
});
})
}
}
}
A clean way to do this is not to rely on selectors but make Quill editor a self-contained component:
<template>
<div class="quill-editor">
<!-- toolbar container -->
<div ref="toolbar">
<button class="ql-bold">Bold</button>
<button class="ql-italic">Italic</button>
</div>
<!-- editor container -->
<div ref="editor">
<p>Hello World!</p>
</div>
</div>
</template>
<script>
...
name: "QuillEditor",
mounted() {
this.quill = new Quill(this.$refs.editor, {
modules: { toolbar: this.$refs.toolbar },
theme: 'snow'
});
}
...
</script>

Conditionally rendering icons only one time depending on property

I am trying to write a template that displays either Icon AAA or Icon BBB, depending on whether or not the item in the current iteration has a specific flag. Here is my code:
<div v-for="(item, itemIndex) in items">
<div v-if="item.hasUnreadComments">
<span>Display Icon AAA</span>
</div>
<div v-else>
<span>Display Icon BBB</span>
</div>
</div>
The issue here is that I need either icon displayed ONCE. If more than one item has it set to item.hasUnreadComments === true, Icon AAA will be displayed equally as many times, which is not what I want. Couldnt find anything in the docs and I dont want to bind it to a v-model.
Can this be done in Vue without a third data variable used as a flag?
You will have to do some sort of intermediate transformation. v-if is just a flexible, low-level directive that will hide or show an element based on a condition. It won't be able to deal directly with how you expect the data to come out.
If I'm understanding what you're asking for, you want an icon to only be visible once ever in a list. You can prefilter and use an extra "else" condition. This sounds like a use case for computed properties. You define a function that can provide a transformed version of data when you need it.
This example could be improved upon by finding a way to boil down the nested if/elses but I think this covers your use case right now:
const app = new Vue({
el: "#app",
data() {
return {
items: [{
hasUnreadComments: true
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
{
hasUnreadComments: true
},
{
hasUnreadComments: false
},
]
}
},
computed: {
filteredItems() {
let firstIconSeen = false;
let secondIconSeen = false;
return this.items.map(item => {
if (item.hasUnreadComments) {
if (!firstIconSeen) {
item.firstA = true;
firstIconSeen = true;
}
} else {
if (!secondIconSeen) {
item.firstB = true;
secondIconSeen = true;
}
secondIconSeen = true;
}
return item;
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, itemIndex) in filteredItems">
<div v-if="item.firstA">
<span>Display Icon AAA</span>
</div>
<div v-else-if="item.firstB">
<span>Display Icon BBB</span>
</div>
<div v-else>
<span>Display no icon</span>
</div>
</div>
</div>

Move elements passed into a component using a slot

I'm just starting out with VueJS and I was trying to port over a simple jQuery read more plugin I had.
I've got everything working except I don't know how to get access to the contents of the slot. What I would like to do is move some elements passed into the slot to right above the div.readmore__wrapper.
Can this be done simply in the template, or am I going to have to do it some other way?
Here's my component so far...
<template>
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>
<script>
export default {
name: 'read-more',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
}
</script>
You can certainly do what you describe. Manipulating the DOM in a component is typically done in the mounted hook. If you expect the content of the slot to be updated at some point, you might need to do the same thing in the updated hook, although in playing with it, simply having some interpolated content change didn't require it.
new Vue({
el: '#app',
components: {
readMore: {
template: '#read-more-template',
data() {
return {
open: false,
moreLabel: 'more',
lessLabel: 'less'
};
},
methods: {
toggle() {
this.open = !this.open;
}
},
mounted() {
const readmoreEl = this.$el.querySelector('.readmore__wrapper');
const firstEl = readmoreEl.querySelector('*');
this.$el.insertBefore(firstEl, readmoreEl);
}
}
}
});
.readmore__wrapper {
display: none;
}
.readmore__wrapper.active {
display: block;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id='app'>
Hi there.
<read-more>
<div>First div inside</div>
<div>Another div of content</div>
</read-more>
</div>
<template id="read-more-template">
<div class="readmore">
<!-- SOME ELEMENTS PASSED TO SLOT TO GO HERE! -->
<div class="readmore__wrapper" :class="{ 'active': open }">
<slot></slot>
</div>
Read {{ open ? lessLabel : moreLabel }}
</div>
</template>