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.
Related
I'm creating a messaging app and I'm having some trouble with scrolling to the bottom of an ion-content element when a new message is added to an array. I'm using the scrollToBottom() method that comes with ion-content, and I'm using the Composition API in Vue 3.
Consider this snippet:
setup(props) {
const replyContent = ref("")
const messages = ref([])
// References to ion-content in the template
const ionContent = ref(null)
const reply = async () => {
const message = await replyToThread(props.threadId, replyContent.value).then((message) => message)
messages.value.push(message)
nextTick(() => {
console.log("DOM updated!")
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
})
}
return { replyContent, messages, ionContent, reply }
}
replyToThread() performs an API call and returns the new message, and nextTick() should ensure me that the DOM has been updated so that I can have my way with it. The console does successfully log "DOM updated!", but no scrolling to the bottom happens.
But, and somehow this works every time nextTick() doesn't, when I replace the nextTick() code block with the following, it works flawlessly:
setTimeout(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
}, 200)
I have to set the timeout at around 200 ms, otherwise it doesn't work. But relying on this when something fancy like nextTick() should do the trick feels quite dirty. Does anyone know why this is happening?
That is because nextTick() only guarantees that the actual DOM has been updated: it doesn't mean that the browser has actually finished the layout of the page. That is the reason why you need an arbitrary timeout to ensure the scrolling works, because after 200ms the browser is likely to be done laying things out based on the updated DOM.
To fix this you will probably need to rely on window.requestAnimationFrame:
nextTick(() => {
window.requestAnimationFrame(() => {
if (ionContent.value) {
ionContent.value.$el.scrollToBottom()
}
});
});
If this feels like too much nesting for you, you can create a method that returns a promise based on rAF:
methods: {
rAF: function() {
return new Promise(r => window.requestAnimationFrame(r));
}
}
Then it's a matter of ensuring promises returned by both nextTick() and rAF() are resolved before scrolling:
await nextTick();
await this.rAF();
if (ionContent.value) {
ionContent.value.$el.scrollToBottom();
}
What is the right way to put a false in loading, (after the dispatch)?
Like: (I know it doesn't work)
this.loading = true;
this.$store.dispatch('items', data);
this.loading = false;
<script>
export default {
data() {
return {
loading: false,
}
},
methods: {
store() {
this.loading = true;
this.$store.dispatch('items', data);
},
},
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
There are a couple of ways you could do this.
Option 1:
Actions return promises, so make use of that.
this.loading = true;
this.$store.dispatch('items', data)
.then(() => {
this.loading = false;
})
I prefer this way of handling it because loading is defined and updated exactly where you use it.
Option 2:
Useful if you need to share the loading state among more than one component.
Put loading into the store's state and update it at the appropriate time in the items action. Then you can use loading in your component as $store.state.loading (or as a computed property).
I have a Vue.js SPA with some pages that display data from a backend. When I navigate the pages via the navbar, everything works fine, components and data are loaded.
When I'm on the page, e.g. localhost:8080/#/mypage and press F5, the data doesn't get loaded / rendered. Same goes for when I directly navigate to the page via the address bar.
The data gets loaded in this function:
async beforeMount() {
await this.initializeData();
}
I've tried to call the method in every lifecycle hook, i.e. created, beforeCreated, mounted etc...
In the mounted lifecycle hook I'm setting a boolean property to true, so that the table is only rendered when the component is loaded (done with v-if).
mounted() {
this.componentLoaded = true;
}
Not sure if this is important, but I've tried it with or without and it doesn't work.
I would really appreciate it if somebody knew whats happening here.
EDIT:
this.applications is a prop and contains multiple applications which contain instances. I want to add some variables from the backend to each application.
console.log(1) gets printed
console.log(2) does not
initializeData: function () {
let warn = 0;
console.log("1");
this.applications.forEach(async application => {
const instance = application.instances[0];
console.log("2");
let myData = null;
try {
const response = await instance.axios.get('url/myData');
myData = response.data;
} catch (err) {
}
let tmpCount = 0;
let tmpFulfilled = 0;
myData.forEach(ba => {
if(!ba.fulfilled){
warn++;
application.baAllFulfilled = false;
}else {
tmpFulfilled++;
}
tmpCount++;
})
console.log("3");
// Assign values
this.baTotalWarnings = warn;
application.baAnzahl = tmpCount;
application.baFulfilled = tmpFulfilled;
this.componentLoaded = true;
}
Try removing the async and await keywords from your beforeMount, and remove this.componentLoaded from mounted. Set it instead in the then block (or after await) in your initializeData method. I'm not sure Vue supports the async keyword in its lifecycle methods.
Something like this:
beforeMount() {
this.initializeData(); // start processing the method
}
methods: {
initializeData() {
callToBackend().then(() => {
this.componentLoaded = true // backend call ready, can now show the table
})
}
}
I am trying to load a function when images from a data api are finished loading. However, it looks like the function is run before the ApiService is finished and thus the TiffParser.replaceIMG() function is not working properly
Here's my setup:
data: function() {
return {
images: null,
imageLink: apiService.imgSrc,
loading: true,
errored: false
};
},
created: function() {
// fetch the data when the view is created and the data is
// already being observed
apiService
.getImages(this.$route.params.id)
.catch(error => {
console.log(error);
this.errored = true;
})
.then(response => {
this.loading = false;
this.images = response.data;
});
},
//vue js provides us `mounted()`. This means `onload` in javascript
mounted: function() {
TiffParser.replaceIMG();
}
Is mounted the correct lifecycle hook for this task?
You can create a watcher for your images.
created() {
const unwatch = this.$watch('images', function(newValue = [], oldValue = []) {
// any code here will execulte once the value of `images` changes
TiffParser.replaceIMG();
unwatch(); // remove the watcher
// Note that you cannot use ES6 arrow functions here, since arrow functions
// are bound to the parent context, and the `this` keyword
// would then not be bound correctly to the Vue instance.
});
// fetch images
}
Is mounted the correct lifecycle hook for this task?
Yes, if you need to access or modify the DOM of your component immediately before or after the initial render.
However, images would be empty when it's first mounted so using a watcher instead of the mounted hook seems more appropriate for this use case.
I'm trying to render my DOM, dependent on some data I'm returning from an axios get. I can't seem to get the timing right. The get is in the created hook, but there is a delay between the get and actually receiving the data. Basically if there is info in seller_id then I need to show the cancel button, otherwise don't. Here is my code:
this is in my created hook
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
and then this is the logic to show or hide the button. I've tried created, mounted, beforeUpdate, and updated all with no luck. I've also tried $nextTick but I can't get the timing correct. This is what I have currently:
beforeUpdate: function () {
// this.$nextTick(function () {
function sellerIdNotBlank() {
var valid = this.seller_id == '';
return !valid;
}
if(sellerIdNotBlank()){
this.show_cancel_button = true;
}
// })
},
First, it is pointless to get your data from backend and try to sync with Vue.js lifecycle methods. It never works.
Also, you should avoid beforeUpdate lifecycle event. It is often a code smell. beforeUpdate is to be used only when you have some DOM manipulations done manually and you need to adjust them again before Vue.js attempt to re-render.
Further, show_cancel_button is a very good candidate for a computed property. Here is how component will look:
const componentOpts = {
data() {
return {
seller_id: '',
// ... some more fields
};
},
created() {
axios.get('https://bc-ship.c9users.io/return_credentials').then(response => {
this.seller_id = response.data.seller_id;
this.selected_marketplace = response.data.marketplace;
this.token = response.data.auth_token;
});
},
computed: {
show_cancel_button() {
return this.seller_id !== '';
}
}
}