Nuxt Loader - Throttle for Custom Loader - vue.js

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() { ... }
}

Related

Set a value to one of the props

I use Vue2 and Vuetify. I have a selector component that extends the standard VAutocomplete. According to the logic of the component, I need to display loading when loading data. If you make a separate component, this is solved simply:
<template><v-autocomplete :loading="loading" /></template>
<script>
export default {
name: 'CustomSelect',
data() { return { loading: false } },
methods: {
methodWithLoading() {
this.loading = true
// do code...
this.loading = false
},
},
}
</script>
In the case of the extension, the code will be as follows:
<script>
import VAutocomplete from 'vuetify/lib/components/VAutocomplete'
export default VAutocomplete.extend({
name: 'CustomSelect',
methods: {
methodWithLoading() {
this.loading = true
// do code...
this.loading = false
},
},
})
</script>
loading works because there is a loadable mixin deep within VAutocomplete that has a prop loading in it. This prop is responsible for the loading lane, no local variable or method is used, i.e. the prop directly affects the loading lane.
And here begins the problem: I can't just change this.loading inside the component, because you can't put values of props directly.
How do I set the value of this.loading (I don't care if the parent component can overwrite it)?

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.

Infinite Scroll Implementation Renders Data Twice

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.

watch changes in html element using vue

I'm trying to watch HTML element in the followiing way:
computed: {
content(){
return document.querySelector(".tab-content")
}
},
watch: {
content(newVal) {
return newVal;
}
}
but when .tab-content changes (it's innerHTML) , vue wont track/respond to that. any idea why?
You can use MutationObserver.
It's a vanilla JS feature that lets you watch DOM element and react to the changes(aka mutations)
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
https://developer.mozilla.org/en-US/docs/Web/API/MutationRecord
<script setup>
let observer = null;
let target = document.querySelector(".tab-content");
let options = {
subtree: true
//childList: true,
//attributes: true,
}
onMounted(() => {
observer = new MutationObserver((mutationList, observer) => {
//Analyse mutationList here for conditional processing
});
observer.observe(target, options);
});
onUnmounted(() => observer && observer.disconnect());
</script>
<template>
<div class="tab-content"></div>
</template>
You can't watch computed values, the computed methods are already like watchers that are updated when the content changes. You use watch methods to watch for changes in data or props
export default {
data: () => ({
content: ''
}),
watch: {
content () {
return this.content
}
}
}

Update VueJs component on route change

Is there a way to re-render a component on route change? I'm using Vue Router 2.3.0, and I'm using the same component in multiple routes. It works fine the first time or if I navigate to a route that doesn't use the component and then go to one that does. I'm passing what's different in props like so
{
name: 'MainMap',
path: '/',
props: {
dataFile: 'all_resv.csv',
mapFile: 'contig_us.geo.json',
mapType: 'us'
},
folder: true,
component: Map
},
{
name: 'Arizona',
path: '/arizona',
props: {
dataFile: 'az.csv',
mapFile: 'az.counties.json',
mapType: 'state'
},
folder: true,
component: Map
}
Then I'm using the props to load a new map and new data, but the map stays the same as when it first loaded. I'm not sure what's going on.
The component looks like this:
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
watch: {
load: function() {
this.mounted();
}
},
mounted() {
let _this = this;
let svg = d3.select(this.$el);
d3.queue()
.defer(d3.json, `static/data/maps/${this.mapFile}`)
.defer(d3.csv, `static/data/stations/${this.dataFile}`)
.await(function(error, map, stations) {
// Build Map here
});
}
You may want to add a :key attribute to <router-view> like so:
<router-view :key="$route.fullPath"></router-view>
This way, Vue Router will reload the component once the path changes. Without the key, it won’t even notice that something has changed because the same component is being used (in your case, the Map component).
UPDATE --- 3 July, 2019
I found this thing on vue-router documentation, it's called In Component Guards. By the description of it, it really suits your needs (and mine actually). So the codes should be something like this.
export default () {
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
const id = to.params.id
this.AJAXRequest(id)
next()
},
}
As you can see, I just add a next() function. Hope this helps you! Good luck!
Below is my older answer.
Only saved for the purpose of "progress"
My solution to this problem was to watch the $route property.
Which will ended up you getting two values, that is to and from.
watch: {
'$route'(to, from) {
const id = to.params.id
this.AJAXRequest(id)
}
},
The alternate solution to this question handles this situation in more cases.
First, you shouldn't really call mounted() yourself. Abstract the things you are doing in mounted into a method that you can call from mounted. Second, Vue will try to re-use components when it can, so your main issue is likely that mounted is only ever fired once. Instead, you might try using the updated or beforeUpdate lifecycle event.
const Map = {
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
methods:{
drawMap(){
console.log("do a bunch a d3 stuff")
}
},
updated(){
console.log('updated')
this.drawMap()
},
mounted() {
console.log('mounted')
this.drawMap()
}
}
Here's a little example, not drawing the d3 stuff, but showing how mounted and updated are fired when you swap routes. Pop open the console, and you will see mounted is only ever fired once.
you can use just this code:
watch: {
$route(to, from) {
// react to route changes...
}
}
Yes, I had the same problem and solved by following way;
ProductDetails.vue
data() {
return {
...
productId: this.$route.params.productId,
...
};
},
methods: {
...mapActions("products", ["fetchProduct"]),
...
},
created() {
this.fetchProduct(this.productId);
...
}
The fetchProduct function comes from Vuex store. When an another product is clicked, the route param is changed by productId but component is not re-rendered because created life cycle hook executes at initialization stage.
When I added just key on router-view on parent component app.vue file
app.vue
<router-view :key="this.$route.path"></router-view>
Now it works well for me. Hopefully this will help Vue developers!
I was having the same issue, but slightly different. I just added a watch on the prop and then re-initiated the fetch method on the prop change.
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import Page from './content/Page.vue';
import Post from './content/Post.vue';
const props = defineProps({ pageSlug: String });
const pageData = ref(false);
const pageBodyClass = ref('');
function getPostContent() {
let postRestEndPoint = '/wp-json/vuepress/v1/post/' + props.pageSlug;
fetch(postRestEndPoint, { method: 'GET', credentials: 'same-origin' })
.then(res => res.json())
.then(res => {
pageData.value = res;
})
.catch(err => console.log(err));
}
getPostContent();
watch(props, (curVal, oldVal) => {
getPostContent();
});
watch(pageData, (newVal, oldVal) => {
if (newVal.hasOwnProperty('data') === true && newVal.data.status === 404) {
pageData.value = false;
window.location.href = "/404";
}
});
router - index.js
{
path: "/:pageSlug",
name: "Page",
component: Page,
props: true,
},
{
path: "/product/:productSlug",
name: "Product",
component: Product,
},
{
path: "/404",
name: "404",
component: Error404,
}