Prop value isn't updating in child component Vue.js - javascript

I'm adding a way for the parent App.vue tell all child components to close every modal you have open and for any given child to tell the parent to hide the overlay if someone clicks on the main app div. So for example if the services page has a modal open to view some details, an overlay will be active that is set in the parent App.vue component, and if I click anywhere outside of the modal, the modal will close and the child will tell the parent to close the overlay. I have it setup this way so that it's something globally available and not isolated to a single component.
<template>
<div id="app" v-cloak :class="{'overlay-layer' : overlay.active}" v-on:click="close($event)">
<Header/>
<transition>
<router-view v-on:overlay="overlay.active = $event" :closeAllModals="overlay.close"/>
</transition>
<Footer/>
</div>
</template>
<script>
import Header from '#/components/header/Header.vue';
import Footer from '#/components/footer/Footer.vue';
export default {
components: {
Header,
Footer
},
props: {
closeAllModals: Boolean
},
data: function() {
return {
overlay: { active: false, close: false }
};
},
methods: {
close(e) {
if (this.overlay.active && e.toElement.id === 'app') {
this.overlay = {
active: false,
close: true
}
}
}
}
};
</script>
The problem is that this only executes once, so for example in the services page I have:
import Header from '#/components/header/Header.vue';
import Footer from '#/components/footer/Footer.vue';
export default {
name: 'services',
components: {
Header,
Footer
},
props: {
closeAllModals: Boolean
},
data: function() {
return {
overlay: false,
activeMember: Object,
};
},
methods: {
setActiveMember(member, open) {
this.activeMember = member;
if (open) {
this.activeMember.active = true;
this.overlay = true;
} else {
this.activeMember.active = false;
this.overlay = false;
}
this.$emit('overlay', this.overlay);
this.$forceUpdate();
},
watch: {
closeAllModals: function() {
this.activeMember.active = false;
this.overlay = false;
this.$forceUpdate();
console.log('runs once');
}
}
};
So this works, but only works the first time. The prop only sends the updated value to the child only once. I've tried watching the prop in the child and using forceUpdate too but it isn't working. How do I make this run every single time?

A global event bus could help when components have to talk to each other across different parent/child levels. Below is a good write up on that:
https://alligator.io/vuejs/global-event-bus
Note: but be sure to use it only when necessary and remove the listeners in beforeDestroy life cycle of the component

Related

Sibling component communication not working in vue

I am trying to send this.TC from typing.js to ending-page.js which are sibling components. Emits and event hubs not working. But emit from typing.js to parent works as I want. (There will be only one more call in this app, so i don't want use Vuex if it isnt necessary for this - i want to do it with simple emits ) Here's my code:
Parent:
<template>
<div id = "app">
<typing v-if = "DynamicComponent === 'typing'" />
<ending_page v-else-if = "DynamicComponent === 'ending_page'" />
</div>
</template>
<script>
/* Importing siblings components to parent component */
import typing from './components/typing/index.vue'
import ending_page from './components/ending-page/index.vue'
export default {
name: 'app',
components: {
typing,
ending_page
},
data() {
return {
DynamicComponent: "typing",
};
},
methods: {
updateDynamicComponent: function(evt, data){
this.DynamicComponent = evt;
},
},
};
</script>
typing.js:
import { eventBus } from "../../main";
export default {
name: 'app',
components: {
},
data() {
return {
/* Text what is in input. If you write this.input = "sometext" input text will change (It just works from JS to HTML and from HTML to JS) */
input: "",
/* Object of TypingCore.js */
TC: "somedata",
/* Timer obejct */
timer: null,
is_started: false,
style_preferences: null,
};
},
ICallThisFunctionWhenIWantToEmitSomething: function(evt) {
/* Sending data to ending_page component */
this.$root.$emit('eventname', 'somedata');
/* Calling parent to ChangeDynamicComponent && sending TC.data what will be given to ending_page (I think it looks better with one syntax here) */
this.$emit('myEvent', 'ending_page', this.TC.data);
}
},
};
ending-page.js:
import { eventBus } from "../../main";
export default {
name: 'ending-page',
components: {},
data () {
return {
data: "nothing",
}
},
computed: {
},
props: {
},
methods: {
},
/* I know arrow functions etc but i was trying everyting */
created: function () {
this.$root.$on('eventname', function (data) {
console.log(data)
this.title = data
this.$nextTick()
})
}
}
It is an example of how to share data between siblings components.
Children components emits events to parent. Parent components send data to children.
So, the parent has the property title shared between the children. When typing emits
the input event the directive v-modelcapture it an set the value on parent.
Ref:
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
https://v2.vuejs.org/v2/guide/components.html#Using-v-model-on-Components
https://benjaminlistwon.com/blog/data-flow-in-vue-and-vuex/
Vue.component('typing', {
props: {
value: ''
},
template: '<button #click="emit">Click to change</button>',
methods: {
emit() {
this.$emit('input', `changed on ${Date.now()}`);
}
}
});
Vue.component('ending-page', {
props: {
title: ''
},
template: '<div>{{ title }}</div>',
});
var app = new Vue({
el: '#app',
data() {
return {
title: 'unchanged',
};
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<typing v-model="title"></typing>
<ending-page :title="title"></ending-page>
</div>
One can try communication using vuex,
the data you want to share make it on this.$store.state or if recalling for functions use mutation(sync functions) and actions(async functions)
https://vuex.vuejs.org/
I like what Jeffrey Way suggested once, just create a global events object (which accidentally can be another Vue instance) and then use that as an event bus for any global communication.
window.eventBus = new Vue();
// in components that emit:
eventBus.$emit('event', data);
// in components that listen
eventBus.$on('event');

Access a component and trigger click event from App.vue component

In my template I have one click event
<span v-on:click="showGalery()">
And I am using one method for it
export default {
name: 'osaka',
data: function () {
return {
galery: false,
}
},
methods: {
showGalery () {
this.galery = true
}
}
}
Is it possible to trigger this method from App.vue template where is my nav and router links is located?
I am using vue-webpack template.
I have components, router.js, App.js and main.js structure.
Remember Vue has a one way data flow, so if you want to set something on the component you can simply pass a prop and use a watcher to trigger the change:
Vue.component('gallery', {
template: `<div v-show="gallery">Gallery</div>`,
props: {
show: {
type: Boolean,
default: false
}
},
created() {
this.gallery = this.show;
},
watch: {
show(val) {
this.gallery = val;
}
},
data() {
return {
gallery: false
}
}
});
Then in the parent you would have:
new Vue({
el: '#app',
data: {
showGallery: false
}
});
And use the following markup:
<gallery :show="showGallery"></gallery>
See this JSFiddle: https://jsfiddle.net/yx1uq370/
Incidentally, if you just want to show hide the entire component, then you can just use v-show on the component itself which
Vue.component('gallery', {
template: `<div>Gallery</div>`
});
new Vue({
el: '#app',
data: {
showGallery: false
}
});
Then your markup:
<gallery v-show="showGallery"></gallery>
And here's the fiddle for that: https://jsfiddle.net/gfr9kmub/
One final thing, are you sure that you really need to trigger this from your nav? I would assume that your nav would display the views and the views themselves would take care of this type of state management. Otherwise you may want to look at vuex to handle this situation

How to call function on child component on parent events

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");
}
}
}

Vue.js Changing props

I'm a bit confused about how to change properties inside components, let's say I have the following component:
{
props: {
visible: {
type: Boolean,
default: true
}
},
methods: {
hide() {
this.visible = false;
}
}
}
Although it works, it would give the following 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. Prop being mutated: "visible"
(found in component )
Now I'm wondering what the best way to handle this is, obviously the visible property is passed in when created the component in the DOM: <Foo :visible="false"></Foo>
Referencing the code in your fiddle
Somehow, you should decide on one place for the state to live, not two. I don't know whether it's more appropriate to have it just in the Alert or just in it's parent for your use case, but you should pick one.
How to decide where state lives
Does the parent or any sibling component depend on the state?
Yes: Then it should be in the parent (or in some external state management)
No: Then it's easier to have it in the state of the component itself
Kinda both: See below
In some rare cases, you may want a combination. Perhaps you want to give both parent and child the ability to hide the child. Then you should have state in both parent and child (so you don't have to edit the child's props inside child).
For example, child can be visible if: visible && state_visible, where visible comes from props and reflects a value in the parent's state, and state_visible is from the child's state.
I'm not sure if this is the behavour that you want, but here is a snippet. I would kinda assume you actually want to just call the toggleAlert of the parent component when you click on the child.
var Alert = Vue.component('alert', {
template: `
<div class="alert" v-if="visible && state_visible">
Alert<br>
<span v-on:click="close">Close me</span>
</div>`,
props: {
visible: {
required: true,
type: Boolean,
default: false
}
},
data: function() {
return {
state_visible: true
};
},
methods: {
close() {
console.log('Clock this');
this.state_visible = false;
}
}
});
var demo = new Vue({
el: '#demo',
components: {
'alert': Alert
},
data: {
hasAlerts: false
},
methods: {
toggleAlert() {
this.hasAlerts = !this.hasAlerts
}
}
})
.alert {
background-color: #ff0000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo" v-cloak>
<alert :visible="hasAlerts"></alert>
<span v-on:click="toggleAlert">Toggle alerts</span>
</div>
According to the Vue.js component doc:
When the parent property updates, it will flow down to the child, but not the other way around. So, how do we communicate back to the parent when something happens? This is where Vue’s custom event system comes in.
Use $emit('my-event) from the child to send an event to the parent. Receive the event on the child declaration inside the parent with v-on:my-event (or #my-event).
Working example:
// child
Vue.component('child', {
template: '<div><p>Child</p> <button #click="hide">Hide</button></div>',
methods: {
hide () {
this.$emit('child-hide-event')
}
},
})
// parent
new Vue({
el: '#app',
data: {
childVisible: true
},
methods: {
childHide () {
this.childVisible = false
},
childShow () {
this.childVisible = true
}
}
})
.box {
border: solid 1px grey;
padding: 16px;
}
<script src="https://unpkg.com/vue/dist/vue.min.js"></script>
<div id="app" class="box">
<p>Parent | childVisible: {{ childVisible }}</p>
<button #click="childHide">Hide</button>
<button #click="childShow">Show</button>
<p> </p>
<child #child-hide-event="childHide" v-if="childVisible" class="box"></child>
</div>
If the prop is only useful for this child component, give the child a prop like initialVisible, and a data like mutableVisible, and in the created hook (which is called when the component's data structure is assembled), simply this.mutableVisible = this.initialVisible.
If the prop is shared by other children of the parent component, you'll need to make it the parent's data to make it available for all children. Then in the child, this.$emit('visibleChanged', currentVisible) to notify the parent to change visible. In parent's template, use <ThatChild ... :visibleChanged="setVisible" ...>. Take a look at the guide: https://v2.vuejs.org/v2/guide/components.html
After a read of your latest comments it seems that you are concerned about having the logic to show/hide the alerts on the parent. Therefore I would suggest the following:
parent
# template
<alert :alert-visible="alertVisible"></alert>
# script
data () {
alertVisible: false,
...
},
...
Then on the child alert you would $watch the value of the prop and move all logic into the alert:
child (alert)
# script
data: {
visible: false,
...
},
methods: {
hide () {
this.visible = false
},
show () {
this.visible = true
},
...
},
props: [
'alertVisible',
],
watch: {
alertVisible () {
if (this.alertVisible && !this.visible) this.show()
else if (!this.alertVisible && this.visible) this.hide()
},
...
},
...
To help anybody, I was facing the same issue. I just changed my var that was inside v-model="" from props array to data. Remember the difference between props and data, im my case that was not a problem changing it, you should weight your decision.
E.g.:
<v-dialog v-model="dialog" fullscreen hide-overlay transition="dialog-bottom-transition">
Before:
export default {
data: function () {
return {
any-vars: false
}
},
props: {
dialog: false,
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
After:
export default {
data: function () {
return {
dialog: false
}
},
props: {
notifications: false,
sound: false,
widgets: false
},
methods: {
open: function () {
var vm = this;
vm.dialog = true;
}
}
}
Maybe it looks like on hack and violates the concept of a single data source, but its work)
This solution is creating local proxy variable and inherit data from props. Next work with proxy variable.
Vue.component("vote", {
data: function() {
return {
like_: this.like,
dislike_: this.dislike,
}
},
props: {
like: {
type: [String, Number],
default: 0
},
dislike: {
type: [String, Number],
default: 0
},
item: {
type: Object
}
},
template: '<div class="tm-voteing"><span class="tm-vote tm-vote-like" #click="onVote(item, \'like\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{like_}}</span></span><span class="tm-vote tm-vote-dislike" #click="onVote(item, \'dislike\')"><span class="fa tm-icon"></span><span class="tm-vote-count">{{dislike_}}</span></span></div>',
methods: {
onVote: function(data, action) {
var $this = this;
// instead of jquery ajax can be axios or vue-resource
$.ajax({
method: "POST",
url: "/api/vote/vote",
data: {id: data.id, action: action},
success: function(response) {
if(response.status === "insert") {
$this[action + "_"] = Number($this[action + "_"]) + 1;
} else {
$this[action + "_"] = Number($this[action + "_"]) - 1;
}
},
error: function(response) {
console.error(response);
}
});
}
}
});
use component and pass props
<vote :like="item.vote_like" :dislike="item.vote_dislike" :item="item"></vote>
I wonder why it is missed by others when the warning has a hint
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. Prop being mutated: "visible" (found in component )
Try creating a computed property out of the prop received in the child component as
computed: {
isVisible => this.visible
}
And use this computed in your child component as well as to emit the changes to your parent.

Nesting VueJS Components

I wanted to try to nest components in my vue files:
The parent is implementing the component GISDispatches.vue. This should represent a table later on in the master page. So to test the whole thing, I want to see how the methods are working alltogether. I would expect in this setup, that the child method loadDispatchTable gets called and returns the console output.
Component Parent
<script>
import GISDispatches from '../components/GISDispatches.vue';
export default {
template: require('../templates/map.html'),
components: {
gisdispatches: GISDispatches
},
events: {
MapsApiLoaded: function() {
this.loadDispatchTable;
}
}
}
}
</script>
Component Child - GISDispatches.vue
<script>
export default {
template: require('../templates/GISDispatches.html'),
data() {
return {
dispatches: ''
}
},
computed: {
},
events: {
},
methods: {
loadDispatchTable: function() {
console.log("OK");
}
}
}
</script>
But the console output does not appear, still the page is loaded as expected. Did I need to change the setup?

Categories