Infinite Scroll Implementation Renders Data Twice - vue.js

I'm implementing an infinite scroll in Nuxt 2. When I scroll to the bottom of the page, my function is executing twice (two GET requests). It should be once. How to achieve this?
<template>
<main>
<div v-for="content in contents" :key="content.id">
<div>{content.name}</div>
</div>
</main>
</template>
<script>
// Minimal setup
import { throttle } from 'lodash'
import axios from 'axios'
data() {
return {
loading: false,
contents: []
}
},
// Methods
methods: {
handleScroll() {
// Vanilla JS
const pixelsFromWindowBottomToBottom = 0 + document.body.offsetHeight - window.pageYOffset - window.innerHeight
if (pixelsFromWindowBottomToBottom < 200) {
this.getContents() // <-- Fires twice. Why?
}
},
getContents() {
this.loading = true // Got it. This is this issue
axios.get('foo')
.then(({ data }) => {
this.contents = this.contents.concat(data) // array
})
}
},
created() {
if (process.browser) {
document.addEventListener('scroll', throttle(this.handleScroll, 300))
}
}
</script>
this.getContents() seems to fires/renders twice: I get duplicate data displayed on page. Have I placed the document.addEventListener in the correct place?
Update:
this.loading = true is causing it. When I update the data(), this triggered the double execution.

Related

Nuxt Loader - Throttle for Custom Loader

I'm using a custom loader component for my project, and my nuxt config looks like this:
loading: '~/components/common/loading.vue'
The problem is that this component doesn't throttle a few milli-seconds and with every page change, this flickers and causes a bad user experience. Is there any way to add a throttle as we'd normally add for the default component like throttle: 200 inside the loading object like,
loading: { throttle: 200 }
Since my loading option doesn't have an object, instead has a string/path to my custom loading component, I'm not sure what to do here.
Reference: https://nuxtjs.org/docs/2.x/features/loading
This is how I use a custom loading component using Vuetify overlay component with a throttle:
<template>
<v-overlay :value="loading">
<v-progress-circular
indeterminate
size="64"
/>
</v-overlay>
</template>
<script>
export default {
data: () => ({
loading: false
}),
methods: {
clear () {
clearTimeout(this._throttle)
},
start () {
this.clear()
this._throttle = setTimeout(() => {
this.loading = true
}, 200)
},
finish () {
this.clear()
this.loading = false
}
}
}
</script>
This is inspired by the Nuxt default loading component.
You could add a setTimeout within your start() method in your custom loader component ~/components/common/loading.vue.
methods: {
start() {
setTimeout(() => {
this.loading = true;
}, 2000);
},
finish() { ... }
}

Show HTML content with events, loaded from the backend in Vue template

I need to show an image and HTML content with events in the template.
The HTML of the template comes in part from the backend and I need to do a treatment on the front end.
I need to put an image in the new HTML.
I'm doing it this way, but it doesn't work.
The image is always empty.
<template>
<div
v-html="resultado"
></div>
</>
data: ()=>({
resultado:null
}),
mounted(){
fillElement();
},
computed:{
getImage() {
return require("#/assets/pdf.png");
},
},
methods:{
fillElement(){
//get html from backend
const ohtml=getHtmlFrmBackEnd();
let p1 = `<div>Image<img :src='getImage()'></img>${ohtml}</div>`;
this.resultado = p1;
},
}
Solution:
<template>
<div>
<component :is="resultado"></component>
</div>
</template>
<script>
import Vue from "vue";
export default {
data: () => {
return {
resultado: null
};
},
computed: {
compiledData() {
return {
resultado: null
};
}
},
methods: {
delay() {
//making a backend call
return new Promise(resolve => {
setTimeout(() => {
resolve(
"<input type='button' name='btnVoltar' id='btnVoltar' value=' Voltar ' class='button' v-on:click='fVoltar()'>"
);
}, 1000);
});
},
replace(content) {
this.resultado = Vue.component("template-from-server", {
template: content,
methods: {
fVoltar() {
console.log("click");
}
}
});
},
async fillElement() {
//get html from backend
const ohtml = await this.delay();
let p1 = `<div>Image<img src='${require("#/assets/logo.png")}'></img>${ohtml}</div>`;
this.replace(p1);
}
},
mounted() {
this.fillElement();
}
};
</script>
Working Code Example
You can see I loaded the image directly into the src and called fillElement() with this keyword in the mounted() hook.
I also added a delay function to demonstrate a request to the backend.
Edit:
In order to handle events coming with the template from the backend, I created a mini component within the current component that will get rendered once the content is passed. For that, I had to locally import Vue.
Please keep in mind that you will need to replace onclick with #click or v-on:click. You can use regex for that as you have done so already.

Detect vuex state change to execute a method inside a nuxt layout

I am trying to show vuetify snackbar alert, once I completed a form submission inside a page or vue component. I use vuex store to manage alert type and message.
my-nuxt-app/store/alerts.js
export const state = () => ({
message: '',
type: ''
});
export const getters = {
hasAlert(state) {
return state.message !== '';
},
alertMessage(state) {
return state.message;
},
alertType(state) {
return state.type;
}
};
export const mutations = {
SET_ALERT(state, payload) {
state.type = payload.type;
state.message = payload.message;
}
};
export const actions = {
setAlert({commit}, payload) {
commit('SET_ALERT', payload);
},
clearAlert({commit}) {
commit('SET_ALERT', {});
}
};
And I created a nuxt plugin to access getters globally in my application.
my-nuxt-app/plugins/alert.js
import Vue from 'vue';
import {mapGetters} from 'vuex';
const Alert = {
install(Vue, options) {
Vue.mixin({
computed: {
...mapGetters({
hasAlert: 'alerts/hasAlert',
alertType: 'alerts/alertType',
alertMessage: 'alerts/alertMessage'
})
}
});
}
};
Vue.use(Alert);
Inside my AccountForm component submit method, I am dispatching my alert information to store like below.
my-nuxt-app/components/form/AccountForm.vue
...
methods: {
async submit () {
try {
await this.$axios.patch("/settings/profile", this.form);
this.$store.dispatch('alerts/setAlert', {
type: 'success',
message: 'You have successfully updated your information.'
});
} catch (e) {
}
}
},
...
}
...
And this AccountForm.vue component is a child component of profile.vue page which is obviously inside the pages folder of my project. And also I have extended the dashboard.vue layout to this profile.vue page and to the most of the pages inside my pages directory as a common layout. Hence, I added the snackbar component into dashboard layout to show a alert message whenever required.
my-nuxt-app/layouts/dashboard.vue
<template>
...
<v-snackbar
:timeout="snackbar.timeout"
:color="snackbar.color"
:top="snackbar.y === 'top'"
:bottom="snackbar.y === 'bottom'"
:right="snackbar.x === 'right'"
:left="snackbar.x === 'left'"
:multi-line="snackbar.mode === 'multi-line'"
:vertical="snackbar.mode === 'vertical'"
v-model="snackbar.show"
>
{{ snackbar.text }}
<v-btn flat icon dark #click.native="snackbar.show = false">
<v-icon>close</v-icon>
</v-btn>
</v-snackbar>
...
</template>
<script>
...
data: () => ({
snackbar: {
show: false,
y: 'top',
x: null,
mode: '',
timeout: 6000,
color: '',
text: ''
},
}),
computed: {
availableAlert: function () {
return this.hasAlert;
}
},
watch: {
availableAlert: function(alert) {
if(alert) {
this.showAlert(this.alertType, this.alertMessage);
this.$store.dispatch('alerts/clearAlert');
}
}
},
methods: {
showAlert(type, message) {
this.snackbar.show = true;
this.snackbar.color = type;
this.snackbar.text = message;
}
}
</script>
I am getting the alert message for the first time submission of the form and after that I have to reload the page and then submit to get the alert. Please enlighten me a way to detect the vuex state change and trigger showAlert method inside the dashboard.vue accordingly.
It's most likely the way you're checking hasAlert
Your clearAlert passes an empty object, your setAlert is trying to assign properties of that empty object, while your hasAlert is checking if it's an empty string.
If you change your clearAlert to:
clearAlert({commit}) {
commit('SET_ALERT', { message: '', type: '' });
}
That should fix your issue.

Vue content modified after serverPrefetch on client side, when using SSR

I am working with Vue, by means of Quasar, with the pages being rendered via SSR. This works well enough, but I have a component that doesn't seem to behaving properly.
The issue is that the content is rendered correctly on the server side (verified by checking network log in Chrome), with the axios call loading in the data into an element using v-html, but when we get to the browser the state seems to be reset and server side rendered content gets lost, when using the 'elements' tab in the inspector.
Any ideas?
The Vue component is as follows:
<template>
<div class="dy-svg" v-html="svgData"></div>
</template>
<script>
/**
* This provides a way of loading an SVG and embedding it straight into
* the page, so that it can have css applied to it. Note, since we are
* using XHR to load the SVG, any non-local resource will have to deal
* with CORS.
*/
import axios from 'axios';
export default {
props: {
src: String,
prefetch: {
type: Boolean,
default: true
}
},
data() {
return {
svgData: undefined,
};
},
async serverPrefetch() {
if (this.prefetch) {
await this.loadImage();
}
},
async mounted() {
// if (!this.svgData) {
// await this.loadImage();
// }
},
methods: {
async loadImage() {
try {
let url = this.src;
if (url && url.startsWith('/')) {
url = this.$appConfig.baseUrl + url;
}
const response = await axios.get(url);
let data = response.data;
const idx = data.indexOf('<svg');
if (idx > -1) {
data = data.substring(idx, data.length);
}
this.svgData = data;
} catch (error) {
console.error(error);
}
}
}
};
</script>
Note, I did try add the v-once attribute to the div, but it seems to have no impact.
Environment:
Quasar 1.1.0
#quasar/cli 1.0.0
#quasar/app 1.0.6
NodeJS 10.15.3
Vue 2.6.10 (dependency via Quasar)
The fetched data needs to live outside the view components, in a dedicated data store, or a "state container". On the server, you should pre-fetch and fill data into the store while rendering. For this you can use Vuex.
Example Vuex store file:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
// import example from './module-example'
Vue.use(Vuex)
export default function ( /* { ssrContext } */ ) {
const Store = new Vuex.Store({
state: () => ({
entities: {}
}),
actions: {
async get({
commit
}) {
await axios.get('https://example.com/api/items')
.then((res) => {
if (res.status === 200) {
commit('set', res.data.data)
}
})
}
},
mutations: {
set(state, entities) {
state.entities = entities
},
},
modules: {},
// enable strict mode (adds overhead!)
// for dev mode only
strict: process.env.DEV
})
return Store
}
Example Vue page script:
export default {
name: 'PageIndex',
computed: {
// display the item from store state.
entities: {
get() {
return this.$store.state.entities
}
}
},
serverPrefetch() {
return this.fetchItem()
},
mounted() {
if (!this.entities) {
this.fetchItem()
}
},
methods: {
fetchItem() {
return this.$store.dispatch('get')
}
}
}
This should solve the issue you're facing.

ag grid not retrieving data when mounted with vue using axios

I have this strange case when trying to retrieve data from mongoDB using axios not showing on grid. It should be already successful given the data can already loaded into the view (already tested it), but it's nowhere inside beforeMount, mounted, or ready hook.
I already tried with
this.gridOptions.onGridReady = () => {
this.gridOptions.api.setRowData(this.ticketData)
}
but only yields partial success (unreliable),
here's a code snippet to show what I mean,
<template>
<div class="ticketing">
<ag-grid-vue style="width: 100%; height: 350px;"
class="ag-fresh"
:gridOptions="gridOptions"
>
</ag-grid-vue>
{{testData}} <!--testData can be loaded-->
<input type="button" #click.prevent="showData" value="test"> </div>
</template>
<script>
//import stuff
//header and url stuff
export default {
//component stuff
data () {
return {
gridOptions: null,
ticketData: [],
testData: [] // only for testing purpose
}
},
methods: {
showData () {
console.log('data shown')
this.testData = this.ticketData // this is working
}
},
beforeMount () {
var vm = this
axios.get(ticketingAPIURL, {'headers': {'Authorization': authHeader}})
.then(function (response) {
vm.ticketData = response.data
}) // this is working
.catch(function (error) {
console.log(error)
})
this.gridOptions = {}
this.gridOptions.rowData = this.ticketData // this is not working
this.gridOptions.columnDefs = DummyData.columnDefs
}
// mount, ready also not working
}
</script>
To be more specific, I still can't determine what really triggers onGridReady of ag-grid in conjunction with Vue component lifecycle, or in other words, how can I replace button to show testData above with reliable onGridReady/Vue component lifecycle event?
You define vm.ticketData and after you call it like this.ticketData
You can change it by: this.rowData = vm.ticketData
You are setting this.gridOptions.rowData outside of the axios callback, so this.ticketData is still empty.
Set it inside the callback:
mounted() {
var vm = this
axios.get(ticketingAPIURL, {'headers': {'Authorization': authHeader}})
.then(function (response) {
vm.ticketData = response.data
vm.gridOptions = {}
vm.gridOptions.rowData = vm.ticketData
vm.gridOptions.columnDefs = DummyData.columnDefs
})
.catch(function (error) {
console.log(error)
})
}
it is due to overlapped intialization between axios, ag-grid, and vue.
after much tinkering, I am able to solve it with using Vue's watch function:
watch: {
isAxiosReady(val) {
if (val) {
this.mountGrid() // initiate gridOptions.api functions
}
}
}