Vue2: Animate Items Inside a Slot with Anime.js - javascript

Goal
I'd like to stagger the entry of the .modal-slot children.
Problem
I am unable to animate each child node individually, although animating .modal-slot works.
Context
<template>
<transition
:css="false"
#before-enter="beforeEnter"
#enter="enter"
#leave="leave"
#after-leave="afterLeave">
<div class="modal-content">
<div v-if="title" class="modal-title">{{ title }}</div>
<div class="modal-slot">
<slot></slot>
</div>
</div>
</transition>
</template>
<script>
import _ from 'lodash';
import anime from 'animejs';
export default {
name: 'modal',
data () {
return {
delay: 50,
duration: 400,
easing: 'easeInOutBack',
modalBackground: null,
modalTitle: null,
modalSlotElements: null,
modalClose: null
};
},
methods: {
beforeEnter (element) {
// Correctly logs selected '.modal-slot' children
console.log('modalSlotElements', this.$data.modalSlotElements);
_.forEach(this.$data.modalSlotElements, element => {
element.style.opacity = 0;
element.style.transform = 'translateY(10px)';
});
},
enter (element, done) {
anime({
targets: this.$data.modalSlotElements,
duration: this.$data.duration,
easing: this.$data.easing,
delay: (target, index) => this.$data.delay * (index + 1),
translateY: 0,
opacity: 1,
complete: () => { done(); }
});
}
},
mounted () {
// I can select '.modal-slot' and the animation works as intended. Looking for its children breaks the animation.
this.$data.modalSlotElements = document.querySelectorAll('.modal-slot > .action-card');
}
};
</script>

It is a scope issue. My solution is to apply the animations to those elements in the component in which they are defined, not the component into which they are slotted.

Related

How to create Hooper navigation by range slider?

I need Hooper to change slides depending on what value the frame slider has. That is, if the slider is set to "6", Hooper should switch to slide index "6". I researched the documentation example but my solution doesn't work. Here is the code:
Html:
<div class="range-slider">
<input
class="range-slider__input"
type="range"
min="0"
max="10"
v-model="rangeSliderValue"
/>
<p>{{ rangeSliderValue }}</p>
</div>
<hooper
class="weekly-novelties__slider"
:settings="hooperSettings"
#slide="updateCarousel"
></hooper>
<slide
class="weekly-novelties__slide-container"
v-for="(slide, indx) in slides"
:key="indx"
:index="indx"
>*slide*
</slide>
Script:
<script>
import {
Hooper,
Slide,
Progress as HooperProgress,
Pagination as HooperPagination,
Navigation as HooperNavigation,
} from 'hooper'
import 'hooper/dist/hooper.css'
export default {
name: 'WeeklyNovelties',
components: {
Hooper,
Slide,
},
data() {
return {
rangeSliderValue: 0,
hooperSettings: {
itemsToShow: 3.932,
itemsToSlide: 2,
ref: 'carousel',
navigation: {
type: 'custom',
},
// initialSlide: 2,
// infiniteScroll: true,
},
watch: {
carouselData() {
this.$refs.carousel.slideTo(this.rangeSliderValue)
},
},
methods: {
updateCarousel(payload) {
this.myCarouselData = payload.currentSlide
},
changeSliderValue() {
this.$refs.carousel.slideTo(this.rangeSliderValue)
},
},
}
},
setup() {
const slides = Array.from({ length: 10 }).map(
(el, index) => `Slide ${index + 1}`
)
return {
slides,
}
},
}
</script>
The value of the variable rangeSliderValue is displayed correctly, but when moving the range slider, the carousel does not change slides

vue3 child component won't load with computed data as property

Here's the problem I'm having. I have a Leads page, that is my Leads.vue template. It loads my leads and then passes the leads data to other components via props.
The LeadSources component receives a computed method as it's property.
You can see that on the Leads.vue page, the LeadSources component calls the getSourceData() method for it's property data.
When I check the value of the props for LeadSources.vue in the setup() the value for chartData initially is an empty array. If the page hot-reloads then the LeadSources apexchart will populate with the :series data but otherwise I cannot get it to work.
Essentially it works like this.
Leads.vue passes getSourceData() to the LeadsSources.vue component which on the setup() sets it as the variable series and tries to load the apexchart with it.
It will not work if I refresh my page but if I save something in my IDE, the hot-reload will load the updated apexchart and the data appears. It seems like the prop values don't get set in the setup() function the first time around. How do I architecturally get around this? What's the proper way to set this up? I can't tell if the issue is on the leads.vue side of things or with the way that the LeadSources.vue component is being put together.
Any help would be appreciated, I've spent way too long trying to get this to work properly.
Leads.vue
<template>
<!--begin::Leads-->
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadTracker
:lead-data="leadData"
:key="componentKey"
/>
</div>
</div>
<div class="row gy-5 g-xl-8 mb-8">
<div class="col-xxl-12">
<LeadSources
chart-color="primary"
chart-height="500"
:chart-data="getSourceData"
:chart-threshold="sourceThreshold"
widget-classes="lead-sources"
></LeadSources>
</div>
</div>
<!--end::Leads-->
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent, onMounted } from "vue";
import { setCurrentPageTitle } from "#/core/helpers/breadcrumb";
import LeadSources from "#/components/leads/sources/LeadSources.vue";
import LeadTracker from "#/components/leads/tracker/LeadTracker.vue";
import LeadService from "#/core/services/LeadService";
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default defineComponent({
name: "leads",
components: {
LeadTracker,
LeadSources
},
data() {
return {
leadData: [] as Lead[],
}
},
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
setup() {
onMounted(() => {
setCurrentPageTitle("Lead Activity");
});
const sourceThreshold = 5;
return {
sourceThreshold,
componentKey: 0
};
},
computed: {
getSourceData() {
interface SingleSource {
source: string;
value: number;
}
const sourceData: Array<SingleSource> = [];
// Make array for source names
const sourceTypes = [];
this.leadData.filter(lead => {
if (!sourceTypes.includes(lead.source)) sourceTypes.push(lead.source);
});
// Create objects for each form by name, push to leadSourceData
sourceTypes.filter(type => {
let totalSourceLeads = 1;
this.leadData.filter(form => {
if (form.source == type) totalSourceLeads++;
});
const leadSourceData = {
source: type,
value: totalSourceLeads
};
sourceData.push(leadSourceData);
});
// Sort by source popularity
sourceData.sort(function(a, b) {
return a.value - b.value;
});
return sourceData;
}
}
});
</script>
LeadSources.vue
<template>
<!--begin::Lead Sources Widget-->
<div :class="widgetClasses" class="card card-footprint">
<!--begin::Body-->
<div
class="card-body p-0 d-flex justify-content-between flex-column overflow-hidden"
>
<div class="d-lg-flex flex-stack flex-grow-1 px-9 py-6">
<!--begin::Text-->
<div class="d-flex flex-column text-start col-lg-10">
<span class="card-title">Lead Sources</span>
<span class="card-description">Where your leads are coming from.</span>
<!--begin::Chart-->
<div class="d-flex flex-column">
<apexchart
class="mixed-widget-10-chart lead-sources-donut"
:options="chartOptions"
:series="series"
type="donut"
:height="chartHeight"
:threshold="chartThreshold"
></apexchart>
</div>
<!--end::Chart-->
</div>
<!--begin::Unused Data-->
<div class="d-flex flex-row flex-lg-column lg-col-2 justify-content-between unused-data">
<div class="alt-sources flex-fill">
<div><span class="alt-header">Other Sources:</span></div>
<div v-for="item in otherSources" :key="item.source">
<span>{{ item.source }}</span>
<span>{{ item.value }}%</span>
</div>
</div>
<div class="alt-sources flex-fill">
<div><span class="alt-header">Sources Not Utilized:</span></div>
<div v-for="item in unusedSources" :key="item.source">
<span>{{ item.source }}</span>
</div>
</div>
</div>
<!--end::Unused Data-->
</div>
</div>
</div>
<!--end::Lead Sources Widget-->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "LeadSource",
props: {
widgetClasses: String,
chartColor: String,
chartHeight: String,
chartLabels: Array,
chartData: Array,
chartThreshold: Number
},
setup(props) {
const sum = (data) => {
let total = 0;
data?.map(function(v) {
total += v;
});
return total;
}
const chartData = ref(props.chartData).value;
const threshold = ref(props.chartThreshold).value;
const usedSourcesLabel: string[] = [];
const usedSourcesData: number[] = [];
const otherSources: any = [];
const unusedSources: any = [];
const splitData = (data, max) => {
// set used, other sources < 5%, unused sources
data.filter((item) => {
if (item.value > max) {
usedSourcesLabel.push(item.source);
usedSourcesData.push(item.value);
} else if (item.value < max && item.value != 0 && item.value !== null) {
otherSources.push(item);
} else if (item.value == 0 || item.value === null) {
unusedSources.push(item);
}
});
};
splitData(chartData, threshold);
const chartOptions = {
chart: {
width: 380,
type: "donut"
},
colors: [
"#1C6767",
"#CD2E3B",
"#154D5D",
"#F1D67E",
"#4F9E82",
"#EF8669",
"#393939",
"#30AEB4"
],
plotOptions: {
pie: {
startAngle: -90,
endAngle: 270
}
},
dataLabels: {
enabled: false
},
fill: {
type: "gradient",
gradient: {
type: "horizontal",
shadeIntensity: 0.5,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100],
}
},
legend: {
show: true,
position: "left",
fontSize: "16px",
height: 220,
onItemClick: {
toggleDataSeries: false
},
onItemHover: {
highlightDataSeries: false
},
formatter: function (val, opts) {
return val + " - " + opts.w.globals.series[opts.seriesIndex];
}
},
title: {
text: undefined
},
tooltip: {
style: {
fontSize: "14px"
},
marker: {
show: false
},
y: {
formatter: function(val) {
return val + "%";
},
title: {
formatter: (seriesName) => seriesName,
},
}
},
labels: usedSourcesLabel,
annotations: {
position: "front",
yaxis: [{
label: {
text: "text annotation"
}
}],
xaxis: [{
label: {
text: "text xaxis annotation"
}
}],
},
responsive: [{
breakpoint: 480,
options: {
legend: {
position: "bottom",
horizontalAlign: "left"
}
}
}]
};
const series = usedSourcesData;
return {
chartOptions,
series,
otherSources,
unusedSources
};
}
});
</script>
Edited
I will attach the LeadService.ts class as well as the ApiService.ts class so you can see where the data is coming from
LeadService.ts
import ApiService from "#/core/services/ApiService";
import {Lead} from "#/core/interfaces/lead";
export default class LeadService {
getLeads() {
const accountInfo = JSON.parse(localStorage.getItem('accountInfo') || '{}');
ApiService.setHeader();
return ApiService.query("/leads", {params: {client_id : accountInfo.client_id}})
.then(({ data }) => {
let leadData: Lead[] = data['Items'];
return leadData;
})
.catch(({ response }) => {
return response;
});
}
}
ApiService.ts
import { App } from "vue";
import axios from "axios";
import VueAxios from "vue-axios";
import JwtService from "#/core/services/JwtService";
import { AxiosResponse, AxiosRequestConfig } from "axios";
import auth from "#/core/helpers/auth";
/**
* #description service to call HTTP request via Axios
*/
class ApiService {
/**
* #description property to share vue instance
*/
public static vueInstance: App;
/**
* #description initialize vue axios
*/
public static init(app: App<Element>) {
ApiService.vueInstance = app;
ApiService.vueInstance.use(VueAxios, axios);
ApiService.vueInstance.axios.defaults.baseURL = "https://api.domain.com/";
}
/**
* #description set the default HTTP request headers
*/
public static setHeader(): void {
ApiService.vueInstance.axios.defaults.headers.common[
"Authorization"
] = `Bearer ${auth.getSignInUserSession().getIdToken().jwtToken}`;
ApiService.vueInstance.axios.defaults.headers.common[
"Content-Type"
] = "application/json application/vnd.api+json";
}
/**
* #description send the GET HTTP request
* #param resource: string
* #param params: AxiosRequestConfig
* #returns Promise<AxiosResponse>
*/
public static query(
resource: string,
params: AxiosRequestConfig
): Promise<AxiosResponse> {
return ApiService.vueInstance.axios.get(resource, params).catch(error => {
// #TODO log out and send home if response is 401 bad auth
throw new Error(`[KT] ApiService ${error}`);
});
}
}
export default ApiService;
I think the issue is caused when you're calling the data from the api.
This code:
beforeCreate: async function() {
this.leadData = await new LeadService().getLeads()
},
I'd refactor to
async created () {
const service = new LeadService()
const value = await service.getLeads()
}
Also it would be nice to be able to see how you're fetching your data.
Sometimes this code: const value = await axios.get('/api/getStuff').data can be problematic because of paratheses issues. Which causes the same issue you described of, hot reloading working, but fresh not. I suspect the same sort of issue relies of the code executing like => (await new LeadService()).getLeads() Where you're probably awaiting the class, rather than the actual async code.

Use a variable defined in a method inside the template

it's the first time I use Vue (v2 not v3) and I'm stucked trying to use a variable (defined inside a methods) inside the template.
My semplified code:
<template>
<div class="container" #mouseover="isHovered = true" #mouseleave="isHovered = false">
<div class="c-container">
<div ref="topCContainerRef" class="top-c-container">
<div
:class="['top-c', ...]"
:style="{ height: `${isHovered ? 0 : this.scaledHeight}` }" // <-- HERE I need `scaledHeight`
>
</div>
</div>
</div>
</div>
</template>
<script>
import { scaleLinear } from 'd3-scale'
export default {
name: 'MyComponent',
components: { },
props: {
...,
datum: {
type: Number,
required: true,
},
...
},
data: function () {
return {
isHovered: false,
scaledHeight: {},
}
},
mounted() {
this.matchHeight()
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
const scaledHeight = heightScale(this.datum)
this.scaledHeight = scaledHeight // I want to use this value inside the template
},
},
}
</script>
How can I get the value of scaledHeight inside the template section?
If I didn't use this, I get no error but the height value is always 0, like scaledHeight is ignored..
I read the documentation but it doesn't help me
I encountered and solved this problem today.
You can change your styles like below.
<div
:class="['top-c', ...]"
:style="{ height: isHovered ? 0 : scaledHeight }"
>
It works fine for me, and hope it will help you~~
Fixed using computed
computed: {
computedHeight: function () {
return this.isHovered ? 0 : this.matchHeight()
},
},
methods: {
matchHeight() {
const topCContainerHeight = this.$refs.topCContainerRef.clientHeight
const heightScale = scaleLinear([0, 100], [20, topCContainerHeight])
return heightScale(this.datum)
},
},

How do you animate menu with framer motion on-click?

(Using React obviously + Gatsby)
I have a hamburger button that gonna open a nav-menu in my website.
I wanted to know how to make the menu open with an animation using Framer Motion.
you could use this method that is given in the examples section of the framer motion documentation.
Framer Motion API Documentation
import { motion } from "framer-motion"
const variants = {
open: { opacity: 1, x: 0 },
closed: { opacity: 0, x: "-100%" },
}
export const MyComponent = () => {
const [isOpen, setIsOpen] = useState(false)
return (
<motion.nav
animate={isOpen ? "open" : "closed"}
variants={variants}
>
'Menu Content'
</motion.nav>
)
}
<MonkeyPic rotate={showFistBump} />
// ...
// Then switch animation variant depending on rotate prop
const variants = {
rotate: { rotate: [0, -30, 0], transition: { duration: 0.5 } },
// You can do whatever you want here, if you just want it to stop completely use `rotate: 0`
stop: { y: [0, -10, 0], transition: { repeat: Infinity, repeatDelay: 3 } }
};
const MonkeyPic = ({ rotate }) => {
return (
<div>
<motion.img
variants={variants}
animate={rotate ? 'rotate' : 'stop'}
id="monkeyFace"
src="/images/Monkey.png"
/>
</div>
);
};

How can I transition between two nuxt pages, while first waiting on a child component transition/animation to finish?

I have a question regarding transitions. When transitioning from one page to the other, is it possible to wait for a child transition/animation (extra file, extra component) to finish and then transition to the next page?
Example:
1) Home (Page Component)
a) Logo (Vue Component)
2) About (Page Component)
When I click on the About one the homepage, I first would like to animate the Logo component, then fade out the whole homepage and then route to the About page.
Here the relevant code:
Index.vue:
<template>
<div class="home" style="opacity: 0">
<Logo v-show="showChild"/>
<nuxt-link to="/about">About</nuxt-link>
<p>Homepage</p>
</div>
</template>
<script>
import Logo from "~/components/Logo.vue";
import { TweenMax, CSSPlugin } from "gsap";
export default {
components: {
Logo
},
data() {
return {
showChild: true
};
},
transition: {
enter(el, done) {
console.log("Enter Parent Home");
this.showChild = true;
TweenLite.to(el, 1, {
opacity: 1,
onComplete: done
});
},
leave(el, done) {
this.showChild = false;
TweenLite.to(el, 1, {
opacity: 0,
onComplete: done
});
console.log("Leave Parent Home");
console.log("Child Visible: " + this.showChild);
},
appear: true,
css: false
}
};
</script>
Logo.vue
<template>
<transition #enter="enter" #leave="leave" mode="out-in" :css="false">
<div style="display: block; width: 200px; height: 200px;">
<img
style="objec-fit: cover; width: 100%; height: 100%"
src="https://images.unsplash.com/photo-1508138221679-760a23a2285b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1334&q=80"
>
</div>
</transition>
</template>
<script>
export default {
props: {
showChild: {
type: Boolean,
default: true
}
},
methods: {
enter(el, done) {
console.log("Enter Child Home");
TweenLite.fromTo(el, 1, { x: -100 }, { x: 0, onComplete: done });
},
leave(el, done) {
console.log("Leave Child Home");
TweenLite.to(el, 1, {
x: -100,
onComplete: done
});
}
}
};
</script>
About.vue
<template>
<div class="about" style="opacity: 0">
<nuxt-link to="/">Home</nuxt-link>
<p>About</p>
</div>
</template>
<script>
export default {
transition: {
enter(el, done) {
console.log("Enter Parent About");
TweenLite.to(el, 1, {
opacity: 1,
onComplete: done
});
},
leave(el, done) {
console.log("Leave Parent About");
TweenLite.to(el, 1, {
opacity: 0,
onComplete: done
});
},
appear: true,
css: false
}
};
</script>
I have also created a sandbox.
https://codesandbox.io/s/codesandbox-nuxt-psks0
Unfortunately I am stuck with two problems:
1) The leave transition of the child component (Logo) isn't starting right now.
2) I would like to first finish the Child Component (Logo) transition and then finish the home page transition and then route to the about page. Is that even possible?
Thank you very much for your help.
Best regards
Chris
First thing i would wrap your custom transition in your home.vue file something like this:
<transition #enter="enter" #leave="leave" mode="out-in" :css="false">
<client-only>
<Logo v-if="showChild"/>
</client-only>
</transition>
export default {
data() {
return {
showChild: true
};
},
methods: {
enter(el, done) {
console.log("Enter Child Home");
TweenLite.fromTo(el, 1, { x: -100 }, { x: 0, onComplete: done });
},
leave(el, done) {
console.log("Leave Child Home");
TweenLite.to(el, 1, {
x: -100,
onComplete: this.$router.push("/about")
});
}
}
}
And the idea here is that you use instead of an nuxt-link an common a tag or button that just set your showChild to false, that will trigger your leave method.
<button #click="showChild = false" />
And at the end of your leave method you set this.$router.push("/about") when your transition is finished. In theorie your logo should transition first, and when its finished your page transition should start at the end then.

Categories