###The Problem
I have a child component that may or may not exist on the page with a v-if. Trying to keep it cached when the user has clicked other things so that search terms and whatnot show up again when the user returns fails no matter how I try to cache with <keep-alive>.
###What I've tried
The documentation seems to indicate that all I need to do is wrap my component in a <keep-alive> tag and things should just work. I tried matching some documentation that uses the <component> tag which clearly doesn't work. I also tried using the include prop since just wrapping it doesn't work.
Vue.component('child', {
template: '<div>child: {{text}}<div>',
data() {return {text: ""}},
created(){
this.$nextTick(() => {
this.text = `${Math.round(Math.random() * 100)}`
})
},
activated: function() {
this.$nextTick(() => {
console.log('in activated');
});
}
})
var vm = new Vue({
el: '#app',
data: function() {
return {
showNow:false,
message: 'This is a test.'
}
},
methods: {
changeText: function() {
this.message = 'changed';
},
toggle() {
this.showNow = !this.showNow
}
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<keep-alive include="child">
<div v-if="showNow">
<h4>Title of section to be toggled</h4>
<component is="child"></component>
</div>
</keep-alive>
<button #click="toggle()">toggle child</button>
</div>
I've also tried not using include and just wrapping like so <keep-alive><child></child></keep-alive> or using the <component> syntax and neither of those work either.
If you remove the conditional rendering, the activated hook is called, but that obviously defeats the purpose of <keep-alive>! As it stands right now, that hook is never called, which is frustrating.
Also, the example in the documentation doesn't help either because they're not using v-if, just changing which components are rendered via a string....
P.S. I have no idea how to tag this other than Vue.js
Here is a lightly modified version of your code with keep-alive working correctly:
Vue.component('child', {
template: '<div>child: {{text}}</div>',
data() {return {text: ""}},
created(){
console.log('in created')
this.$nextTick(() => {
this.text = `${Math.round(Math.random() * 100)}`
})
},
activated: function() {
this.$nextTick(() => {
console.log('in activated');
});
}
})
var vm = new Vue({
el: '#app',
data: function() {
return {
showNow:false,
message: 'This is a test.'
}
},
methods: {
changeText: function() {
this.message = 'changed';
},
toggle() {
this.showNow = !this.showNow
}
},
});
<script src="https://unpkg.com/vue#2.6.11/dist/vue.js"></script>
<div id="app">
<keep-alive>
<child v-if="showNow"></child>
</keep-alive>
<button #click="toggle()">toggle child</button>
</div>
The created hook is only called the first time. On subsequent activation the activated hook is called but not created.
The key change is that the <child> component must be a direct child of <keep-alive>. This direct child must also be the component that has the v-if.
You can't use a <div> for this purpose as <keep-alive> specifically looks for a component, not just an element. See:
https://github.com/vuejs/vue/blob/ec78fc8b6d03e59da669be1adf4b4b5abf670a34/src/core/components/keep-alive.js#L85
Another common mistake with keep-alive is to put a v-if on the keep-alive component itself, or on one of its ancestors. This won't work as the keep-alive component itself will be destroyed. The keep-alive component maintains a cache of child components but if the keep-alive is itself destroyed then that cache is lost.
Related
I just met one issue, if one component only update its own data, it will not trigger the hook=componentUpdated of the directive at the parent component.
As Vue official Guide said:
componentUpdated: called after the containing component’s VNode and
the VNodes of its children have updated.
It seems componentUpdated should be triggered.
Did I do something wrong? or misunderstand something?
At below demo, hit Click Me! button then you will see componentUpdated is not called.
But when click change data (execute similar behavior with click me!, the difference is it changes the data at parent component), it will trigger correctly.
Many thanks for any.
Vue.config.productionTip = false
Vue.component('child', {
template: `<div>{{point}}
<span style="background-color:gray;font-weight:bold;color:red">
-{{mytest}}
</span>
<button #click="plusOne()">Click me!</button>
</div>`,
props: ['point'],
data(){
return {
mytest: 1
}
},
updated: function () {
console.log('updated component=child')
},
methods: {
plusOne() {
this.mytest += 1
}
}
})
let vMyDirective = {}
vMyDirective.install = function install (Vue) {
Vue.directive('my-directive', {
inserted: function () {
console.log('!!!directive for inserted')
},
bind: function bind (el, binding, vnode) {
console.log('!!!directive for bind')
},
componentUpdated: function componentUpdated (el, binding, vnode) {
console.log('!!!directive for component updated')
},
update: function () {
console.log('!!!directive for update')
}
})
}
Vue.use(vMyDirective)
new Vue({
el: '#app',
data() {
return {
testValues: ['label a', 'label b'],
testIndex: 1
}
},
methods:{
pushArray: function() {
this.testValues.push('label c')
},
changeData: function () {
this.testIndex += 1
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button v-on:click="pushArray()">Add one Child!!!</button>
<button v-on:click="changeData()">Change Data - {{testIndex}}</button>
<div v-my-directive>
<child v-for="(item, index) in testValues" :key="index" :point="item"></child>
</div>
</div>
Based on Vue Team Feedback, it is not one issue on the hook=componentUpdated, it is my misunderstanding on the words.
For the prerequisite of the hook=comopnentUpdated is triggered, it is the VNode which the directive binds to already changed. That means if only child VNode changes, Vue will not catch it probably like what #Jacob Goh said in the comments (only flows one way).
So componentUpdated doesn't means it will detect child components are updated or not, it only means when will be triggered.
After reading the vue.js docs I just jumped into components.
I want to create a custom (local) input component that emits an event to the parent on keyup, but I have two problems. (see code example at the end of the post)
[solved] 1. I already get an error when I register the child component that says
[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <InputTest>
<Root>
I guess it's a complete no-brainer, but I just don't get it.
[solved] 2. The child event doesn't even fire
Before abstracting and simplyfing the code for this question I tried to create the same behaviour with single-file (.vue) components. With SFCs the template compiles / mounts successfully, but the child component events doesn't fire. Obviously I can not tell for sure if this problem will occur in my provided example as well, but I'd guess so.
EDIT 1: Solved problem 1
My child-component should be an object instead of a vue instance. I updated the code for that. I also changed the onChange method from lambda to function, as this doesn't point to the vue instance in a lambda.
EDIT 2: Solved problem 2
There may be times when you want to listen for a native event on the root element of a component.
Apparently the native modifier can only be used on components and not on native elements. Removing the modifier fixed the problem. I changed the code accordingly.
CODE
const inputText = {
data () {
return {
model: ''
}
},
template: '<input type="text" v-model="model" #keyup="onChange">',
methods: {
onChange: function () {
this.$emit('update', this.model);
}
}
};
const app = new Vue({
el: '#app',
data () {
return {
txt: ''
}
},
methods: {
onUpdate: function(txt) {
this.txt = txt;
}
},
components: {
'input-text': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<input-text #update="onUpdate"></input-text><br>
{{ txt }}
</div>
You don't need two vue instances. You can create a component as a simple object and use it in your vue instance
const inputText = {
template: '<div> <input type="text" #keyup.native="onChange"> </div>',
methods: {
onChange: () => {
console.log('onChange');
this.$emit('update')
}
}
}
const app = new Vue({
el: '#app',
template: '<input-test #keyup.native="onKeyup" #update="onUpdate"></input-test>',
methods: {
onUpdate: () => console.log('onUpdate'),
onKeyup: () => console.log('onKeyup')
},
components: {
'input-test': inputText
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app"></div>
Context
In Vue 2.0 the documentation and others clearly indicate that communication from parent to child happens via props.
Question
How does a parent tell its child an event has happened via props?
Should I just watch a prop called event? That doesn't feel right, nor do alternatives ($emit/$on is for child to parent, and a hub model is for distant elements).
Example
I have a parent container and it needs to tell its child container that it's okay to engage certain actions on an API. I need to be able to trigger functions.
Vue 3 Composition API
Create a ref for the child component, assign it in the template, and use the <ref>.value to call the child component directly.
<script setup>
import {ref} from 'vue';
const childComponentRef = ref(null);
function click() {
// `childComponentRef.value` accesses the component instance
childComponentRef.value.doSomething(2.0);
}
</script>
<template>
<div>
<child-component ref="childComponentRef" />
<button #click="click">Click me</button>
</div>
</template>
Couple things to note-
If your child component is using <script setup>, you'll need to declare public methods (e.g. doSomething above) using defineExpose.
If you're using Typescript, details of how to type annotate this are here.
Vue 3 Options API / Vue 2
Give the child component a ref and use $refs to call a method on the child component directly.
html:
<div id="app">
<child-component ref="childComponent"></child-component>
<button #click="click">Click</button>
</div>
javascript:
var ChildComponent = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue: function(value) {
this.value = value;
}
}
}
new Vue({
el: '#app',
components: {
'child-component': ChildComponent
},
methods: {
click: function() {
this.$refs.childComponent.setValue(2.0);
}
}
})
For more info, see Vue 3 docs on component refs or Vue 2 documentation on refs.
What you are describing is a change of state in the parent. You pass that to the child via a prop. As you suggested, you would watch that prop. When the child takes action, it notifies the parent via an emit, and the parent might then change the state again.
var Child = {
template: '<div>{{counter}}</div>',
props: ['canI'],
data: function () {
return {
counter: 0
};
},
watch: {
canI: function () {
if (this.canI) {
++this.counter;
this.$emit('increment');
}
}
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
data: {
childState: false
},
methods: {
permitChild: function () {
this.childState = true;
},
lockChild: function () {
this.childState = false;
}
}
})
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.1/vue.js"></script>
<div id="app">
<my-component :can-I="childState" v-on:increment="lockChild"></my-component>
<button #click="permitChild">Go</button>
</div>
If you truly want to pass events to a child, you can do that by creating a bus (which is just a Vue instance) and passing it to the child as a prop.
You can use $emit and $on. Using #RoyJ code:
html:
<div id="app">
<my-component></my-component>
<button #click="click">Click</button>
</div>
javascript:
var Child = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue: function(value) {
this.value = value;
}
},
created: function() {
this.$parent.$on('update', this.setValue);
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
methods: {
click: function() {
this.$emit('update', 7);
}
}
})
Running example: https://jsfiddle.net/rjurado/m2spy60r/1/
A simple decoupled way to call methods on child components is by emitting a handler from the child and then invoking it from parent.
var Child = {
template: '<div>{{value}}</div>',
data: function () {
return {
value: 0
};
},
methods: {
setValue(value) {
this.value = value;
}
},
created() {
this.$emit('handler', this.setValue);
}
}
new Vue({
el: '#app',
components: {
'my-component': Child
},
methods: {
setValueHandler(fn) {
this.setter = fn
},
click() {
this.setter(70)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id="app">
<my-component #handler="setValueHandler"></my-component>
<button #click="click">Click</button>
</div>
The parent keeps track of the child handler functions and calls whenever necessary.
Did not like the event-bus approach using $on bindings in the child during create. Why? Subsequent create calls (I'm using vue-router) bind the message handler more than once--leading to multiple responses per message.
The orthodox solution of passing props down from parent to child and putting a property watcher in the child worked a little better. Only problem being that the child can only act on a value transition. Passing the same message multiple times needs some kind of bookkeeping to force a transition so the child can pick up the change.
I've found that if I wrap the message in an array, it will always trigger the child watcher--even if the value remains the same.
Parent:
{
data: function() {
msgChild: null,
},
methods: {
mMessageDoIt: function() {
this.msgChild = ['doIt'];
}
}
...
}
Child:
{
props: ['msgChild'],
watch: {
'msgChild': function(arMsg) {
console.log(arMsg[0]);
}
}
}
HTML:
<parent>
<child v-bind="{ 'msgChild': msgChild }"></child>
</parent>
The below example is self explainatory. where refs and events can be used to call function from and to parent and child.
// PARENT
<template>
<parent>
<child
#onChange="childCallBack"
ref="childRef"
:data="moduleData"
/>
<button #click="callChild">Call Method in child</button>
</parent>
</template>
<script>
export default {
methods: {
callChild() {
this.$refs.childRef.childMethod('Hi from parent');
},
childCallBack(message) {
console.log('message from child', message);
}
}
};
</script>
// CHILD
<template>
<child>
<button #click="callParent">Call Parent</button>
</child>
</template>
<script>
export default {
methods: {
callParent() {
this.$emit('onChange', 'hi from child');
},
childMethod(message) {
console.log('message from parent', message);
}
}
}
</script>
If you have time, use Vuex store for watching variables (aka state) or trigger (aka dispatch) an action directly.
Calling child component in parent
<component :is="my_component" ref="my_comp"></component>
<v-btn #click="$refs.my_comp.alertme"></v-btn>
in Child component
mycomp.vue
methods:{
alertme(){
alert("alert")
}
}
I think we should to have a consideration about the necessity of parent to use the child’s methods.In fact,parents needn’t to concern the method of child,but can treat the child component as a FSA(finite state machine).Parents component to control the state of child component.So the solution to watch the status change or just use the compute function is enough
you can use key to reload child component using key
<component :is="child1" :filter="filter" :key="componentKey"></component>
If you want to reload component with new filter, if button click filter the child component
reloadData() {
this.filter = ['filter1','filter2']
this.componentKey += 1;
},
and use the filter to trigger the function
You can simulate sending event to child by toggling a boolean prop in parent.
Parent code :
...
<child :event="event">
...
export default {
data() {
event: false
},
methods: {
simulateEmitEventToChild() {
this.event = !this.event;
},
handleExample() {
this.simulateEmitEventToChild();
}
}
}
Child code :
export default {
props: {
event: {
type: Boolean
}
},
watch: {
event: function(value) {
console.log("parent event");
}
}
}
I used vue-loader to help me install vue and webpack
I have a file called App.vue
In App.vue I added a component called widget. If I clicked some button there's a function that set the btnClicked = true hence the widget appears
<widget v-show="btnClicked"></widget>
but I also want that function to access the widgetShowMe, it's a property in my component.
I want the function activated in my App.vue to also set widgetShowMe = true
I tried this but it didn't work
methods:{
btnClickedFunc () {
this.btnClicked = true;
Widget.widgetShowMe = true;
}
}
Accessing child component's data in parent component in vuejs
If you have a parent component called parent and child component called child, you can communicate between each other using props and events.
props: Facilitates communication from parent to child.
events: Can be used to pass data in a child component to the parent component.
For this question we require events and will use v-model to make the child component usable everywhere with much less setup.
Vue.component('counter', {
template: `<div><button #click='add'>+1</button>
<button #click='sub'>-1</button>
<div>this is inside the child component: {{ result }}</div></div>`,
data () {
return {
result: 0
}
},
props: ['value'],
methods: {
emitResult () {
this.$emit('input', this.result)
},
add () {
this.result += 1
this.emitResult()
},
sub () {
this.result -= 1
this.emitResult()
}
}
})
new Vue({
el: '#demo',
data () {
return {
resultFromChild: null
}
}
})
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id='demo'>
<counter v-model='resultFromChild'></counter>
This is in parent component {{ resultFromChild }}
</div>
Custom component with v-model
This needs two requirements.
You have a prop on the child component with the name value.
props: ['value'], // this part in the child component snippet
You emit the event input with the value.
this.$emit('input', this.result) // this part in the child component snippet
All you need to think of is, when to emit the event with the value of widgetShowMe, and your app.vue can easily capture the value inside your widget.
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();