Vuetify v-intersect in Nuxt app fires before enters view - vue.js

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.

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>

Vuetify reset form after submitting

I am using a form inside dialog using vuetify.
Imported the component in page like this -
<template>
<div>
<topicForm :dataRow="dataRow" v-model="dialog" />
</div>
</template>
methods: {
openDialog(item = {}) {
this.dataRow = item;
this.dialog = true;
},
}
Dialog form code -->
<template>
<div>
<v-dialog v-model="value" max-width="500px" #click:outside="close">
<v-card outlined class="pt-5">
<v-form ref="form" class="px-3">
<v-card-text class="pt-5">
<v-row no-gutters>
<v-text-field
required
outlined
label=" Name"
v-model="data.name"
:rules="[rules.required]"
></v-text-field>
</v-row>
<v-row no-gutters>
<v-textarea
required
outlined
label=" Description"
v-model="data.description"
></v-textarea>
</v-row>
</v-card-text>
</v-form>
<v-divider> </v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
large
dark
outlined
color="success"
#click="save"
class="ma-3"
>
Save
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
props: [
"dataRow",
"value",
// ----------------------
],
methods: {
save() {
if (this.$refs.form.validate()) {
this.$root
.$confirm("Are you sure you want to save?")
.then((confirm) => {
if (confirm) {
this.ADD_TOPIC_DATA(this.data)
.then((data) => {
this.FETCH_TOPIC_DATA();
this.$refs.form.reset();
this.$refs.form.resetValidation();
this.close();
})
.catch((err) => {
console.log(err)
});
}
});
}
},
close() {
this.$emit("input", false);
},
}
watch: {
dataRow(val) {
this.data = { ...val };
},
},
Problem I am having is after adding a data, then if I try to add again by opening the dialog, the required field shows validation error, which is name here!
Image of that -->
Searched in stackoverflow. Found that should use this.$refs.form.reset(). Used that in save method without success. Also used this.$refs.form.resetValidation(), but don't work.
Any suggestion?
Thanks in advance.
The problem here is you're assigning new value to dataRow when opening the dialog which triggers validation inside the dialog. You could also use lazy-validation prop which allows you to only manually trigger the validation.

Adding more than 1 field in v-for is causing an infinite loop

I am trying to add a payment form for the user to fill out of field.type === 'payment'. However, when I add more than one field to add inside of the v-for loop, I get a “You may have an infinite update loop in a component render function.” error. What can I do to avoid this? Here is a snippet of what I'm trying to do
<div v-for="(field, key) in page.fields" :key="key">
<v-row v-if="field.type === 'payment'">
<v-col cols="12" sm="8">
<v-text-field //ADDING THIS FIELD BY ITSELF WORKS FINE
label="Card Number"
prepend-inner-icon="credit_card"
v-model="card_number"
/>
</v-col>
<v-col cols="12" sm="4">
<v-text-field //WHEN I TRY TO ADD IN THIS FIELD, THE LOOP ERROR OCCURS
label="CVV"
v-model="cvv"
/>
</v-col>
</v-row>
</div>
<script>
computed: {
...mapGetters('formbuilder', ['form'])
},
watch: {
form(newVal) {
this.page = newVal;
}
},
data() {
return {
cvv: '',
card_number: '',
page: {}
}
}
</script>

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'
}

Vue + Vuetify expansion panel enter/leave animation

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>