Vue + Vuetify expansion panel enter/leave animation - vue.js

I'm currently developing a webapp in Vue.js with the help of Vuetify.
I found the expansion panel element very usefull and I wanted to use it in my website.
So I easily get the data i want to display from Firebase and i proceed to load the items with this template:
<v-expansion-panel popout>
<v-expansion-panel-content
v-for="doc in filteredclienti"
:key="doc.id">
<div slot="header">{{doc.data().Nome}} {{doc.data().Cognome}}</div>
<v-card>
Card content
</v-card>
</v-expansion-panel-content>
</v-expansion-panel
Everything works fine, the panel is ok and the popup animation works fine too.
Now I'd like to display a simple enter/leave animation to each item.
I tried to add a <transition-group> tag after the <v-expansion-panel popuot> but the console tells me want only a <v-expansion-panel-content> element.
So i tried to add <transition-group> inside <v-expansion-panel-content> but in this case the layout is no more correct and the popup animation is not working anymore.
How can i do it? Thanks!

That should do the trick, I've added a delete button, that will slide the "deleted" doc out.
enjoy.
<template>
<v-app>
<v-expansion-panel popout>
<transition-group name="list" tag="v-expansion-panel">
<v-expansion-panel-content v-for="doc in filteredclienti" :key="doc.id">
<div slot="header">
<span>{{doc.data}}</span>
<v-btn class="error" #click="deleteItem(doc.id)">
<v-icon>delete</v-icon>
</v-btn>
</div>
<v-card>Card content</v-card>
</v-expansion-panel-content>
</transition-group>
</v-expansion-panel>
</v-app>
</template>
<script>
export default {
data: () => ({
filteredclienti: [
{ id: 1, data: "data1" },
{ id: 2, data: "data1" },
{ id: 3, data: "data1" },
{ id: 4, data: "data1" }
]
}),
methods: {
deleteItem(id) {
this.filteredclienti = this.filteredclienti.filter(d => d.id !== id);
}
}
};
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all 1s;
}
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateX(100vw);
}
</style>

Related

Vue/Vuetify - breakpoints based on component size

As far as I can see, Vuetify allows you to define breakpoints based on viewport size. Is it also possible to define breakpoints based on the size of the component? E.g.
when several components are shown on an overview page, it would use a more "compact" layout to be able to show the components side by side on large screens.
when only one component is shown it could take up more space (on large screens).
the "compact" layout could also be used when only one component is shown on a small screen.
on small screens, the overview page could show several components vertically rather than side by side.
Or can you recommend a better approach to this?
As far as I know, there is no such feature in Vuetify. However, you can always hide v-col containing the component and other columns will take up the freed-up space.
For example:
https://codepen.io/olegnaumov/pen/rNpzvEX
Since you cannot rely on global breakpoints (at least for changes based on the number of components), you can pass your component a layout prop that dictates the internal layout of the component.
Widget component
<template>
<div>
<div>Widget</div>
<v-row :class="{ 'flex-column': layout === 'vertical' }">
<v-col> Column 1 </v-col>
<v-col> Column 2 </v-col>
<v-col v-if="layout !== 'compact'"> Column 3 </v-col>
<v-col v-if="layout !== 'compact'"> Column 4 </v-col>
</v-row>
</div>
</template>
<script>
export default {
name: "Widget",
props: {
layout: {
type: String,
default: "normal", // Options: ['normal', 'vertical', 'compact']
},
},
};
</script>
Then, you can make use of two directives provided by Vuetify to compute the layout of the child components and parent container: v-mutate to watch for changes in the number of children (using the child modifier) and v-resize to watch for resize of the app.
Parent component
<template>
<v-app v-resize="onResize">
<v-container fluid>
<v-row
ref="widget_container"
v-mutate.child="onMutate"
:class="{ 'flex-column': layout_container === 'vertical' }"
>
<template v-for="n in show">
<v-col :key="n">
<widget :layout="layout_widget"></widget>
</v-col>
</template>
</v-row>
</v-container>
</v-app>
</template>
<script>
import Widget from "/src/components/widget.vue";
export default {
name: "App",
components: {
Widget,
},
data() {
return {
show: 2, // Any other method to change the number of widgets shown
// See the codesanbox for one of them
container_width: null,
widget_width: null,
// Tweak below values accordingly
container_breackpoints: {
400: "vertical",
900: "normal",
},
widget_breackpoints: {
200: "compact",
500: "normal",
},
};
},
computed: {
layout_container() {
let breackpoint = Object.keys(this.container_breackpoints)
.sort((a, b) => b - a)
.find((k) => this.container_width > k);
return this.container_breackpoints[breackpoint] || "normal";
},
layout_widget() {
if (this.layout_container === "vertical") return "vertical";
let breackpoint = Object.keys(this.widget_breackpoints)
.sort((a, b) => b - a)
.find((k) => this.widget_width > k);
return this.widget_breackpoints[breackpoint] || "normal";
},
},
methods: {
setLayout() {
this.container_width = this.$refs.widget_container.clientWidth;
this.widget_width =
this.container_width / this.$refs.widget_container.children.length;
},
onMutate() {
this.setLayout();
},
onResize() {
this.setLayout();
},
},
};
</script>
Note, this is not a production-ready solution, but rather a proof of concept showing the use of v-mutate and v-resize for this purpose.
You can see this Codesanbox for demonstration.
You can write your own, using useResizeObserver and applying size classes on the element, which can in turn be used to modify the CSS rules to itself and its descendants:
Example:
const { createApp, ref } = Vue;
const { createVuetify } = Vuetify;
const vuetify = createVuetify();
const { useResizeObserver } = VueUse;
const app = createApp({
setup() {
const el = ref(null)
const size = ref('')
useResizeObserver(el, (entries) => {
const { width } = entries[0].contentRect;
size.value =
width < 600
? "xs"
: width < 960
? "sm"
: width < 1264
? "md"
: width < 1904
? "lg"
: "xl";
});
return { el, size };
},
});
app.use(vuetify).mount("#app");
.xs span {
color: orange;
}
.sm span {
color: red;
}
.md span {
color: blue;
}
.lg span {
color: green;
}
<script src="https://cdn.jsdelivr.net/npm/vue#3.2.45/dist/vue.global.prod.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify#3.1.1/dist/vuetify.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/vuetify#3.1.1/dist/vuetify.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/#vueuse/shared"></script>
<script src="https://unpkg.com/#vueuse/core"></script>
<div id="app">
<v-app>
<v-main class="h-100">
<v-row no-gutters class="h-100" style="background: #f5f5f5">
<v-col
class="h-100"
cols="12"
sm="6"
lg="9"
>
<v-card class="h-100 pa-2" :class="size" ref="el">
v-card: {{ size }} <br>
window:
<span class="d-inline-block d-sm-none">xs</span>
<span class="d-none d-sm-inline-block d-md-none">sm</span>
<span class="d-none d-md-inline-block d-lg-none">md</span>
<span class="d-none d-lg-inline-block d-xl-none">lg</span>
<span class="d-none d-xl-inline-block">xl</span>
</v-card>
</v-col>
</v-row>
</v-main>
</v-app>
</div>

How to give a dynamically rendered element it's own data value & target it in it's parent component?

I have a BaseMenuItem component that has normal button elements as well as special news elements.
I have added a ticker effect to the news type els and want to stop the ticker on that element when it's clicked. Currently the click event stops the ticker effect on the whole group.
How can I target a single element from that group?
There are two methods, openNews one showing the specific news article that the element is linked to.
And clearItemType that clears the itemType upon recieving the emitted event from the BaseMenuItem component.
I'm just not sure which element to target to change it's itemType.
Does Vuejs have a way to make an unique data value for dynamically generated elements?
If you need anymore information please let me know!
Cheers!
BaseMenuItem
<template>
<q-btn align="left" dense flat class="main-menu-item" v-on="$listeners">
<div class="flex no-wrap items-center full-width">
<iconz v-if="iconz" :name="iconz" type="pop" color="black" class="mr-md" />
<q-icon v-if="menuIcon" :name="menuIcon" class="text-black mr-md" />
<div #click="$emit('stop-ticker')" v-if="itemType === 'news'" class="ellipsis _ticker">
<div class="ellipsis _ticker-item">{{ title }}</div>
</div>
<div v-else>
<div class="ellipsis">{{ title }}</div>
</div>
<slot>
<div class="ml-auto"></div>
<div class="_subtitle mr-md" v-if="subtitle">{{ subtitle }}</div>
<q-icon name="keyboard_arrow_right" class="_right-side" />
<ComingSoon v-if="comingSoonShow" />
</slot>
</div>
</q-btn>
</template>
<style lang="sass" scoped>
// $
.main-menu-item
display: block
font-size: 15px
position: relative
width: 100%
border-bottom: 1px solid #F5F5F5
+py(10px)
._left-side
color: #000000
._subtitle
margin-left: auto
opacity: 0.7
._ticker
position: absolute
font-weight: bold
margin-left: 2em
width: 82%
&-item
display: inline-block
padding-left: 100%
animation: ticker 8s linear infinite
#keyframes ticker
to
transform: translateX(-100%)
</style>
<script>
import { iconz } from 'vue-iconz'
export default {
name: 'MainMenuItem',
components: { iconz },
props: {
comingSoonShow: { type: Boolean, default: false },
title: { type: String, default: 'menu' },
subtitle: { type: String, default: '' },
menuIcon: { type: String, default: '' },
iconz: { type: String, default: '' },
itemType: { type: String, default: '' },
}
}
</script>
MainMenuPage
<template>
<div class="eachMenuGroup" v-if="newsList.length">
<MainMenuItem
v-for="news in newsList"
:key="news.id"
#click="openNews(news)"
:title="news.title"
:itemType="itemType"
:class="{ readLink: readNewsList[news.id] }"
menuIcon="contactless"
#stop-ticker="clearItemType"
></MainMenuItem>
</div>
</template>
<style lang="sass" scoped>
.readLink
font-weight: 500
</style>
<script>
methods: {
openNews(postInfo) {
dbAuthUser().merge({ seenNewsPosts: { [postInfo.id]: true } })
Browser.open({ url: postInfo.url, presentationStyle: 'popover' })
},
clearItemType() {
this.itemType = ''
return
},
</script>
EDIT: I edited since your latest comment. See below
To target the exact element within your v-for that fired the event, you can use $refs by using an index :
<MainMenuItem v-for="(item, index) in items" :ref="`menuItem--${index}`" #stop-ticker="clearItemType(index)" />
In your method:
clearItemType(index){
console.log(this.$refs[`menuItem--${index}`])
// this is the DOM el that fired the event
}
Edited version after your comments:
If you pass the exact same prop to each el of your v-for you wont be able to modify its value just for one child in the parent context.
Instead, keep an array of distinctive values in your parent. Each child will receive one specific prop that you can change accordingly in the parent either by listening to an child emitted event or by passing index as click event as I've done below for simplicity.
See snippet below
Vue.config.productionTip = false;
const Item = {
name: 'Item',
props: ['name', 'isActive'],
template: `<div>I am clicked {{ isActive }}!</div>`,
};
const App = new Vue({
el: '#root',
components: {
Item
},
data: {
items: ["item1", "item2"],
activeItemTypes: []
},
methods: {
toggle(index) {
this.activeItemTypes = this.activeItemTypes.includes(index) ? this.activeItemTypes.filter(i => i !== index) : [...this.activeItemTypes, index]
}
},
template: `
<div>
<Item v-for="(item, i) in items" :name="item" #click.native="toggle(i)" :isActive="activeItemTypes.includes(i)"/>
</div>
`,
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="root"></div>

How to disable vue draggable placeholder

I am currently doing a website builder, where user can drag and drop to add element.
The drag and drop works well, but what i want is, how can i disable/hide the drop placeholder in the target container ?
As show in the image, whenever I hover on a container, it will show a copy of my dragging element by default, which I don't want.
Here is my code :
<template>
<div style="display : flex;">
<div id="dragArea">
<draggable
class="dragArea list-group"
:list="list1"
:group="{ name: 'item', pull: 'clone', put: false }"
:clone="cloneItem"
#change="log"
>
<div class="list-group-item" v-for="element in list1" :key="element.id">{{ element.name }}</div>
</draggable>
</div>
<div id="dropArea">
<draggable class="dragArea list-group" :list="list2" group="item" #change="log">
<div class="list-group-item" v-for="element in list2" :key="element.id">{{ element.name }}</div>
</draggable>
</div>
</div>
</template>
Script :
<script>
import draggable from "vuedraggable";
let idGlobal = 8;
export default {
name: "custom-clone",
display: "Custom Clone",
order: 3,
components: {
draggable,
},
data() {
return {
hover : false,
list1: [
{ name: "cloned 1", id: 1 },
{ name: "cloned 2", id: 2 },
],
list2: [
]
};
},
methods: {
log: function(evt) {
window.console.log(evt);
},
cloneItem({ name, id }) {
return {
id: idGlobal++,
name: name
};
},
},
};
</script>
On each of your <draggable> components within your <template>, you can set the ghost-class prop to a CSS class that hides the drop placeholder (ie. "ghost", or "dragging element" as you called it) using display: none; or visibility: hidden;.
For example:
In your <template>:
<draggable ghost-class="hidden-ghost">
and in the <style> section of your Vue Single File Component, or in the corresponding stylesheet:
.hidden-ghost {
display: none;
}
Working Fiddle
The ghost-class prop internally sets the SortableJS ghostClass option (see all the options here). The ability to modify these SortableJS options as Vue.Draggable props is available as of Vue.Draggable v2.19.1.

Vuetify v-intersect in Nuxt app fires before enters view

I have a page with the following layout:
<template>
<v-container fluid class='pa-0 ma-0 assignment-container'>
<v-row class='pa-0 ma-0 gallery-bg'>
// ...v-img with height 60vh
</v-row>
<v-row class='pa-3'>
// ...row content
</v-row>
<v-row class='pa-3'>
// ... row content
</v-row>
<v-row
v-if='!works.length'
v-intersect='onIntersect'
class='pa-3 mt-4 flex-column'>
<v-row class='pa-3 ma-0'>
<h3 class='mb-4 acumin-semibold section-title section-title-h3'>
Works:
</h3>
</v-row>
<v-row class='pa-0 ma-0'>
<v-col
v-for='(skeleton, i) in skeletonCards'
:key='i'
xs='12'
sm='6'
md='4'
lg='3'>
<v-skeleton-loader
class='mx-auto'
max-width='374'
height='250'
type='card' />
</v-col>
</v-row>
</v-row>
<v-container v-else fluid>
<v-row class='pa-3 ma-0'>
<h3 class='mb-4 acumin-semibold section-title section-title-h3'>
Works:
</h3>
</v-row>
<v-row class='pa-0 ma-0'>
<v-col
v-for='work in works'
:key='work._id'
xs='12'
sm='6'
md='4'
lg='3'>
<WorkCard :assignment-id='$route.params.id' :work='work' />
</v-col>
</v-row>
</v-container>
</v-container>
</template>
<script>
// ...all the imports
export default {
components: {WorkCard, UIButton},
async asyncData(context) {
// ... fetch and return assignment
},
data() {
return {
works: []
}
},
computed: {
skeletonCards() {
return this.$vuetify.breakpoint.lg ? 4 : 3
}
},
methods: {
async fetchWorks() {
this.works = await this.$nuxt.context.app.apolloProvider.defaultClient.query({
query: worksByAssignmentIdQuery,
variables: {
id: this.$nuxt.context.route.params.id,
}
})
.then(({data}) => data.assignmentWorks)
.catch(err => console.error(err))
},
onIntersect() {
console.log('intersect fired')
this.fetchWorks()
}
},
}
</script>
The problem is that v-intersect directive fires even when it's not in the view yet.
I tried to define threshold:
v-intersect='{
handler: onIntersect,
options: {
threshold: [1.0]
}
}'
And it keeps firing.
Then I thought maybe it's because it's rendered on the server, so I tried to wrap this part of markup in <client-side> element. Still firing. I tried to wrap the entire page in that element, tried to put an empty <p> element after all the rest and apply that directive on it - and it still fired.
I had the fetching part inside fetch() method with fetchOnServer set to false and I called this.$fetch() in my onIntersect method. And it kept firing every time. As if this row is always in the view, even though it is not.
I ran out of ideas... Any help, please?
OK, looks like I finally solved it. First of, apparently you can't place the v-intersect directive on a v-row element.
So I created an invisible div element with 0px width and height, on which I applied the v-intersect directive:
...
<div
v-intersect='{
handler: onIntersect,
options: {
threshold: [1.0]
}
}'
class='invisible' />
...
<style scoped>
.invisible {
width: 0;
height: 0;
position: absolute;
bottom: 0;
}
<style>
Thein in my onIntersect method I'm passing isIntersecting parameter, and invoking the fetchWorks method if it's true:
onIntersect(entries, observer, isIntersecting) {
if (isIntersecting) {
this.fetchWorks()
}
}
Now it intersects correctly and fetching data when the view (div) is in viewport (even though it's invisible), whether when scrolled there or the page was refreshed on that pixel.
Since I'll need to use v-intersect in other parts of my project, I'm considering turning this div into a component, to which I'll pass the intersect callback function.

Render Component in loop, use Index in method of child component (VueJS)

I have two components where some exchange of props takes place. Props is the whole todo array, which is updated by a click on the button with the "addTodo" method. Passing the array down to the child works fine. I can display the props dynamically in my p-tags, but it seems to be not possible to use it my the methods of my child component.
<template>
<v-app>
<v-content>
<h2>Add a Todo</h2>
<v-col cols="12" sm="6" md="3">
<v-text-field label="Regular" v-model="text"></v-text-field>
</v-col>
<div class="my-3">
<v-btn medium #click="addTodo">Add Todo</v-btn>
</div>
<div v-for="(todo, index) in todos" v-bind:key="index">
<HelloWorld
v-bind:todos="todos"
v-bind:index="index"
v-bind:class="(todos[index].done)?'green':'red'"
/>
</div>
</v-content>
</v-app>
</template>
<script>
import HelloWorld from "./components/ToDo.vue";
export default {
components: {
HelloWorld
},
data: function() {
return {
text: "",
todos: []
};
},
methods: {
addTodo() {
this.todos.push({
text: this.text,
done: false
});
}
}
};
</script>
This is my child component
<template>
<v-card max-width="250">
<v-card-text>
<h2 class="text-center">{{todos[index].text}}</h2>
<p class="display-1 text--primary"></p>
<p>{{index}}</p>
</v-card-text>
<v-card-actions>
<v-btn text color="deep-purple accent-4" #click="done"></v-btn>
<v-btn text color="orange accent-4">Delete Task</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: ["todos", "index"],
methods: {
done() {
this.todos[1].text = "bla";
}
}
};
</script>
<style scoped>
.seperator {
display: flex;
justify-content: space-between;
}
</style>
I pass a whole array with objects as props, and using the index inside the p-tag works fine, but I also want to use it like this:
methods: {
done() {
this.todos[index].text = "bla";
}
}
'index' is not defined
Everything works fine, but I am not able use the index value inside the method. What am I doing wrong here?
The way you write it out, there is nothing in scope defining index. Where is that value coming from?
Index is a prop and so it must be referenced with this.
done () {
this.todos[this.index].text = 'bla'
}