Why is nuxt-link refreshing the page when used with Bootstrap-vue? - vue.js

I am using nuxt and bootstrap to build a custom hover dropdown menu for navigation. The issue I have is that my navigation submenu NuxtLinks are refreshing the entire page instead of smoothly changing the app content in my Nuxt block. The nav bar is dynamically generated in the default.vue layout and uses a b-dropdown-hover component where the NuxtLink is wrapped around that content. Why does the page do a full refresh for those links/anchors but my b-navbar-brand image does a smooth transition? I apologize, I am very new to Nuxt. This video # ~1:35:00 demonstrates what I'm trying to do.
components/BDropdownHoverRight.vue
<template>
<nuxt-link :to="aTo">
<div class="ddr-top" #mouseover="onOver1($event.target)" #mouseleave="onLeave1($event.target)">
<b-dropdown ref="dropdown_ddr" :text="cText" class="m-md-2 ddr">
<slot></slot>
</b-dropdown>
</div>
</nuxt-link>
</template>
<script>
export default {
name: 'BDropdownHoverRight',
props: {
cText: {
type: String,
},
aTo: {
type: String,
},
},
methods: {
onOver1(t) {
if (t.nodeName === 'DIV') {
console.log(t)
console.log(t.nodeName)
let num_child_nodes = 0
try {
if (t.querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length >= 0) {
num_child_nodes = t.querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length
}
} catch (e) {
if (t.querySelectorAll(':scope > div > ul')[0].getElementsByTagName('div').length >= 0) {
num_child_nodes = t.querySelectorAll(':scope > div > ul')[0].getElementsByTagName('div').length
}
}
if (num_child_nodes > 0) {
try {
t.querySelectorAll(':scope > div > ul')[0].style.display = 'block'
} catch (e) {
try {
t.querySelectorAll(':scope > ul')[0].style.display = 'block'
} catch (e) {}
}
}
}
},
onLeave1(t) {
try {
t.querySelectorAll(':scope > div > ul')[0].style.display = 'none'
} catch (e) {
try {
t.querySelectorAll(':scope > ul')[0].style.display = 'none'
} catch (e) {}
}
},
},
}
</script>
layouts/default.vue
<template>
<div>
<b-navbar id="top-nav-bar" toggleable="lg" type="light" sticky>
<b-navbar-brand to="/">
<Rabbit id="tl-logo" />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<template v-for="dir in navtop_dd">
<b-dropdown-hover
:key="dir.id"
:c-text="dir.name"
:a-to="dir.hasOwnProperty('ato') ? dir.ato : '/nolink'"
>
<template v-if="'submenus' in dir && dir.submenus.length > 0">
<template v-for="dir1 in dir.submenus">
<b-dropdown-hover-right
:key="dir1.id"
:c-text="dir1.name"
:a-to="dir1.hasOwnProperty('ato') ? dir1.ato : '/nolink'"
>
<template v-if="'submenus' in dir1 && dir1.submenus.length > 0">
<template v-for="dir2 in dir1.submenus">
<b-dropdown-hover-right
:key="dir2.id"
:c-text="dir2.name"
:a-to="dir2.hasOwnProperty('ato') ? dir2.ato : '/nolink'"
>
</b-dropdown-hover-right>
</template>
</template>
</b-dropdown-hover-right>
</template>
</template>
</b-dropdown-hover>
</template>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav-form>
<b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
<b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
</b-nav-form>
<b-nav-item-dropdown right>
<!-- Using 'button-content' slot -->
<template #button-content>
<b-img src="../assets/imgs/account-circle.svg" style="height: 35px"> </b-img>
<!-- <em>User</em> -->
</template>
<b-dropdown-item href="#">Profile</b-dropdown-item>
<b-dropdown-item href="#">Sign Out</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-container id="app-content">
<Nuxt />
</b-container>
<div id="footer">
<div style="height: 100%; padding: 5px">© 2021</div>
</div>
</div>
</template>
<script>
import BDropdownHover from '#/components/BDropdownHover'
import BDropdownHoverRight from '#/components/BDropdownHoverRight'
export default {
components: {
BDropdownHover,
BDropdownHoverRight,
},
data() {
return {
navtop_dd: [
{
id: 1,
name: 'Transactions',
ato: '/transactions',
submenus: [
{
id: '1a',
name: 'Sales Orders',
ato: '/transactions/salesorders',
submenus: [
{
id: '1b',
name: 'New',
},
{
id: '2b',
name: 'List',
},
],
},
{
id: '2a',
name: 'Item Fulfillments',
ato: '/transactions/itemfulfillments',
submenus: [
{
id: '1b',
name: 'New',
},
{
id: '2b',
name: 'List',
},
],
},
],
},
{
id: 2,
name: 'Inventory',
},
{
id: 3,
name: 'Reports',
},
{
id: 4,
name: 'Setup',
},
{
id: 5,
name: 'Support',
},
],
}
},
mounted() {
var x = document.querySelectorAll('.b-dropdown.navtop-dd')
for (var i = 0; i < x.length; i++) {
if (x[i].querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length == 0) {
var btn = x[i].querySelectorAll(':scope > .btn')[0]
btn.classList += ' no-content-after'
}
}
var x = document.querySelectorAll('.b-dropdown.ddr')
for (var i = 0; i < x.length; i++) {
if (x[i].querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length == 0) {
var btn = x[i].querySelectorAll(':scope > .btn')[0]
btn.classList += ' no-content-after'
}
}
},
}
</script>
<style>
#top-nav-bar {
border-bottom: 1px solid green;
}
#tl-logo {
height: 40px;
margin: 5px;
}
#footer {
height: 40px;
color: black;
border-top: 1px solid green;
margin: auto;
text-align: center;
display: flex;
align-items: center;
justify-content: space-around;
}
.navtop-dd button {
background: none !important;
color: #6c757d !important;
border: none !important;
}
#app-content {
margin: 20px auto;
}
.ddr > button::after {
display: inline-block;
margin-left: 0.555em;
right: 0px;
content: "";
border-top: 0.25em solid transparent;
border-right: 0.3em solid transparent;
border-bottom: 0.25em solid transparent;
border-left: 0.35em solid;
vertical-align: 0.075em;
}
.b-dropdown {
width: 100%;
}
.ddr > button {
text-align: left;
}
.no-content-after::after {
content: none !important;
}
.ddr > ul {
top: -1.2rem;
left: calc(100% - 0.5rem);
}
.dropdown-menu {
min-width: 0 !important;
}
.dropdown-item {
color: #6C757D;
}
.ddr-top:hover {
background-color: #e4ffda;
}
a:hover {
text-decoration: none !important;
}
</style>

There is a LOT of irrelevant code here. I took the time to format it properly. Please make the effort yourself next time (to format and input interesting bits only).
Also, the answer on how to fix the issue was actually given in the video itself. The video is talking about the differences between a and nuxt-link tags.
Which relates to this part of Bootstrap's Vue documentation where you can see that
[to] prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object
So, you should use something like this
<template>
<b-dropdown>
<template #button-content>
Custom <strong>Content</strong> with <em>HTML</em> via Slot
</template>
<b-dropdown-item to="/test">Go to test page via Vue-router</b-dropdown-item>
</b-dropdown>
</template>
I also saw that your code is rather different from the video. You should not use querySelector, you don't have to import Nuxt components neither and you have several ESlint warning/errors.
I do recommend trying to focus on a single part of learning and not mixing all of them. It's fine to want to go a bit further, but be careful of not being lost with too much abstraction while you do learn a lot of new concepts (Vue/Nuxt).
On a side note, if you want to continue learning Nuxt, you can check this: https://masteringnuxt.com/ (created by a Nuxt ambassador and other well known people in the Vue ecosystem)
Have fun creating projects with Nuxt!

Related

How to hide next and prev btn in carousel using vue 2 / nuxt js?

I have created a multi items carousel in which i slide one item at a time.
Initially i want to hide prev btn , but when clicked on next btn and one or more item is moved/slide to left in want the prev btn to be visible and when i am at end of the caorusel items i want to hide next button
template
<div class="main-container">
<div class="carousel-container position-relative" ref="container">
<div class="carousel-inner overflow-hidden">
<div class="carousel-track" >
<nuxt-link to="" class="card-container" v-for="(index, i) in 9" :key="i">
<div class="card"></div>
</nuxt-link>
</div>
</div>
<div class="btns-container">
<button class="prevBtn" #click="prev" >
prev
</button>
<button class="nextBtn" #click="next" >
next
</button>
</div>
</div>
</div>
script
methods: {
next() {
const track = document.querySelector('.carousel-track')
const item = document.querySelector('.card-container')
track.scrollLeft += item.clientWidth
},
prev() {
const track = document.querySelector('.carousel-track')
const item = document.querySelector('.card-container')
track.scrollLeft -= item.clientWidth
},
}
styles
.carousel-track{
display: flex;
overflow: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar {
display: none;
}
.card-container{
flex-shrink: 0;
scroll-snap-align: start;
}
.btns-container button{
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.prevBtn{
left: -1rem;
}
.nextBtn{
right: -1rem;
}
You should not use querySelectors but rather state since you're working with Vue.
Sorry, I didn't had a carousel example so I did something a bit different.
<template>
<div class="main-container">
<div ref="container" class="carousel-container position-relative">
<div class="carousel-inner overflow-hidden">
<div class="carousel-track">
<nuxt-link
v-for="(city, index) in cities"
:key="city.id"
to=""
class="card-container"
>
<div
class="card"
:class="index === currentCityIndex && 'active-city'"
>
{{ city.name }}
</div>
</nuxt-link>
</div>
</div>
<br />
<p>Current city index: {{ currentCityIndex }}</p>
<div class="btns-container">
<button v-show="currentCityIndex !== 0" class="prevBtn" #click="prev">
prev
</button>
<button
v-show="currentCityIndex !== cities.length - 1"
class="nextBtn"
#click="next"
>
next
</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentCityIndex: 0,
cities: [
{ name: 'New york', id: '1' },
{ name: 'Los Angeles', id: '2' },
{ name: 'Chicago', id: '3' },
{ name: 'Houston', id: '4' },
{ name: 'Philadelphia', id: '5' },
{ name: 'Phoenix', id: '6' },
{ name: 'San Antonio', id: '7' },
{ name: 'San Diego', id: '8' },
{ name: 'Dallas', id: '9' },
{ name: 'San Jose', id: '10' },
],
}
},
methods: {
next() {
this.currentCityIndex++
},
prev() {
this.currentCityIndex--
},
},
}
</script>
<style scoped>
.active-city {
border: 2px solid red;
}
</style>
The idea is mainly to do a check on the button with v-show="currentCityIndex !== 0" and display it depending on the current index you're on. You can of course also use visibility or any kind of cool CSS approach to avoid any shift regarding the location of the buttons.
v-show="currentCityIndex !== cities.length - 1" is checking if we are on the last element of your collection.
This is a basic carrousel I sometimes use and modify if I need it. It is originally an infinite carrousel but I added that function you are looking for.
You can copy the code and check how it works and sure I can be improve a lot. But so far it works.
Template
<template>
<div class="main-container">
<div class="carrousel-container">
<span ref="nextArrow" #click="nextSlide" class="slider-btn next">
<svg
width="25"
height="43"
viewBox="0 0 25 43"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M23.5858 20.0623L4.39859 0.875151C3.13866 -0.384778 0.984375 0.507553 0.984375 2.28936V40.6638C0.984375 42.4456 3.13866 43.3379 4.39859 42.078L23.5858 22.8908C24.3668 22.1097 24.3668 20.8434 23.5858 20.0623Z"
/>
</svg>
</span>
<span ref="prevArrow" #click="prevSlide" class="slider-btn prev">
<svg
width="25"
height="43"
viewBox="0 0 25 43"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M1.41421 20.0623L20.6014 0.875151C21.8613 -0.384778 24.0156 0.507553 24.0156 2.28936V40.6638C24.0156 42.4456 21.8613 43.3379 20.6014 42.078L1.41421 22.8908C0.633165 22.1097 0.633165 20.8434 1.41421 20.0623Z"
/>
</svg>
</span>
<!-- carousel -->
<div
:id="activeSlideIndex"
v-if="showSlides"
class="carousel-container"
>
<div
class="slide"
ref="slide"
v-for="(slide, idx) in slides"
:class="slide.position"
:key="slide.id"
>
{{ idx }}
</div>
</div>
<!-- carousel -->
</div>
</div>
</template>
Script and Styles
<script>
export default {
props: {
slides: {
type: Array,
default: () => [
{ id: 1, name: "sliderOne" },
{ id: 2, name: "slider Two" },
{ id: 3, name: "slider Three" },
],
},
},
mounted() {
this.orderSlides(this.slides, this.activeSlideIndex);
},
data() {
return {
activeSlideIndex: 0,
showSlides: false,
};
},
methods: {
nextSlide() {
++this.activeSlideIndex;
this.displayOrder(this.activeSlideIndex);
},
prevSlide() {
--this.activeSlideIndex;
this.displayOrder();
},
displayOrder() {
if (this.activeSlideIndex === this.slides.length) {
console.log(this.activeSlideIndex);
this.activeSlideIndex = 0;
} else if (this.activeSlideIndex < 0) {
this.activeSlideIndex = this.slides.length - 1;
}
this.orderSlides(this.slides, this.activeSlideIndex);
},
orderSlides(array, activeIndex) {
array.forEach((element, idx) => {
element.position = "nextSlide";
if (idx === activeIndex) {
element.position = "activeSlide";
} else if (
idx === activeIndex - 1 ||
(activeIndex === 0 && idx === array.length - 1)
) {
element.position = "lastSlide";
}
});
if (!this.showSlides) {
this.showSlides = true;
}
// toggle arrows
if (activeIndex === array.length - 1) {
this.$refs.nextArrow.style.display = "none";
} else if (activeIndex === 0) {
this.$refs.prevArrow.style.display = "none";
} else {
this.$refs.nextArrow.style.display = "block";
this.$refs.prevArrow.style.display = "block";
}
},
},
};
</script>
<style scoped>
.main-container {
height: 100vh;
width: 100%;
background: rgba(0, 0, 0, 0.6);
overflow: hidden;
display: grid;
place-content: center;
}
.carrousel-container {
width: 769px;
height: 631px;
position: relative;
padding-top: 37px;
padding-bottom: 20px;
}
.slider-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.slider-btn svg {
fill: red;
}
.next {
right: 25px;
}
.prev {
left: 25px;
}
.carousel-container {
width: 600px;
height: 631px;
overflow: hidden;
position: relative;
display: flex;
background: white;
justify-content: center;
align-items: center;
flex-direction: column;
margin: 0 auto;
}
.slide {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 2rem;
transition: 0.3s all linear;
background: var(--primary-light);
border-radius: var(--border-radius-1);
}
.slide.activeSlide {
opacity: 1;
transform: translateX(0);
}
.slide.lastSlide {
transform: translateX(-100%);
}
.slide.nextSlide {
transform: translate(100%);
}
img {
width: 100%;
}
</style>

(Vue3) [Vue warn]: Property "..." was accessed during render but is not defined on instance. at <App> error when Class binding

<template>
<div class="container">
<div class="gameboard">
<div class="title">Game Board</div>
<div class="main">
<div
v-for="item in boardFields"
:key="item.number"
:class="{ notclicked: !isclicked, clicked: isclicked }"
#click="toggleClick(item)"
>
{{ item.number }}
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "App",
components: {},
data() {
return {
boardFields: [],
};
},
methods: {
toggleClick(item) {
item.isclicked = !item.isclicked;
},
},
mounted() {
this.boardFields = [...Array(49)].map((_, i) => ({
number: i + 1,
isclicked: false,
}));
},
};
</script>
<style>
.notclicked {
font-size: 3.5rem;
background-color: gray;
display: flex;
justify-content: center;
align-items: center;
margin: 0.3rem;
width: calc(40.4rem / 7);
height: calc(40.4rem / 7);
border-radius: 0.8rem;
}
.clicked {
font-size: 3.5rem;
background-color: green;
display: flex;
justify-content: center;
align-items: center;
margin: 0.3rem;
width: calc(40.4rem / 7);
height: calc(40.4rem / 7);
border-radius: 0.8rem;
}
</style>
I want to change the class of each 'boardFields object' div through a click event by class binding to the 'isclicked' boolean in each object but I get this error message:
[Vue warn]: Property "isclicked" was accessed during render but is not defined on instance.
at
Does it have something to do with the fact that the objects are created in mounted()? Or is it something else?
The issue is in the class-binding:
:class="{ notclicked: !item.isclicked, clicked: item.isclicked }"
the parameters passed in by the a method have no meaning for the effect you want to achieve.i don't know whether you want to change the whole array after clicking or a single one. my example i give is to change the entire array, changeing the current one is almost the same , just adding a pointer to the loop where it traverses
data() {
return {
isclicked: false,
}
},
methods: {
toggleClick(item) {
this.isclicked = !this.isclicked;
},
},
<div
v-for="item in 10"
:class="{ notclicked: !isclicked, clicked: isclicked }"
#click="toggleClick()"
>
{{ item }}
</div>

Vuetify v-card elevation doesn't show up, flat attribute does

I'm trying to change the elevation of my v-cards in vuetify in my index.vue page but the changes dont show up, right now the code for my card reads: <v-card class="cards" elevation="0" shaped> but I tried it with class="elevation-0" as well. It doesn't work with any elevation but if I e.g. use the attribute: ``` the changes show up.
In my other pages the elevation attribute works (in my _slug.vue page), just in the index.vue page it doesn't. I also tried to change move the code of the cards to an own component but it doesn't work as well.
The Component in my index.vue file:
<template>
<NuxtLink :to="'/' + Link">
/* This is the card I'm talking about */
<v-card class="cards" elevation="0" shaped>
<img class="thumbnail" :src="Image" />
<h3 class="video-title">{{ Title }}</h3>
<h3 class="video-type">Most {{ Name }}:</h3>
</v-card>
</NuxtLink>
</template>
<script>
export default {
name: "ThumbnailCard",
props: {
Image: String,
Title: String,
Link: String,
Name: String,
},
};
</script>
<style scoped>
.cards {
margin-top: 15vh;
margin-left: 1vw;
margin-right: 1vw;
height: 30vh;
text-align: center;
}
.thumbnail {
width: 100%;
height: 30vh;
}
.video-type {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-top: 9vh;
}
.video-title {
position: absolute;
top: 0;
left: 0;
right: 0;
margin-top: 18vh;
}
#media only screen and (max-width: 599px) {
.cards {
margin-top: 2vh;
margin-bottom: 2vh;
margin-left: 15vw;
margin-right: 15vw;
height: 14.5vh;
}
.thumbnail {
height: 14.5vh;
}
.video-type {
margin-top: 4vh;
}
.video-title {
margin-top: 8vh;
}
}
</style>
The whole code for my index.vue:
<template id="main">
<div>
<div id="title-section">
<h1 id="title">Title</h1>
<h2 id="description">This Site is awesome, have a look</h2>
</div>
<v-container id="content-wrapper">
<v-row no-gutters>
<v-col cols="12" sm="4">
<ThumbnailCard
:Image="recentImage"
:Title="recentTitle"
:Link="recentLink"
Name="Recent"
/>
</v-col>
<v-col cols="12" sm="4">
<ThumbnailCard
:Image="popularImage"
:Title="popularTitle"
:Link="popularLink"
Name="Popular"
/>
</v-col>
<v-col cols="12" sm="4">
<ThumbnailCard
:Image="relevantImage"
:Title="relevantTitle"
:Link="relevantLink"
Name="Recent"
/>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script>
import ThumbnailCard from "#/components/global/ThumbnailCard";
export default {
name: "index",
components: {
ThumbnailCard: ThumbnailCard,
},
async asyncData({ $content, params }) {
const articles = await $content("articles")
.only(["date", "slug", "title", "img"])
.fetch();
const datesArr = articles.map((a) => {
return new Date(a.date).getTime() / 1000;
});
const recentTitle = articles[datesArr.indexOf(Math.min(...datesArr))].title;
const recentLink = articles[datesArr.indexOf(Math.min(...datesArr))].slug;
const recentImage = articles[datesArr.indexOf(Math.min(...datesArr))].img;
const popularTitle = articles[0].title;
const popularLink = articles[0].slug;
const popularImage = articles[0].img;
const relevantTitle = articles[0].title;
const relevantLink = articles[0].slug;
const relevantImage = articles[0].img;
return {
recentTitle,
recentLink,
recentImage,
popularTitle,
popularLink,
popularImage,
relevantTitle,
relevantLink,
relevantImage,
};
},
};
</script>
<style>
html {
overflow-y: auto;
}
::-webkit-scrollbar {
width: 5px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
#title-section {
background: black;
width: 100%;
height: 40vh;
text-align: center;
}
#title {
color: white;
font-size: 4rem;
}
#description {
color: white;
}
#content-wrapper {
height: 60vh;
width: 100%;
}
</style>
For anyone having the same problem: I forgot the <v-app> component which somehow resulted in the elevation attribute not working
the flat attribute works well to remove elevation from most vuetify components

How to animate todo moving from one list to another with Vue.js?

I am trying to do this svelte example todo moving animation with Vue.js.
Below you can find what I have done so far. Just click on the todo to see.
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div class="todo undone" v-for="item of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
In order to animate the todo move from one list to another, I used CSS grid but I can't find a way to distinguish todos (left and right) without having a grid cell which is empty.
I would appreciate if there is a better way to achieve the example in svelte docs or a way to omit the empty cells.
Even though it seemed easy in the beginning, it's a bit tricky.
You can target the first element by tracking the index in the v-for loop. Index 0 is always going to be the first element. And give it the following style:
grid-row-start: 1;
EDIT DEMO:
new Vue({
el: "#app",
data: {
items: [
{ id: 1, name: 'John', done: false },
{ id: 2, name: 'Jane', done: false },
{ id: 3, name: 'Jade', done: true },
{ id: 4, name: 'George', done: true },
]
},
computed: {
done () {
return this.items.filter(i => i.done)
},
undone () {
return this.items.filter(i => !i.done)
}
},
methods: {
toggle: function(todo){
todo.done = !todo.done
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
height: 500px;
transition: all 0.2s;
}
.todos {
display: grid;
grid-template-columns: 1fr 1fr;
}
.todo {
border: 1px solid #ccc;
}
.todo.undone {
grid-column: 2 /span 1;
}
.todo.done {
grid-column: 1 /span 1;
background: blue;
color: white;
}
.first-right {
grid-row-start: 1;
}
.flip-list-move {
transition: all 1s ease-in-out;
}
.header-wrapper {
display: grid;
grid-auto-flow: column;
}
.header, .todo {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="header-wrapper">
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
<div class="header">
<span>Name</span>
<span>Age</span>
<span>Gender</span>
</div>
</div>
<transition-group name="flip-list" tag="div" class="todos">
<div class="todo done" v-for="item of done" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>26</span>
<span>Male</span>
</div>
<div :class="['todo', 'undone', { 'first-right': index === 0 }]" v-for="(item, index) of undone" :key="item.id" #click="toggle(item)">
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
</transition-group>
</div>
Adding grid-row-start to the first undone element doesn't works if there are more than 6 items in array.
As a solution, I used the index of v-for loop to add to every undone todo the corresponding grid-row-start.
index starts at 0 so we have to make index + 1
<div
class="todo undone"
v-for="(item, index) of undone"
:key="item.id"
:style="{'grid-row': index + 1}" // => HERE we guarantee no gaps are present in undone list`
#click="toggle(item)"
>
<span>{{item.name}}</span>
<span>20</span>
<span>Male</span>
</div>
You can find the working example on this codesandbox

Vue Accordion with transition

I'm trying to integrate the Accordion component with a body transition, but without success :( . All is working as well except the animation.
template:
<div class="accordion">
<div class="accordion-title" #click="isOpen = !isOpen" :class="{'is-open': isOpen}">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body" :class="{'is-open': isOpen}">
<div class="card">
<slot name="body"></slot>
</div>
</div>
</div>
component:
props: {
title: {
type: String,
default: 'Title'
}
},
data() {
return {
isOpen: false
}
}
And styles:
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
transition: .3s cubic-bezier(.25,.8,.5,1);
&:not(.is-open) {
display: none;
height: 0;
overflow: hidden;
}
&.is-open {
height: auto;
// display: block;
padding: 16px;
}
}
.card {
height: auto;
}
I tried to use <transition> but it doesn't work with height or display properties.
Help please!
display:none will remove your content and avoid the animation, you should trick with opacity, overflow:hidden and height, but you ll be forced to do a method for that.
For example (not tested, but inspiring):
in template:
<div class="accordion" #click="switchAccordion" :class="{'is-open': isOpen}">
<div class="accordion-title">
<span>{{title}}</span>
<i class="ic ic-next"></i>
</div>
<div class="accordion-body">
<p></p>
</div>
</div>
in component (add a method):
methods: {
switchAccordion: function (event) {
let el = event.target
this.isOpen = !this.isOpen // switch data isOpen
if(this.isOpen) {
let childEl1 = el.childNodes[1]
el.style.height = childEl1.style.height
} else {
let childEl2 = el.childNodes[2]
el.style.height = childE2.style.height // or .clientHeight + "px"
}
}
}
in style:
.accordion {
transition: all .3s cubic-bezier(.25,.8,.5,1);
}
.accordion-body {
font-size: 1.3rem;
padding: 0 16px;
opacity:0
}
.is-open .accordion-body {
opacity:0
}
In this case, your transition should work as you want.
The javascript will change the height value and transition transition: all .3s cubic-bezier(.25,.8,.5,1); will do the animation