I need to call below method in created(). For this purpose, I need to make created() as async. Per Vue documentation, created() is called synchronously. Will Vue Framework await on created() to avoid any race conditions?
this.isAuthenticated = await authService.isAuthenticated();
Vue.config.productionTip = false;
function tm(ms, msg) {
return new Promise(resolve => {
setTimeout(() => {
resolve(msg);
}, ms);
});
}
new Vue({
async beforeCreate() {
console.log(await tm(1000, "BEFORE CREATE"));
},
async created() {
console.log(await tm(2000, "CREATED"));
},
async beforeMount() {
console.log(await tm(3000, "BEFORE MOUNT"));
},
async mounted() {
console.log(await tm(4000, "MOUNTED"));
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
If you really need to wait until your asynchronous function is done. You can basically await it before you create the Vue instance. This may not be always useable, but in a case like in this question it is, and it is more solid than putting an asynchronous lifecycle hook that isn't awaited.
Vue.config.productionTip = false;
// mock service
const authService = {
isAuthenticated: () => new Promise((r) => setTimeout(() => r(true), 2000))
};
// async iife to await the promise before creating the instance
(async() => {
const isAuthenticated = await authService.isAuthenticated();
new Vue({
data: {
isAuthenticated
},
created() {
console.log('authenticaed:', this.isAuthenticated);
},
})
})().catch(console.warn);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
Related
I am working on a project built on Vue3 and composition API and writing test cases.
The component I want to test is like below.
Home.vue
<template>
<div>
<Child #onChangeValue="onChangeValue" />
</div>
</template>
<script lang="ts>
...
const onChangeValue = (value: string) => {
store.dispatch("changeValueAction", {
value: value,
});
};
</scirpt>
Now I want to test if changeValueAction has been called.
Home.spec.ts
...
import { key, store } from '#/store';
describe("Test Home component", () => {
const wrapper = mount(Home, {
global: {
plugins: [[store, key]],
},
});
it("Test onChangeValue", () => {
const child = wrapper.findComponent(Child);
child.vm.$emit("onChangeValue", "Hello, world");
// I want to check changeValueAction has been called.
expect(wrapper.vm.store.state.moduleA.value).toBe("Hello, world");
});
});
I can confirm the state has actually been updated successfully in the test case above but I am wondering how I can mock action and check if it has been called.
How can I do it?
I have sort of a similar setup.
I don't want to test the actual store just that the method within the component is calling dispatch with a certain value.
This is what I've done.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}
Hope this will help you further :)
let fetchData = async function() {
return $.get("https://jsonplaceholder.typicode.com/todos/1");
}
Vue.use(VueRouter);
const cComponent = {
data() {
return {
fetchedData: null
}
},
template: `<div>{{$route.params.id+': '}}{{fetchedData}}</div>`,
async beforeRouteEnter(to, from, next) {
let data = await fetchData();
next(vm => vm.fetchedData = data);
},
async beforeRouteUpdate(to, from, next) {
console.log("beforeRouteUpdate");
let data = await fetchData();
this.fetchedData = data;
next();
}
}
const routes = [{
path: "/path/:id",
component: cComponent,
}]
const router = new VueRouter({
mode: "history",
routes,
})
const app = new Vue({
el: "#app",
router,
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src=" https://unpkg.com/vue-router#3.5.1/dist/vue-router.js"></script>
<div id="app">
<router-link to="/path/1">An URL</router-link>
<router-link to="/path/2">Another one</router-link>
<router-link to="/path/3">And another one</router-link>
<router-view :key="$route.path"></router-view>
</div>
This is the fetch function that retrieves data from a server:
let fetchPage = async (route) => {
return $.get('route');
}
This is my beforeRouteUpdate navigation guard, which fetchedData property of the Vue instance (declared in data option beforehand) does not changes.
async beforeRouteUpdate(to, from, next) {
this.fetchedData = await fetchPage(to.path);
//fetchData is not changed while changing route param
next();
}
The next() callback is activated at the end (URL does change), but this.fetchedData shows null in Vue DevTools (initializing value).
On the other hand, if I do the same thing with async/await in beforeRouteEnter hook, everything works perfectly.
async beforeRouteEnter(to, from, next) {
let data = await fetchPage(to.path);
next(app => app.fetchedData = data); //fetchedData has fetched data as supposed.
},
I tried replacing this with app just like in beforeRouteEnter hook. But there's still no result.
What am I missing? Is this the best practice with guard hooks and how should I use them better? (ignore Exception handling part)
Updated: SPA experience for code snippet on StackOverflow is kinda not perfect so you better try using my provided JS (here). It has the same problem but I simplified it.
Versions
Vue 2.6
VueRouter 3.5
I'm trying to implement unit testing with Jest for a Vue component which on created() lifecycle calls a function which has axios who runs async.
The thing is, the created is being triggered, but the value of the object that sits in the component data section won't show it's new value. (remains undefined, tested also when initialized with some other value then undefined)
I've used many solutions that was offered around here but nothing was 100% accurate to the situation I'm dealing with.
Products component:
data() {
return {
products: undefined,
countries: undefined,
fields: undefined,
selectedItem: undefined,
alert: undefined,
};
},
methods: {
async getProducts() {
try {
let res = await axios.get("http://localhost:5000");
this.products = res.data.products;
this.countries = res.data.countries;
this.fields = Object.keys(this.products[0]).splice(1);
this.fields.push({ key: "Edit", label: "Click & Edit" });
}
catch (error) {
this.toggleAlert(true, "Failed to retrieve data from server");
console.log(error);
}
},
}
created() {
this.getProducts();
}
testfile:
// create an extended `Vue` constructor
const localVue = createLocalVue();
// install plugins as normal
localVue.use(BootstrapVue);
describe("Testing Products component:", () => {
test("checks the incoming data from api", async () => {
const wrapper = shallowMount(Products, { localVue });
await flushPromises();
console.log(wrapper.vm.$data.products, "products"); //prints undefined instead of an array
expect(wrapper.vm.$data.products).not.toBe(undefined);
});
});
I want to make several API calls to get data into a component. I created a PostService.ts that looks like this:
const apiClient = axios.create({
baseURL: '/api/v1',
})
export default {
async getPosts() {
const { data }: { data: Post[] } = await apiClient.get('/posts')
// transform data ...
return data
},
async getTags() {
const { data }: { data: Tag[] } = await apiClient.get('/tags')
return data
},
async getComments() {
const { data }: { data: Comment[] } = await apiClient.get('/comments')
return data
},
}
This is my posts.vue:
<template>
<div>
<div v-if="dataLoaded">
content
</div>
<div v-else>
loading...
</div>
</div>
</template>
<script>
finishedApiCalls = 0
get dataLoaded() {
return this.finishedApiCalls === 3
}
created() {
PostService.getPosts()
.then((posts) => {
this.posts = posts
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
PostService.getTags()
.then((tags) => {
this.tags = tags
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
PostService.getComments()
.then((comments) => {
this.comments = comments
this.finishedApiCalls++
})
.catch((error) => {
console.log('There was an error:', error)
})
}
</script>
The key point is that I want to display a loading spinner as long as the data has not been loaded. Is it recommended to make the API calls from created()? What would be a more elegant way to find out when all calls are finished? It does not feel right to use the finishedApiCalls variable.
I recommend using Nuxt's fetch method along with Promise.all() on all your async PostService fetches:
// MyComponent.vue
export default {
fetch() {
return Promise.all([
PostService.getPosts().then((posts) => ...).catch((error) => ...),
PostService.getTags().then((tags) => ...).catch((error) => ...),
PostService.getComments().then((comments) => ...).catch((error) => ...)
])
}
}
Nuxt provides a $fetchState.pending prop that you could use for conditionally rendering a loader:
<template>
<div>
<Loading v-if="$fetchState.pending" />
<div v-else>My component data<div>
</div>
</template>
You can use Promise.all for this kind of requirements.
this.loading = true
Promise.all([PostService.getPosts(), PostService.getTags(), PostService.getComments()])
.then(values => {
let [posts, tags, comments] = values
this.posts = posts
this.tags = tags
this.comments = comments
//Here you can toggle your fetching flag like below
this.loading = false
})
You can use Promise.all(). This will wait till all resolves or if 1 fails.
With async / await you can make it "synchronous"
data() {
return {
loaded: false
}
},
async created() {
let [posts, tags, comments] = await Promise.all([PostService.getPosts(), PostService.getTags(), PostService.getComments()])
this.posts = posts;
this.tags = tags;
this.comments = comments;
this.loaded = true;
}
methods: {
async create () {
this.disableSubmit = true;
await this.$firestore
.collection('collectionName')
.add(this.item)
.then(() => {
this.$refs.createForm.reset();
this.$notify('positive', 'Item successfully created!');
})
.catch(error => {
this.$notify('negative', 'ERROR! Try again later!', error);
});
this.disableSubmit = false;
},
}
If I use the code above inside the methods property, then everything works fine, but I would like to access that ref from outside the Vue component, for example a plugin, but it gives me an error.
TypeError: "_this.$refs is undefined"
Even when I just import it as a function, the error is the same, so I would like to know how to access the ref outside vue?
Bellow is the code for my plugin, and I would also like to point that I am using the quasar framework.
export let plugin = {
install (Vue, options) {
Vue.prototype.$plugin = async (collection, item) => {
return await firestore
.collection(collection)
.add(item)
.then(() => {
this.$refs.createFrom.reset();
notify('positive', 'Booking successfully created!');
})
.catch(error => {
notify('negative', 'ERROR creating booking! Try again later!', error);
});
};
}
};
I hope my question makes sense, and thanks in advance for any help
you could pass the context of your component, to apply the reset form from your plugin:
// plugin declaration
Vue.prototype.$plugin = async (collection, item, ctx) {
...
ctx.$refs.createFrom.reset()
...
}
then when u call to your plugin from yours components can do it like this:
// your component
methods: {
myFunction () {
this.$plugin(collection, item, this)
}
}
this is the reference of the context of your current component that will be used inside of your plugin
for example:
Vue.component('my-form', {
methods: {
resetForm() {
console.log('the form has been reset')
}
}
})
Vue.prototype.$plugin = (item, ctx) => {
console.log('item passed:', item)
ctx.$refs.refToMyForm.resetForm()
}
new Vue({
el: '#app',
data: {
item: 'foo'
},
methods: {
submit() {
this.$plugin(this.item, this)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<my-form ref="refToMyForm"></my-form>
<button #click="submit">submit</button>
</div>