I have a top-level component that gets data from an API at regular intervals. I want to make a single API request and get all the data for my app in one place to reduce the number of requests to the API server. (FYI, my project looks like it's using Typescript but I'm not yet.)
Everything works fine in my top-level component:
//Parent
<script lang="ts">
import { defineComponent, ref, provide, inject, onMounted } from 'vue'
import getData from '#/data.ts'
export default defineComponent({
setup(){
const workspaces = ref([])
onMounted(async () => {
let api = inject('api') //global var from main.ts
let data = await getData(api) //API request inside data.ts
console.log(data.workspaces) //<-- data looks good here
workspaces.value = data.workspaces
//Trying to share workspaces with other components
provide('workspaces', data.workspaces)
})
return {
workspaces
}
}
})
</script>
<template>
{{ workspaces}} <!-- workspaces render fine here -->
</template>
But my child can't use the provide data via inject:
//Child
<script lang="ts">
import { defineComponent, inject, onMounted, ref } from 'vue'
export default defineComponent({
setup(){
let workspaces = ref([])
onMounted(async () => {
workspaces.value = await inject('workspaces') //<-- just a guess; doesn't work
})
return{
workspaces
}
}
})
</script>
<template>
{{ workspaces }} <!-- nothing here -->
</template>
I've made a couple assumptions as to the cause of the problem:
The Child component loads before the parent's async stuff is done, and is therefore empty.
I probably can't use project/inject in async scenarios like this.
So how can I share async data from an API across components in my app? Is my only option to go back to old-school props and pass the data down manually?
provide/inject are misused and subject to race conditions. Composition API is generally supposed to be at used on component initialization (setup, before any await) and not in onMounted. Even if there weren't such restriction, onMounted in parent component runs after the one in child component and can't provide a value at the time when a child is mounted.
The purpose of refs is to provide a reference to a value that can be changed later, so it could be passed by reference and stay reactive, this property isn't currently used.
It should be in parent component:
setup(){
const workspaces = ref([])
provide('workspaces', workspaces)
let api = inject('api')
onMounted(async () => {
let data = await getData(api)
workspaces.value = data.workspaces
})
return { workspaces }
In child component:
setup(){
let workspaces = inject('workspaces')
return { workspaces }
I want a way to run a function (which talks to the backend) whenever a component is re-displayed.
I understand that the mounted hook will fire if the component is re-added to the DOM by a v-if directive. But, if the component is hidden and re-shown via a v-show directive, this will not fire. I need to update the component regardless of what directive is in control of it's visibility.
I looked at the updated hook but this seems to not be the indented use case.
How do I run a function whenever a component is displayed (not only for the first time)?
updated fires whenever data passed to your component changes. Therefore it will work if you pass in whatever condition controls your v-show, as a prop.
Generic example:
Vue.config.devtools = false;
Vue.config.productionTip = false;
Vue.component('child', {
props: {
shown: {
type: Boolean,
default: true
}
},
template: '<div>{{shown}}</div>',
mounted() {
console.log('child mounted');
},
updated() {
// runs whenever any prop changes
// (optional condition) only run when component is shown
if (this.shown) {
console.log('child updated');
}
}
});
new Vue({
el: '#app',
data: () => ({
showChild: true
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<label><input type="checkbox" v-model="showChild" /> Show child</label>
<child v-show="showChild" :shown="showChild" />
</div>
Now updated hook works properly, because it fires everytime :shown changes its value, which maps precisely on your show/hide logic.
maybe you can achieve it in two ways
1.use :key
whenever you want to rerender your component whether it is shown, change the value of key can rerender it.
<template>
<h1 :key="key">Text</h1>
</template>
<script>
export default{
data(){
return {
key:this.getRandomString()
}
},
methods(){
getRandomString(length = 32) {
let chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678';
let max_pos = chars.length;
let random_string = '';
for (let i = 0; i < length; i++) {
random_string += chars.charAt(Math.floor(Math.random() * max_pos));
}
return random_string;
},
yourMethod(){
// communicate with backend
let data = await axios.get(...);
this.key = this.getRandomString();
}
}
}
</script>
use vm.$forceUpdate()
...
yourMethod(){
// communicate with backend
let data = await axios.get(...);
this.$forceUpdate();
}
...
you could implement this in a couple of ways. However since you would like to got the v-show way, here is how I would suggest you go about it.
v-show (v-show, watcher):
The v-show is definitely dependent on a variable (data, or computed). Create a watcher, to watch that data/computed property change. Depending on the value of the data/computed property, execute whatever function you intend to on the watcher.
I'm writing some tests using vue-test-util for a component I've made. I have boiled my code down to the actual problem.
The component is of the form:
<template>
<inner-component>
</template>
<script>
export default {
name: 'MyList'
}
</script>
and my inner component looks something like this:
<template>
<div v-if="open">Some stuff</div>
</template>
<script>
export default {
name: 'InnerComponent',
props: {
open: false,
}
}
</script>
Now the test I'm writing is testing for the existence of the div in the inner-component when the open prop is set to true, but it is set to false by default. I need a way to set the prop of this child component before I test it.
My test:
import { createLocalVue, mount } from '#vue/test-utils'
import MyList from '#/components/MyList.vue'
describe('My Test', () => {
const localVue = createLocalVue()
const wrapper = mount(MyList)
it('Tests', () => {
// need to set the prop here
expect(wrapper.find('div').exists()).toBeTruthy()
}
}
I can use:
wrapper.vm.$children[0].$options.propsData.open = true
Which does appear to set the prop, but my tests still come up as receiving false.
I can change the component so the default is true and then my tests pass so I don't think it's the way I'm checking.
If anyone can spot why this isn't working or knows a better way to come at it, please let me know!
According to the guide:
vm.$options
The instantiation options used for the current Vue instance.
So, $options is not what we write in props.
Use $props to set property for a child component:
wrapper.vm.$children[0].$props.open = true
But this way leads to the warning:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
So, let's follow the advice and bind property of the child component with data of the parent component. Here I bind it with isOpen variable:
<template>
<inner-component :open='isOpen'></inner-component>
</template>
<script>
import InnerComponent from '#/components/InnerComponent.vue'
export default {
name: 'MyList',
data() {
return {
isOpen: false
}
},
components:{InnerComponent}
}
</script>
Then in your test, you can just change the value of isOpen when you want to change the value of open property in the child component:
wrapper.setData({isOpen:true})
With the vue-test-util you can use the methods setProps from the wrapper, check the relative docs here
For example
const wrapper = mount(Foo)
wrapper.setProps({ foo: 'bar' })
Let's say I have a main Vue instance that has child components. Is there a way of calling a method belonging to one of these components from outside the Vue instance entirely?
Here is an example:
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
$('#external-button').click(function()
{
vm['my-component'].increaseCount(); // This doesn't work
});
<script src="http://vuejs.org/js/vue.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="app">
<my-component></my-component>
<br>
<button id="external-button">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 5px;">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
So when I click the internal button, the increaseCount() method is bound to its click event so it gets called. There is no way to bind the event to the external button, whose click event I am listening for with jQuery, so I'll need some other way to call increaseCount.
EDIT
It seems this works:
vm.$children[0].increaseCount();
However, this is not a good solution because I am referencing the component by its index in the children array, and with many components this is unlikely to stay constant and the code is less readable.
In the end I opted for using Vue's ref directive. This allows a component to be referenced from the parent for direct access.
E.g.
Have a component registered on my parent instance:
var vm = new Vue({
el: '#app',
components: { 'my-component': myComponent }
});
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Now, elsewhere I can access the component externally
<script>
vm.$refs.foo.doSomething(); //assuming my component has a doSomething() method
</script>
See this fiddle for an example: https://jsfiddle.net/0zefx8o6/
(old example using Vue 1: https://jsfiddle.net/6v7y6msr/)
Edit for Vue3 - Composition API
The child-component has to return the function in setup you want to use in the parent-component otherwise the function is not available to the parent.
Note: <sript setup> doc is not affacted, because it provides all the functions and variables to the template by default.
You can set ref for child components then in parent can call via $refs:
Add ref to child component:
<my-component ref="childref"></my-component>
Add click event to parent:
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
var vm = new Vue({
el: '#app',
components: {
'my-component': {
template: '#my-template',
data: function() {
return {
count: 1,
};
},
methods: {
increaseCount: function() {
this.count++;
}
}
},
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<my-component ref="childref"></my-component>
<button id="external-button" #click="$refs.childref.increaseCount()">External Button</button>
</div>
<template id="my-template">
<div style="border: 1px solid; padding: 2px;" ref="childref">
<p>A counter: {{ count }}</p>
<button #click="increaseCount">Internal Button</button>
</div>
</template>
For Vue2 this applies:
var bus = new Vue()
// in component A's method
bus.$emit('id-selected', 1)
// in component B's created hook
bus.$on('id-selected', function (id) {
// ...
})
See here for the Vue docs.
And here is more detail on how to set up this event bus exactly.
If you'd like more info on when to use properties, events and/ or centralized state management see this article.
See below comment of Thomas regarding Vue 3.
You can use Vue event system
vm.$broadcast('event-name', args)
and
vm.$on('event-name', function())
Here is the fiddle:
http://jsfiddle.net/hfalucas/wc1gg5v4/59/
A slightly different (simpler) version of the accepted answer:
Have a component registered on the parent instance:
export default {
components: { 'my-component': myComponent }
}
Render the component in template/html with a reference:
<my-component ref="foo"></my-component>
Access the component method:
<script>
this.$refs.foo.doSomething();
</script>
Say you have a child_method() in the child component:
export default {
methods: {
child_method () {
console.log('I got clicked')
}
}
}
Now you want to execute the child_method from parent component:
<template>
<div>
<button #click="exec">Execute child component</button>
<child-cmp ref="child"></child_cmp> <!-- note the ref="child" here -->
</div>
</template>
export default {
methods: {
exec () { //accessing the child component instance through $refs
this.$refs.child.child_method() //execute the method belongs to the child component
}
}
}
If you want to execute a parent component method from child component:
this.$parent.name_of_method()
NOTE: It is not recommended to access the child and parent component like this.
Instead as best practice use Props & Events for parent-child communication.
If you want communication between components surely use vuex or event bus
Please read this very helpful article
This is a simple way to access a component's methods from other component
// This is external shared (reusable) component, so you can call its methods from other components
export default {
name: 'SharedBase',
methods: {
fetchLocalData: function(module, page){
// .....fetches some data
return { jsonData }
}
}
}
// This is your component where you can call SharedBased component's method(s)
import SharedBase from '[your path to component]';
var sections = [];
export default {
name: 'History',
created: function(){
this.sections = SharedBase.methods['fetchLocalData']('intro', 'history');
}
}
Using Vue 3:
const app = createApp({})
// register an options object
app.component('my-component', {
/* ... */
})
....
// retrieve a registered component
const MyComponent = app.component('my-component')
MyComponent.methods.greet();
https://v3.vuejs.org/api/application-api.html#component
Here is a simple one
this.$children[indexOfComponent].childsMethodName();
I am not sure is it the right way but this one works for me.
First import the component which contains the method you want to call in your component
import myComponent from './MyComponent'
and then call any method of MyCompenent
myComponent.methods.doSomething()
Declare your function in a component like this:
export default {
mounted () {
this.$root.$on('component1', () => {
// do your logic here :D
});
}
};
and call it from any page like this:
this.$root.$emit("component1");
If you're using Vue 3 with <script setup> sugar, note that internal bindings of a component are closed (not visible from outside the component) and you must use defineExpose(see docs) to make them visible from outside. Something like this:
<script setup lang="ts">
const method1 = () => { ... };
const method2 = () => { ... };
defineExpose({
method1,
method2,
});
</script>
Since
Components using are closed by default
Sometimes you want to keep these things contained within your component. Depending on DOM state (the elements you're listening on must exist in DOM when your Vue component is instantiated), you can listen to events on elements outside of your component from within your Vue component. Let's say there is an element outside of your component, and when the user clicks it, you want your component to respond.
In html you have:
Launch the component
...
<my-component></my-component>
In your Vue component:
methods() {
doSomething() {
// do something
}
},
created() {
document.getElementById('outsideLink').addEventListener('click', evt =>
{
this.doSomething();
});
}
I have used a very simple solution. I have included a HTML element, that calls the method, in my Vue Component that I select, using Vanilla JS, and I trigger click!
In the Vue Component, I have included something like the following:
<span data-id="btnReload" #click="fetchTaskList()"><i class="fa fa-refresh"></i></span>
That I use using Vanilla JS:
const btnReload = document.querySelector('[data-id="btnReload"]');
btnReload.click();
I'm working on a project using vue vuex and webpack. I've got a Vue instance and imported a vue component and a vuex store. component and store are all registered to Vue instance. I was using axios made a async post request in the component.. After I got the result but I couldn't manipulate the store and i can't get the Vue instance or component... how could i do with it? plz?
app.js
import indexPage from './vue/index.vue'
const store = new Vuex.Store(....not relavent.....)
const router = new VueRouter({routes:[{path:'/', component:indexPage}]})
const app = new Vue({
el:"#app",
router,
store
})
index.vue
<template>not relavent</template>
<script>
export default {
data(){return{}},
methods:{
dopost: function(){
axios.post('/api',{}).then(){
// apparently "this" wouldn't workhere
// I've tried give this module a name and just use it
// but it is just an object, not a instance.
// And I couldn't use the vue instance
}
}
}
</script>
Is there any way that i don't need to change the async request to the sync request?
Thanks a lot
All the methods of your component should go in it's methods property.
<script>
export default {
data(){
return {}
},
methods: {
postTo() {
const self = this; // assigning this to self
axios.post('/api',{}).then(){
self.$store.commit('SOME_MUTATION_TYPE')
}
}
}
}
</script>