Passing the isLoading value to my component in nuxtjs app - vuejs2

Can anyone help with this, I'm trying to not render the component until the data has loaded
In my page I have
async asyncData ({ params, store, isLoading=true}) {
const thisCaseId = store.dispatch('cases/getCase', params.caseId );
isLoading = false
return { thisCaseId, isLoading}
},
Console logging confirms my isLoading does change from True to false
I thought I could pass this down to the component through a prop as follows
<case-summary :isLoading='isLoading' :caseId='caseId'></case-summary>
and then use a v-if on the container
<section class="case-summary-container" v-if="!isLoading">
But that doesn't work, although the isLoading value does appear to be passing to the child component through a prop as expected and working, just doesn't wait to render.
I also tried using the same on the page element before rendering the component
<b-card v-if="isLoading">
<h2>Loading ...</h2>
</b-card>
<b-card v-if="!isLoading">
<case-summary :caseId='caseId'></case-summary>
</b-card>
but this didn't work either.
Can anyone explain this to me please and what I would need to do to make this work as I still in both cases get that flicker where the previous record is displayed before re rendering the new results (Which is what I am trying to stop)
I should add I am passing an object so can't use .length although I assume as the previous content is still there this wouldn't work anyway
basically I want to delay loading my component until the new data has populated it
my action in my store is as follows
getCase ({ commit, context }, data) {
return new Promise ((reject) => {
this.$axios.get('/cases/' + data + '.json')
.then(res => {
const obj = {};
Object.assign(obj, res.data);
commit('GET_CASE', obj)
})
.catch(e => {
context.error(e)
reject('/cases/')
this.loading = false
})
})
},
Many Thanks

Related

The lifecycle of functions in Vue3 templates

This may be the minimal reproduction of my problem
<template>
<span class='current-page'>
{{ get_page_param('current') }}
</span>
</template>
const get_page_param = function(direction) {
// TODO: need delete the comment
console.log(`get_page_param(${direction}) is calling`);
try {
let url_string = null;
switch (direction) {
case 'next':
url_string = info.value.next;
break;
case 'previous':
url_string = info.value.previous;
break;
default:
return route.query.page;
}
const url = new URL(url_string);
return url.searchParams.get('page');
} catch (err) {
console.log(err);
}
};
onBeforeMount(()=>{
console.log('before mounting');
axios
.get('/api/article')
.then(response => {
info.value = response.data;
});
})
onMounted(() => {
console.log('mounted');
});
When I run this code, I see get_page_param(current) is calling is printed twice in the browser's console, and both of them happen before mounting, and the second is printed after mounted
my question is
Why this function called twice
If the first call is for rendering templates, what is the reason of the second
I checked Vue's official documentation about lifecycle hooks https://cn.vuejs.org/api/composition-api-lifecycle.html#onupdated, but I still don't understand why the page will be execute the function twice during the rendering process. I think it may be front-end related knowledge, but I haven't understood it
The reason why the rendering is called twice is related to the lifecycle of vue components. Vue will always render the component, when a reactive value changes. As you use a function inside a template, this will be called on each render cycle of the component.
In your example the info.value update will trigger a rerender. Also if using a function, vue needs to allways trigger a rerender if you change any reactive value.
That is also why you should not use functions inside the template. Whenever possible you should use computed properties and apply it inside the template. The advantage is, that vue will internally cache the value of the computed property and only update the component, if some of the values you use inside is updated.
You should make sure, you understand how the vue lifecycle works.
A better approach would be to use a computed property that is rendered inside your template. The following shows an example that might work for you.
const direction = ref('');
const pageParam = computed(() => {
try {
let url_string = null;
switch (direction.value) {
case 'next':
url_string = info.value.next;
break;
case 'previous':
url_string = info.value.previous;
break;
default:
return route.query.page;
}
const url = new URL(url_string);
return url.searchParams.get('page');
} catch (err) {
console.log(err);
return '1'; // you need to return the fallback page
}
});
Inside the template you use
<template>
<span class='current-page'>
{{ pageParam }}
</span>
</template>

Vue3: Why when I scroll to element's scrollHeight, the last element is not visible?

I'm trying to make a chat window, where when I send/get a message, the window scrolls to the very bottom. That's how I make it:
template:
<ul class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
script:
const messages = ref()
socket.on('chat-message', (data) => {
messages.value.push(data)
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
But when coded this way, the last message is never seen (you need to scroll to it).
I found out that when I use setTimeout, like this:
setTimeout(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.offsetHeight
}, 10)
then it works fine. So I know how to make it work, but I don't know why I need to use setTimeout. Can anybody please explain? Is there a better way to do it?
The reason why it works with setTimeout is the lifecycle of a vue component. You should read the documentation page to understand the how the lifecycle works.
So if you set a variable like you do
messages.value.push(data)
Vue needs to run a new lifecycle to update the component. If you access the DOM directly after the change of the value, Vue might not have been updated the component yet.
But, you can actively say, you want to wait for the update to be done with the nextTick function: https://vuejs.org/api/general.html#nexttick
// import nextTick from vue
import { nextTick } from 'vue';
// callback needs to be async
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
})
// depending on the weight of your overall page,
// sometimes you also need to request the animation frame first
// this is better then using setTimeout. But you should try without first.
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
requestAnimationFrame(() => {
const chatWindow = document.querySelector('.chat-window')
chatWindow.scrollTop = chatWindow.scrollHeight
});
})
Instead of using querySelector you should use a template ref for the html element as described here: https://vuejs.org/guide/essentials/template-refs.html
<ul ref="chatWindow" class="chat-window">
<li v-for="message in messages">
<span>{{ message.from }}</span>{{ message.message }}
</li>
</ul>
// setup script
const messages = ref([]) // don’t forget the initial empty array here
const chatWindow = ref(null) // template refs are always initialized with null
socket.on('chat-message', async (data) => {
messages.value.push(data)
await nextTick();
// you need to make sure, the chatWindow is not null
if (!chatWindow.value) return;
chatWindow.value.scrollTop = chatWindow.value.scrollHeight
})

vue3: control property with a timed function

First of all, I am a new vuejs developer and my purpose is to get acquainted with Vue, so, not going to use any external plugins or components.
I am writing a simple alert component, which looks like this:
<Alert :show="showAlert" />
I want the show property to return back to false after 2 seconds. How can I do this from inside the component (i.e., not in the page where this component is used). I tried this:
import { computed } from 'vue';
export default {
props: ['show'],
setup(props) {
const shown = computed(() => {
if (props.show) {
setTimeout(() => {
console.log("hiding the alert...")
props.show = false
}, 2000);
}
return props.show.value
})
return { shown }
}
};
the compiler said:
14:15 error Unexpected timed function in computed function vue/no-async-in-computed-properties
16:19 error Unexpected mutation of "show" prop vue/no-mutating-props
My rational is that the delay of alert should be controlled by the alert component (which could be changed by a prop), but not forcing the caller to write some thing like:
function Alert(delay) {
showAlert = true
setTimeout(() => showAlert = false, delay)
}
There are 2 errors.
First vue/no-mutating-props, props are read only so you are not supposed to change it from within the component. It is still possible to change props from outside the component and pass down to it.
For this you should copy the value of props to your data()
data() {
return {
showAlert
}
}
You should be able to update showAlert with no problem.
The second error vue/no-async-in-computed-properties, you cannot write async function inside computed(), so the alternative is to use watch instead.

Vuetify v-select not showing items after full refresh

I have a dialog with a v-select that doesn't show values in the drop down after a full page load. It does work after a hot module reload triggered by modifying the component file.
Using vuetify:
"vuetify": "^1.5.17",
Component contains:
template code:
<v-select
:items="routes"
label="Routes"
multiple
chips
persistent-hint
hint="Send to which routes"
v-model="message.routes"
></v-select>
routes is a computed property:
routes: {
get() {
return this.$store.state.routes
}
}
The data gets downloaded in the created event:
created() {
this.downloadRoutes()
}
This maps to a store method that does an AJAX call that commits the returned list:
downloadRoutes({ commit, state }) {
return new Promise((resolve, reject) => {
commit('SET_LOADING', true)
api.get('/route').then(response => {
var routes = response.data.routes
commit('SET_ROUTES', routes)
commit('SET_LOADING', false)
resolve()
})
.catch(function(error) {
commit('SET_LOADING', false)
reject()
})
})
},
AJAX response is just an array of routes:
{"routes":["Route1","Route2","RouteXX"]}
This I have shown by doing a console.log of the response and the state in the computed routes property.
What am I missing?
It sounds like your problem lies somewhere inside the vue instance lifecycle, i.e. you might call this.downloadRoutes() inside the wrong hook. Do your problem still occure if you try with a hardcoded array of Routes?
Found the problem.
My store is initialised via an initialState function.
export default new Vuex.Store({
state: initialState,
getters: {
......
The initial state function declares all the top level collections used in state.
function initialState() {
return {
fullName: localStorage.fullName,
routes: [] // Was missing
}
Adding routes in that initial state solves the problem.
I think it's because watchers on state are not added when new attributes are added to state.
When I do a full page refresh the routes gets added to state after the select is initialized and so it is not watching for changes. During hot module reload the routes is already in the state and is so picked up by the select.

Loading indicator for long (non-AJAX) function?

I have a function in my Vue app which takes some time (about 2-3 seconds) to complete. It is not an AJAX call.
I would like to include a loading indicator while this code executes, but I am having trouble accomplishing it. I thought I could do the following...
<div v-on:click="doThings()">{{stuff}}</div>
methods: {
doThings: function () {
this.loading = true
console.log(this.loading)
longFunction()
this.loading = false
console.log(this.loading)
}...
}
...but that doesn't work. doThings() seems to not execute anything until longFunction() is done. I even tried making a separate function and changing my button to perform two functions like this...
<div v-on:click="doLoading(); doThings();">{{stuff}}</div>
...but this is also doesn't work. Is what I'm trying to do possible?
Use async code for longFunction() and set this.loading to false after the Promise is resolved.
<div v-on:click="doThings()">{{stuff}}</div>
methods: {
doThings: function () {
this.loading = true
longFunction().then(() => {
this.loading = false
})
}
}
var longFunction = function() {
return new Promise((resolve, reject) => {
window.setTimeout(()=>{ // force a new (pseudo)thread
// do stuff, then
resolve()
},100); // ...some reasonably short interval. One millisecond is working for me when testing locally, but it might be better to include some extra buffer, to ensure Vue is in its next tick
});
}
Alternatively, you could pass an object reference to longFunction that your Vue component can watch for changes on, and use that as the signal back to the component that it can set loading to false.