Vue-Router and Gsap ScrollTrigger - vue.js

I've some problems with vue-router and gsap's scrolltrigger plugin.
I have some vue components using scroltrigger and when I go to a different page and came back to the page having the scrolltrigger effect it doesn't trigger, but work if I manualy refresh the page.
I find this topic with people having the same problem with NuxtJs, but the ScrollTrigger.disable() and ScrollTrigger.kill() solutions not working for me :
https://greensock.com/forums/topic/24886-in-nuxt-when-using-scrolltriggerkill-how-can-it-run-again-when-page-is-viewed/
Here is a component I made with ScrollTrigger :
Template Part
<template>
<section class="marquee">
<div class="marquee__inner" aria-hidden="true" ref="inner">
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
</div>
</section>
</template>
Script part
<script>
import gsap from "gsap"
import ScrollTrigger from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
export default {
name: 'ServicesMarquee',
data() {
return {
currentScroll: 0,
isScrollingDown: true,
test: null,
}
},
methods: {
scrollAnim() {
gsap.to(this.$refs.inner, {
xPercent: -65,
scrollTrigger: {
trigger: ".marquee",
start: "top bottom",
end: "top top",
scrub: 0,
}
})
},
},
mounted() {
gsap.set(this.$refs.inner, {xPercent: -50});
let tween = gsap.to(this.$refs.inner.querySelectorAll('.marquee__part'), {xPercent: -100, repeat: -1, duration: 10, ease: "linear"}).totalProgress(0.5);
let self = this
window.addEventListener("scroll", function(){
if ( window.pageYOffset > self.currentScroll ) {
self.isScrollingDown = true;
} else {
self.isScrollingDown = false;
}
gsap.to(tween, {
timeScale: self.isScrollingDown ? 1 : -1
});
self.currentScroll = window.pageYOffset
});
gsap.to(this.$refs.inner, {xPercent: -65 });
this.scrollAnim()
}
}
</script>

I read all the articles on this and similar problems on Stackoverflow and on GSAP forums, and YES setTimeout working, but this is not the answer.
I was also confused that the problem does not affect a large number of people, but they describe almost the same situations, when vue + router + gsap + scrolltrigger stops working when you go through the pages (routes) and return to previous one with animations (animations don't work in this case).
So guys, first of all look at <transition> above <router-view>. This is the problem when you have components from previous page AND the next page at the same time, and ScrollTrigger calculates the values (offsetTop) in that time

Related

vue: Populate select options dynamically

I have vuejs based application with with a form with a drop-down menu. The options in the menu should eventually be populated from a rest call. Right now I use a very hard coded list:
<template>
<ModalForm v-show="visible" #close="visible = false" title="Add location">
<div class="form-outline mb-4">
<select
class="form-control form-control-lg"
placeholder="Organisation"
v-model="formData.organisation"
>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
</div>
<div class="text-center mt-5">
<button class="btn btn-lg btn-primary">
Add location
</button>
</div>
</ModalForm>
</template>
<script setup>
import { reactive, ref } from "#vue/reactivity";
import { watch } from "#vue/runtime-core";
import ModalForm from "../../../components/ModalForm.vue";
const visible = ref(false);
const formDataDefaults = {
organisation : "",
};
const formData = reactive({ ...formDataDefaults });
watch(visible, async (newValue) => {
if (newValue == false) {
for (var key in formData) formData[key] = formDataDefaults[key];
}
});
function show() {
visible.value = true;
}
defineExpose({ show });
</script>
and that kind of works - but when I try to populate the select options in a slightly more dynamic way things fail in the build stage. From googling around I have tried this:
<option v-for="org in organisations" :value="org.name" :key="org.id">
...
<script setup>
export default {
data() {
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
}
};
</script>
When compiling/running this with npm run serve I get the following error message:
error in ./src/pages/Locations/components/AddLocationModal.vue?vue&type=script&setup=true&lang=js
Syntax Error: TypeError: Cannot read properties of null (reading 'content')
which I do not understand, but I think it is related to the data export in the <script> section. Of course the next step is to actually populate with a rest call - but one step at a time.
Any hints appreciated!
Disclaimer: I am quite new to vue (and anything beyond basic JS), so this is in it's entirety based on CopyPaste and Google.
Update: the error message comes when I only add the
<script setup>
export default {
data() {
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
}
};
</script>
part - without making any changes to the <template> part. So it seems there is "something" wrong with the data export. I guess problem is I struggle to understand the model for flow of data/state in this framework.
Update 2: As pointed out in answer below there was a missing { in the original data export section. Fixing that did not solve the problem.
Update 3/solution: As pointed out by several I had a mix of vue2 and vue3 syntax. This example solved it for me.
<script setup> is only for use in Vue 3 projects using the Composition API. Your current code within <script setup> however is written using the Options API. You can either:
Refactor your code to use the Composition API using script setup,
Remove the keyword setup from <script setup> to stick with your current code which should then work using the Options API. Vue 2 projects must use the Options API.
In the example code you have a syntax error, you're missing a brace:
data() {
˅
return { organisation: [{name: "Org1", id: 1}, {name: "Org2", id: 2}] };
˄
}
Check the official documentation about SELECT or the dynamic example

VueJS template ref causes recursive updates

I'm trying to use the PrimeVue Chart component in my VueJS app, but to do that I need to reference the underlying chart, which requires a template reference. Unfortunately, as soon as I add that, I get "Maximum recursive updates exceeded." Here's a super-simple component that causes it:
<script setup>
import { ref } from "vue"
const pvChart = ref(null)
let basicData = {
datasets: [{
label: "Setpoints",
data: [{ x: -10, y: 0 },
{ x: 0, y: 10 },
{ x: 10, y: 5 },
{ x: 0.5, y: 5.5 }],
showLine: true,
},
]
}
</script>
<template>
<div>
<div class="card">
<pv-chart ref="pvChart" type="scatter" :data="basicData" />
</div>
</div>
</template>
I don't even try to update anything (AFAICT).
I tried to follow the example in this SO question, but it's written in a slightly different style, and I'm not sure I didn't miss something in the translation.
Update:
Changing the name of the ref from pvChart to primeChart fixes all the issues. I just grepped my whole codebase and there is no other pvChart in it anywhere. I don’t know why this is an issue.

Password Strength meter for Vue.js 3

I've started working with Vue.js version 3 and making a simple signup form. I need to implement a password strength meter for my password field but seems there isn't any compatible such component with Vue.js 3 version.
I've found few good components for password strength meter to use with Vue.js but they all seems to have compatibility with Vue.js 2.
I've tried
https://awesomeopensource.com/project/skegel13/vue-password
its working good in DEMO but not compatible with my Vue.js 3.
I'm stuck here. Any help/suggestions ?
Are you looking for a visual component or something that actually computes password strength?
zxcvbn is fairly well-known as a strength calculator - it outputs a score from 0-4 for how strong a password is. You could then roll a simple Vue component that outputs a different value depending on that score.
Below example uses Tailwind CSS classes for styling the visual meter. I wrote this in the browser and haven't tested the Vue but it's fairly simple and you should be able to get the idea.
<!-- PasswordStrengthMeter.vue -->
<template>
<div>
<div class="w-full h-4 flex">
<div :class="style"></div>
<div class="flex-1"></div>
</div>
<div>{{ strength }}</div>
</div>
</template>
<script>
props: {
score: {
required: true,
default: 0,
}
},
computed: {
strength() {
return [
'Very Weak', // 0
'Weak', // 1
'Moderate', // 2
'Strong', // 3
'Very Strong' // 4
][this.score];
},
style() {
return [
'w-1 bg-red-500', // 0
'w-1/4 bg-yellow-500', // 1
'w-1/2 bg-yellow-300', // 2
'w-3/4 bg-green-500', // 3
'w-full bg-blue-500' // 4
][this.score];
},
},
</script>
Here's what it might look like.
This one works nicely with Vue3.
https://github.com/miladd3/vue-simple-password-meter/tree/next
Sample code from the repository:
<template>
<div id="app">
<label>Password</label>
<input type="password" v-model="password" />
<password-meter :password="password" />
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import PasswordMeter from 'vue-simple-password-meter';
export default defineComponent({
components: {
PasswordMeter,
},
setup() {
const password = ref('');
return {
password,
};
},
});
</script>

cant change type(danger,success) and add animated on vue strap

i Want to make a progress bar using vue strap . i install vue strap on this link
this link
now i add a progress bar, this progress bar is showing , this bar is only showing color primary and cant showing animated .
<template>
<div class="progress">
<progressbar now="99" type="danger" striped animated ></progressbar>
</div>
</template>
<script>
import { progressbar } from 'vue-strap'
export default {
components: {
progressbar
},
mounted() {
console.log('Component mounted.')
}
}
</script>
with this code , this type is primary and this animated didnt work .
i change browser from chrome to mozila , but its still didnt work . my browser is newest .
whats wrong about this ? i dont know why animated didnt work
There is a bug in VueStrap library when it comes to progress bar animations. The template for progress bar in VueStrap uses class active to animate, whereas, in Bootstrap 4 we have to use class progress-bar-animated. A work around of this problem is to created your own Progress Bar component which makes use of the Bootstrap 4.
Custom Progress Bar component could be written as:
Vue.component('c-progressbar', {
template: `
<div class="progress">
<div class="progress-bar" :class="progressClasses"
role="progressbar"
:style="progressStyle"></div>
</div>`,
props: {
striped: Boolean,
animated: Boolean,
now: {
type: Number,
required: true
},
contextType: {
type: String,
default: 'primary'
}
},
data: function() {
let context = 'bg-' + this.contextType
return {
progressClasses: {
'progress-bar-striped': this.striped,
'progress-bar-animated': this.animated,
[context]: true
},
progressStyle: {
width: this.now + '%'
}
}
}
})
new Vue({ el: '#app' })
You can use this pen for testing: https://codepen.io/abdullah-shabaz/pen/YzXdYgd

Delay the rendering of a component in vue

I've created a component in vue which wraps a vue-apexchart donut graph. As soon as the page loads and this component is loaded, the vue-apexchart animates and displays a small graph.
Now I would like to instantiate multiple of these components from a dataset side by side. Instead of the components to all load an animate at the same time, I would like a small rendering delay to give it an overall nice effect. Something like this would be nice:
<donut :items="series1"></donut>
<donut :items="series2" delay=1500></donut>
The vue-apexchart doesent support initialization delays, and as far as I can see there isn't any vue-specific official solution to delay the rendering of components.
I've tried to put a setTimeout in any of the component hooks to stall the initialization,
I´ve also tried to inject the all the graph DOM in the template element on a v-html tag in a setTimeout, but apexchart doesent notice this new dom content, and vue doesent notice the html bindings either.
I´ve created this fiddle which loads two instances of a graph:
https://jsfiddle.net/4f2zkq5c/7/
Any creative suggestions?
There are several ways you can do this, and it depends on whether you can actually modify the <animated-component> logic yourself:
1. Use VueJS's built-in <transition-group> to handle list rendering
VueJS comes with a very handy support for transitions that you can use to sequentially show your <animated-component>. You will need to use a custom animation library (like VelocityJS) and simply store the delay in the element's dataset, e.g. v-bind:data-delay="500". VueJS docs has a very good example on how to introduce staggered transitions for <transition-group>, and the example below is largely adapted from it.
You then use the beforeAppear and appear hooks to set the opacity of the individual children of the <transition-group>.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
}
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
},
methods: {
beforeAppear: function(el) {
el.style.opacity = 0;
},
appear: function(el, done) {
var delay = +el.dataset.delay;
setTimeout(function() {
Velocity(
el, {
opacity: 1
}, {
complete: done
}
)
}, delay)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition-group name="fade" v-on:before-appear="beforeAppear" v-on:appear="appear">
<animated-component v-bind:data="dataset.first" v-bind:key="0"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:key="1" v-bind:data-delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:key="2" v-bind:data-delay="1000"> </animated-component>
</transition-group>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div>
<h1>Animated Component</h1>
{{ data }}
</div>
</script>
2. Let <animated-component> handle its own rendering
In this example, you simply pass the a number to the delay property (remember to use v-bind:delay="<number>" so that you pass a number and not a string). Then, in the <animated-component>'s mounted lifecycle hook, you use a timer to toggle the visibility of the component itself.
The technique on how you want to show the initially hidden component is up to you, but here I simply apply an initial opacity of 0 and then transition it after a setTimeout.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
},
delay: {
type: Number,
default: 0
}
},
data: function() {
return {
isVisible: false
};
},
computed: {
styleObject: function() {
return {
opacity: this.isVisible ? 1 : 0
};
}
},
mounted: function() {
var that = this;
window.setTimeout(function() {
that.isVisible = true;
}, that.delay);
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
}
});
.animated-component {
transition: opacity 0.25s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<animated-component v-bind:data="dataset.first"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:delay="1000"> </animated-component>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div class="animated-component" v-bind:style="styleObject">
<h1>Animated Component, delay: {{ delay }}</h1>
{{ data }}
</div>
</script>
If you have the possibility to reformat your data, you can build an array of series objects, add a show: true/false property and iterate it:
//template
<div v-for="serie in series">
<donut :items="serie.data" v-if="serie.show"></donut>
</div>
//script
data: function() {
return {
series: [
{ data: [44, 55, 41, 17, 15], show: false },
{ data: [10, 20, 30], show: false },
]
}
}
Now you can create a setTimeout function which will change the serie.show to true by incrementing the delay based on the serie index.
Then add the function on the mounted hook:
methods: {
delayedShow (serie, idx) {
let delay = 1500 * idx
setTimeout(() => {
serie.show = true
}, delay)
}
},
mounted () {
this.series.forEach((serie, idx) => {
this.delayedShow(serie, idx)
})
}
Live example
Faced the same problem with ApexCharts Pie Charts being redrawn rapidly in sequence due to data being pulled from a pinia store mutating too quickly for the chart to keep up, leading to ugly errors in the console.
I resolved the issue by using a boolean ref in a v-if="showChart" on the component and then using a setTimeout to trigger a delayed drawing of the chart:
import { ref } from "vue";
import useStore from "#/store/myChartStore";
const store = useStore();
const showChart = ref(false);
store.$subscribe((mutation, state) =>{
showChart.value = false;
setTimeout(()=> {
showChart.value = true;
}
, 100);
});
If you're not using a store, you may find another way to watch the initial availability of the chart data and then delay the rendering using that same approach.