Vue.js, transition and slots to work together - vue.js

I've been trying to develop a carousel with Vue.js but I'm stuck with the transition effects. Following are my codes,
// Main component
<slider>
<slide v-for="(slide, index) in compOpts.blockImg" :key="index">
<img :src="slide" :alt="features ${index}`">
</slide>
</slider>
// Slider component
<transition-group class="slider__container" name="slideTr" tag="div" appear mode="out-in">
<slot></slot>
</transition-group>
import slide from './contentSliderSlide';
export default {
name: 'slider',
data: () => ({
slideCount: 0,
activeSlide: 0,
currentSlide: null,
lastSlide: null,
slideInterval: null
}),
mounted() {
this.init();
},
methods: {
getSlideCount() {
this.slideCount = (
this.$slots
&& this.$slots.default
&& this.$slots.default.filter(
slot => slot.tag
&& slot.tag.indexOf('slide') > -1
).length
) || 0;
},
init() {
this.getSlideCount();
this.$slots.default[this.activeSlide].elm.classList.add('visible');
this.playSlide();
},
gotoSlide(n, p) {
this.currentSlide = (n + this.slideCount) % this.slideCount;
if (p) {
this.lastSlide = ((n - 1) + this.slideCount) % this.slideCount;
this.$slots.default[this.lastSlide].elm.classList.remove('visible');
this.activeSlide += 1;
} else {
this.lastSlide = ((n + 1) + this.slideCount) % this.slideCount;
this.$slots.default[this.lastSlide].elm.classList.remove('visible');
this.activeSlide -= 1;
}
this.$slots.default[this.currentSlide].elm.classList.add('visible');
},
playSlide() {
this.slideInterval = setInterval(this.nextSlide, 2000);
},
nextSlide() {
this.gotoSlide(this.activeSlide + 1, true);
}
},
components: {
slide
}
};
// Slide component
<div class="slider__slide">
<slot></slot>
</div>
If there isn't any solution then at least please advise me how to implement css transition effect with before and after hook (using pure css method)

Related

how to make an good working select input component in vue 3

Im trying to make an pagination with an select field but i dont get it how the v-model and the value properties should be given, for now i have like this ↓...
This is the select/pagination part in that component..
<p v-on:click="back" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-l cursor-pointer">Zurück</p>
<p class="bg-gray-300 text-gray-800 font-bold py-2 px-4 "><span>{{siteCount}}/{{siteTotal}}</span></p>
<select :value="modelValue" v-on:change="selectPage">
<option v-for="select in select" :value="select">{{select}}</option>
</select>
<p v-on:click="next" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded-r cursor-pointer">Weiter</p>
</div>
</template>
<script>
import SearchIcon from "../Assets/SearchIcon";
import NavBar from "./NavBar";
export default {
name: "PaginateSeason",
props: {
data: {
name: String,
description: String,
thumbnail: String,
},
siteCount: Number,
siteTotal: Number,
select: Array,
modelValue: String,
},
components: {
NavBar,
SearchIcon
},
emits: ['next', 'back', 'selectPage'],
methods:{
next() {
this.$emit('next')
},
back() {
this.$emit('back')
},
selectPage() {
this.$emit('selectPage')
}
},
}
</script>
This is the main Component where i fetch anything i need...
<template>
<router-view>
<NavBar :is-disabled="true"></NavBar>
<div v-if="isLoading" class="flex flex-col items-center justify-center mt-9">
<LoaderSpinner></LoaderSpinner>
<p>Daten werden geladen...</p>
</div>
<div v-else>
<PaginateSeason
v-model="count"
:data="seasons"
:site-count="current"
:site-total="siteCountTotal"
:select="select"
#next="paginateNext"
#back="paginateBack"
#selectPage="allCount"
/>
</div>
</router-view>
</template>
<script>
import NavBar from './NavBar'
import PaginateSeason from './PaginateSeason'
import LoaderSpinner from "../Assets/LoaderSpinner";
export default {
name: "AllSeasons",
components: {
PaginateSeason,
NavBar,
LoaderSpinner
},
data(){
return {
seasons: null,
current: 1,
pageSize: 4,
isLoading: true,
siteCountTotal: null,
select: [],
count: '',
}
},
mounted(){
this.list(this.current)
const list = async () => {
await axios.get(`/api/seasons/all`).then(response => {
let meta = response.data.meta
this.siteCountTotal = meta.last_page
for (let i = 1; i <= this.siteCountTotal; i++) {
this.select.push(i)
}
}).catch(error => {
console.log(error)
})
}
list()
},
methods: {
async list(page) {
await axios.get(`/api/seasons/all?page=${page}`).then(response => {
this.seasons = response.data.data
}).catch(error => {
console.log(error)
}).finally(() => {
setTimeout(() => {
this.isLoading = false
},1000)
})
},
paginateNext () {
if (this.seasons.length < this.pageSize) {
this.current = 1
this.list(this.current)
} else {
this.current += 1
this.list(this.current)
}
},
paginateBack () {
if (this.current === 1) {
this.current -= 0
} else {
this.current -= 1
this.list(this.current)
}
},
allCount() {
console.log(this.count)
}
},
IF you can help me with that i would be very gratefull!
The count variable on the AllSeasons component will not be updated, because you pass it as v-model to the PaginateSeason component, but PaginateSeason never emits update:modelValue.
Implement selectPage like this to address this issue:
selectPage(event) {
this.$emit('update:modelValue', event.target.value)
}
You can also keep the selectPage event, or add a wach for the count variable in AllSeasons component to watch for page changes. You may also want to emit the update:modelValue event from next and back functions in the PaginateSeason component.

How to trigger a component transition when the components data has loaded in vue js?

I have a fade-in transition that is working with all my components, the problem is a few of my components that are making api calls - transition in before the data is fully loaded.
So if I have a table with each row being populated with data from the api call, the table headers will transition initially and then a few seconds later - many rows with data will suddenly appear. What I want is for the table/data to fade-in. How can I trigger or delay the transition until the job_execs array gets populated with data from the API call?
views/releases.vue
<script>
import NavBar from "../components/NavBar.vue";
import Releases from "../components/releases/Releases.vue";
import Footer from "../components/Footer.vue";
export default {
name: "releases",
data() {
return {
loading: true
};
},
components: {
NavBar,
Releases,
Footer
},
};
</script>
<template>
<div id="vue-main">
<NavBar></NavBar>
<h1><b>Releases</b></h1>
<transition name="fade" appear mode="out-in">
<Releases></Releases>
</transition>
<Footer></Footer>
</div>
</template>
components/releases/Releases.vue
<template>
<div class="releases">
<table>
<template>
<tr>
<td><b>Version</b></td>
<td><b>Platform</b></td>
<td><b>Status</b></td>
</tr>
<tr v-for="(item, index) in orderedReleases">
<td :style="tdStyle">{{ item.version }}</td>
<td :style="tdStyle">{{ item.platform }}</td>
<td :style="tdStyle">{{ item.status }}</td>
</tr>
</template>
</table>
</div>
</template>
<script>
import moment from "moment";
import sortBy from "lodash/sortBy";
export default {
name: "Releases",
props: ["loading"],
data() {
return {
job_execs: []
};
},
computed: {
orderedReleases: function() {
let newlist = this.job_execs.sort(this.naturalCompare).reverse()
for ( var i = 0; i < newlist.length; i++) {
if (typeof newlist[i].version === "string") {
if (newlist[i].version.startsWith("iPad")) {
console.log(newlist[i].version)
newlist.splice(i,1);
i--;
}
}
}
return newlist;
},
},
methods: {
calculateDuration: function(time_start, time_end) {
this.theDuration = moment.duration(time_end.diff(time_start));
if (this.theDuration.seconds() == 0) {
this.cleanDuration = "N/A";
} else {
this.cleanDuration =
this.theDuration.hours() +
" hrs " +
this.theDuration.minutes() +
" min " +
this.theDuration.seconds() +
" sec";
}
return this.cleanDuration;
},
naturalCompare: function(a, b) {
var ax = [], bx = [];
a.version.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { ax.push([$1 || Infinity, $2 || ""]) });
b.version.replace(/(\d+)|(\D+)/g, function(_, $1, $2) { bx.push([$1 || Infinity, $2 || ""]) });
while(ax.length && bx.length) {
var an = ax.shift();
var bn = bx.shift();
var nn = (an[0] - bn[0]) || an[1].localeCompare(bn[1]);
if(nn) return nn;
}
return ax.length - bx.length;
}
},
created() {
this.jobExecEndpoint = process.env.VUE_APP_UATU_URL + "/api/v1/release/";
fetch(this.jobExecEndpoint)
.then(response => response.json())
.then(body => {
for (let i = 0; i < body.length; i++) {
this.cleanStartTime = moment(body[i].start_date);
this.job_execs.push({
version: body[i].version,
status: body[i].status.name,
start: this.cleanStartTime.format("LLL")
});
}
})
.catch(err => {
console.log("Error Fetching:", this.jobExecEndpoint, err);
return { failure: this.jobExecEndpoint, reason: err };
});
}
};
</script>
<style>
</style>
Add a v-if inside your Releases component on the div tag like so:
<div class="releases" v-if="job_execs"></div>
and change your data object like this:
data() {
return {
job_execs: null
};
},
The pattern I use:
loading: smth = null
loaded: smth = [...]
empty state: smth.length === 0
This way you don't need a separate loading property.

Passing Array as prop not received on the other component

I am trying to pass an array of objects as a prop to a component. The Array is being passed without an array. I am neither receiving any compilation error.
I tried actually looking on to the object tried some stuff. But it did not work
Here is the code:
CardRenderer.vue:
<template lang="html">
<div>
<b-container class="bv-example-row">
<b-row v-for="(row, i) of rows" v-bind:key="i">
<b-col v-for="(item, j) of row" v-bind:key="j" >
<!-- you card -->
<b-card
:title="item.title"
img-src="item.icon"
img-alt="Image"
img-top
tag="article"
style="max-width: 20rem;"
class="mb-2"
>
<b-card-text>
<h1>{{item.name}}</h1>
<pre>{{item.description}}</pre>
</b-card-text>
<b-button :href="'/dashboard/'+item.name" variant="primary">More</b-button>
</b-card>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script lang="js">
export default {
name: 'CardRenderer',
props: {
renderData: {
type: Array,
required: true,
default: () => ([]),
}
},
data() {
return {
rows: null
}
},
mounted() {
const itemsPerRow = 3
let rowss = []
// eslint-disable-next-line
console.log(this.renderData)
let arr = this.renderData
for (let i = 0; i < arr.length; i += itemsPerRow) {
let row = []
for (let z = 0; z < itemsPerRow; z++) {
row.push(arr[z])
}
rowss.push(row)
}
this.rows = rowss
// eslint-disable-next-line
// console.log(this.rows)
},
methods: {
},
computed: {
// rows() {
// }
}
}
</script>
<style scoped>
</style>
Something.vue
<template lang="html">
<!-- <h1>Something</h1> -->
<CardRenderer :renderData=valObj />
</template>
<script lang="js">
import CardRenderer from './CardRenderer'
export default {
name: 'something',
components: {
CardRenderer
},
props: [],
data() {
return {
valObj: []
}
},
mounted() {
let key = this.findUrl()
let value = this.$store.getters.responseAPI.apps.filter((elem) => {
if(elem.name == key) return elem.apps
})
if (value && value.length > 0)
this.valObj = value[0].apps
//eslint-disable-next-line
console.log(this.valObj)
},
methods: {
findUrl() {
let url = window.location.pathname.split("/").slice(-1)[0];
return url
}
},
computed: {
}
}
</script>
<style scoped >
.something {
}
</style>
This is what i am sending as a prop.
This is what i receive on the component
There's a couple of issues here.
First, you should be using kebab-cased attribute names and quotes around the value...
<CardRenderer :render-data="valObj" />
The second issue is timing related. In your component, you calculate rows based on the initial renderData in the mounted hook but this will not update when the parent component alters valObj.
What you should do instead is use a computed property which will react to valObj / renderData changes.
For example
data () { return {} }, // removed rows from data
computed: {
rows () {
let itemsPerRow = 3
let rows = []
for (let i = 0; i < this.renderData.length; i += itemsPerRow) {
rows.push(this.renderData.slice(i, i + itemsPerRow))
}
return rows
}
}

Axios response happening after component is rendered

I have a parent component making an Ajax request using Axios, The response is then assigned to a variabled called 'carousel' and is then passed down to the child component.
In the child component on 'created()' I am assigning the passed prop 'carousel' to a new variable called 'slides'
Problem is when I do this is returns undefined and my thinking is the Axios query hasn't returned before this happens.
Is there a way to delay the axios request before the prop is passed and the child component always gets the expected response.
My code is below.
Parent
<template>
<div class='product-container'>
<home-carousel :carousel="carousel"></home-carousel>
<profiler></profiler>
<cta-sections :panels="panels"></cta-sections>
</div>
</template>
<script>
import api from '../api/Home'
import CtaSections from '../components/CtaSections'
import HomeCarousel from '../components/HomeCarousel'
import Profiler from '../components/Profiler'
export default {
components: {
CtaSections,
HomeCarousel,
Profiler,
},
data() {
return {
panels: [],
slides: 'test',
carouselPass: [],
carousel: [],
}
},
created() {
axios.get(window.SETTINGS.API_BASE_PATH + 'pages/5')
.then(response => {
this.panels = response.data.acf.split_panels;
this.carousel = response.data.acf.carousel;
this.carousel.forEach(function (item, index) {
if (index === 0) {
item.active = true;
item.opacity = 1;
} else {
item.active = false;
item.opacity = 0;
}
item.id = index
})
})
},
}
</script>
Child
<template>
<div class='slider'>
<transition-group class='carouse carousel--fullHeight carousel--gradient' tag="div" name="fade">
<div v-for="slide in slides"
class="carousel__slide"
v-bind:class="{ active: slide.active }"
:key="slide.id"
:style="{ 'background-image': 'url(' + slide.image.url + ')' }"
v-show="slide.active"
>
<div class="carousel__caption carousel__caption--centered">
<h2 class="heading heading--white heading--uppercase heading--fixed">{{ slide.tagline }}</h2>
</div>
</div>
</transition-group>
<div class='carousel__controls carousel__controls--numbered carousel__controls--white carousel__controls--bottomRight carousel__controls--flex'>
<div #click="next" class="in">
<img src="/static/img/svg/next-arrow.svg" />
<span v-if="carousel.length < 10">0</span>
<span>{{ slideCount }}</span>
<span>/</span>
<span v-if="carousel.length < 10">0</span>
<span>{{ carousel.length }}</span>
</div>
</div>
</div>
</template>
<script>
import bus from '../bus'
import Booking from './Booking'
export default {
name: 'HomeCarousel',
props: ['carousel'],
data() {
return {
slideCount: 1,
slides: [],
/*
slides: [{
image: this.themepath + 'home-banner.jpg',
active: true,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 1,
id: 1
},
{
image: this.themepath + 'home-banner2.jpg',
active: false,
captionText: 'A PLACE AS UNIQUE AS YOU ARE',
buttonText: 'book now',
buttonUrl: '#',
opacity: 0,
id: 2
}
]
*/
}
},
methods: {
showBooking: function() {
this.$store.state.showBooking = true;
},
next() {
const first = this.slides.shift();
this.slides = this.slides.concat(first)
first.active = false;
this.slides[0].active = true;
if (this.slideCount === this.slides.length) {
this.slideCount = 1;
} else {
this.slideCount++;
}
},
previous() {
const last = this.slides.pop()
this.slides = [last].concat(this.slides)
// Loop through Array and set all active values to false;
var slideLength = this.slides.length;
for (var slide = 0; slide < slideLength; slide++) {
this.slides[slide].active = false;
}
// Apply active class to first slide
this.slides[0].active = true;
this.slideCount--;
},
loopInterval() {
let self = this;
setInterval(function () {
self.next()
}, 8000);
}
},
created() {
this.slides = this.carousel;
}
}
</script>
You can just watch the prop and set this.slides when it changes, i.e. when the async call has finished:
watch:{
carousel(value) {
this.slides = value
}
}
Here's a JSFiddle: https://jsfiddle.net/nwLh0d4w/

Working With Data in Both A Computed Property and a Life Cycle Hook

I am fairly new to VueJS and I just want to make sure that the following code is written properly (and that there is not a better way to do this).
Here is the code:
<template>
<section class="hero" :style="bgSrc">
<slot></slot>
</section>
</template>
<script>
export default {
props: [
'src',
'srcXs',
'srcSm',
'srcMd',
'srcLg',
'srcXl'
],
data () {
return {
xs: 0,
sm: 600,
md: 1024,
lg: 1440 - 16,
xl: 1920 - 16,
sourceImage: '',
screenWidth: ''
}
},
computed: {
bgSrc () {
if (this.src) {
return { backgroundImage: 'url(' + this.sourceImage + ')' }
}
}
},
mounted () {
let screenWidth = document.documentElement.clientWidth
if (screenWidth >= this.xs && screenWidth < this.sm) { this.sourceImage = this.srcXs }
if (screenWidth >= this.s && screenWidth < this.md) { this.sourceImage = this.srcSm }
if (screenWidth >= this.md && screenWidth < this.lg) { this.sourceImage = this.srcMd }
if (screenWidth >= this.lg && screenWidth < this.xl) { this.sourceImage = this.srcLg }
if (screenWidth >= this.xl) { this.sourceImage = this.srcXl }
}
}
</script>
The goal is to create a background image for a hero section, but allow for different sized images for different screen sizes.
Here is how an implementation would look:
<template>
<hero-section class="full secondary"
src="http://via.placeholder.com/480x270"
src-xs="http://via.placeholder.com/480x270"
src-sm="http://via.placeholder.com/720x405"
src-md="http://via.placeholder.com/1440x810"
src-lg="http://via.placeholder.com/1920x1080"
src-xl="http://via.placeholder.com/2560x1440"
>
<v-container fill-height>
<v-layout column align-center justify-center>
<!-- TEXT GOES HERE -->
</v-container>
</hero-section>
</template>
Am I doing this right, or is there a better way to do it?
Thanks.