Watching properties in child component does not work - vue.js

I have 2 components, a parent and a child. I want to modify the value of a prop in my parent component (by calling a method when a button is clicked) and send it to the child component. In the child component, I want to watch for changes in my prop, so that anytime it changes, it does something (for testing purposes, I tried to console.log() the prop).
Parent:
<template>
<div>
<h5>Your Feeds</h5>
<div id="feeds">
<div class="card" v-for="feed in feeds">
<div class="card-body" :id="feed['_id']" >
{{ feed['name'] }}
<button v-on:click="loadFeed(feed['_id'])">Button</button>
</div>
</div>
</div>
</div>
</template>
<script>
import GridComponent from "./GridComponent";
export default {
name: "FeedsListComponent",
data() {
return {
feeds: []
}
},
mounted() {
axios
.get("/getFeeds")
.then(response => (this.feeds = response.data))
.catch(error => console.log(error))
},
methods: {
loadFeed(id) {
this.feedId = id
}
},
components: {
GridComponent
}
}
</script>
Child:
<template>
<div id="grid">
<v-grid
theme="compact"
:source="rows"
:columns="columns"
></v-grid>
</div>
</template>
<script>
import VGrid from "#revolist/vue-datagrid";
export default {
name: "Grid",
props: ['feedId'],
data() {
return {
columns: [],
rows: [],
};
},
watch: {
feedId: function(val, oldVal) {
console.log(val)
console.log(oldVal)
console.log(this.feedId)
//here I want to send an ajax request with feedId to one of my controllers in order to get
//the data needed for rows and colums
}
},
components: {
VGrid,
},
};
</script>

I put together a sample that is working in order to help you diagnose why yours isn't working:
Parent.vue
<template>
<div class="parent">
<h3>Parent</h3>
<div class="row">
<div class="col-md-6">
<button class="btn btn-secondary" #click="incrementCounter">Change parent message</button>
</div>
</div>
<child :propMessage="message" />
</div>
</template>
<script>
import Child from '#/components/stackoverflow/watch-prop/Child'
export default {
components: {
Child
},
data() {
return {
counter: 0
}
},
computed: {
message() {
return 'Message' + this.counter;
}
},
methods: {
incrementCounter() {
this.counter++;
}
}
}
</script>
Child.vue
<template>
<div class="child">
<hr>
<div class="row">
<div class="col-md-6">
<label>Message in child from watched prop:</label>{{ dataMessage }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
propMessage: {
type: String,
required: true
}
},
data() {
return {
dataMessage: this.propMessage
}
},
watch: {
propMessage(newMessage) {
this.dataMessage = newMessage;
}
}
}
</script>
<style scoped>
label {
font-weight: bold;
margin-right: 0.5rem;
}
</style>

Related

can't display vuejs data property inside vue template

I'm trying to use eventbus to send data from component A:
<template>
<div v-for="(user, index) in users" :key="index" class="col-lg-6">
<div class="card card-primary card-outline">
<div class="card-body d-flex">
<h1 class="mr-auto">{{ user.name }}</h1>
Afficher
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: {},
}
},
methods: {
envoyerDetails($data){
Fire.$emit('envoyer_details_projet', $data);
this.$router.push('details-projet');
},
loadUser() {
if(this.$gate.isAdmin()){
axios.get("api/user").then(({ data }) => (this.users = data.data));
}
}
},
mounted() {
this.loadUser()
}
}
</script>
In component B, i receive the data and i want to display it inside the template this way:
<template>
<div class="right_col text-center" role="main">
<h5><b>name: {{ user.name }}</b> </h5>
</div>
</template>
export default {
data() {
return {
user: {},
}
},
methods: {
afficherDetails (args) {
this.user = args;
console.log(this.user.name);
}
},
mounted() {
Fire.$on('envoyer_details_projet', this.afficheDetails);
}
}
The data is not displayed in the template but it is displayed in the console. What am i missing?
Maybe when you emit the event envoyer_details_projet in component A, but component B is not mounted yet so that it can't receive the data.

Vue resets child Component data on props update

I have child component, that has some internal data, that should not be changed from outside. But when I update prop from parent component, this internal data is reset.
Basically in example below, when we change title from outside, value is set back to empty ''. How can I make value persistent with Child component props update?
Child.vue
<template>
<div class="child">
<h2>{{title}}</h2>
<input type="text" :value="value" v-on:change="$emit('change', $event.target.value)">
</div>
</template>
<script>
export default {
props: {
title: {
default: 'Just title'
}
},
data() {
return {
value: ''
}
}
}
</script>
Parent.vue
<template>
<div class="parent">
<Child :title="title" v-on:change="processTitle($event)"></Child>
</div>
</template>
<script>
import Child from './Child';
export default {
data() {
return {
title: 'Title from parent'
}
},
methods: {
processTitle(value) {
this.title = value.reverse();
}
}
}
</script>
You are not setting the value data attribute, :value=value means that "if value changes, the input value should pick up that change". But value doesn't change. Use v-model instead if you want to keep it simple.
Vue.component("Child", {
props: {
title: {
default: 'Just title'
}
},
data() {
return {
value: ''
}
},
template: `
<div class="child">
<h2>{{title}}</h2>
<input type="text" v-model="value" v-on:change="$emit('change', $event.target.value)">
</div>
`
})
new Vue({
el: "#app",
data() {
return {
title: 'Title from parent'
}
},
methods: {
processTitle(value) {
this.title = value.split("").reverse().join("");
}
},
template: `
<div class="parent">
<child :title="title" v-on:change="processTitle($event)"></child>
</div>
`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
EDIT
Also, if you want a continuous effect, don't use #change - use #input instead:
Vue.component("Child", {
props: {
title: {
default: 'Just title'
}
},
data() {
return {
value: ''
}
},
template: `
<div class="child">
<h2>{{title}}</h2>
<input type="text" v-model="value" v-on:input="$emit('change', $event.target.value)">
</div>
`
})
new Vue({
el: "#app",
data() {
return {
title: 'Title from parent'
}
},
methods: {
processTitle(value) {
this.title = value.split("").reverse().join("");
}
},
template: `
<div class="parent">
<child :title="title" v-on:change="processTitle($event)"></child>
</div>
`
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>

Vuejs passing function from parent to child

I have a beginner question about passing function from parent to child. In my example, I want to to use the child more times and sometimes it should to do someting else v-on:focus. How can i do that? There are options to pass it with prop but i don't know how and i think it's not good to do it ? Maybe with EventBus and if yes then how ? I want to know the right way how to do it in VueJs.
Here is the Parent Component:
import Child from "./child.js";
export default {
name: "app",
components: {
Child
},
template: `
<div>
<child></child>
<child></child>
<child></child>
</div>
`
};
And here is the child Component:
export default {
name: "test",
template: `
<div class="form-group">
<div class="input-group">
<input v-on:focus="functionFromChild">
</div>
</div>
`,
methods: {
functionFromChild() {
//run the function from parent
}
}
};
You can pass the function as any other prop
import Child from "./child.js";
export default {
name: "app",
components: {
Child
},
methods: {
calledFromChild(id){
console.log(id)
}
},
template: `
<div>
<child :callback="calledFromChild" id="1"></child>
<child :callback="calledFromChild" id="2"></child>
<child :callback="calledFromChild" id="3"></child>
</div>
`
};
And then in the child
export default {
name: "test",
props: ["callback", "id"],
template: `
<div class="form-group">
<div class="input-group">
<input v-on:focus="() => this.calledFromChild(this.id)">
</div>
</div>
`,
}
I'm also adding an id to the child so you know which child is making the call.
But this is not a good idea. You should use emit from your child to send an event, and listen to it from the parent.
In the child
export default {
name: "test",
template: `
<div class="form-group">
<div class="input-group">
<input v-on:focus="handleFocus">
</div>
</div>
`,
methods: {
handleFocus() {
this.$emit('focusEvent')
}
}
};
And in the parent
<child #focusEvent="handleFocusFromChild"></child>
A working example here
This should work:
const Child = {
template: `
<div class="form-group">
<div class="input-group">
<input v-on:focus="functionFromChild">
</div>
</div>
`,
props: {
functionFromParent: Function
},
methods: {
functionFromChild: function() {
this.functionFromParent();
}
},
data() {
return {
message: 'Oh hai from the component'
}
}
}
const App = {
template: `
<div>
<h1>Quick test</h1>
<p>{{ message }}</p>
<Child :functionFromParent="functionOnParent"/>
<Child :functionFromParent="functionOnParent"/>
<Child :functionFromParent="functionOnParent"/>
</div>
`,
components: {Child},
methods: {
functionOnParent: function(){
console.log("there we go");
}
},
data() {
return {
message: 'Hello'
}
}
}
new Vue({
render: h => h(App),
}).$mount('#app')
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>
If you're trying to call a function in the parent from the child component, then try
this.$parent.parentMethod()
This will invoke the method in parent component.

VueJS display dynamic modal component

I have posts and replys s.t. replies belong to posts via the attribute reply.posts_id.
I am attempting to show the reply form as a modal for the user to enter a reply. However, I want to create a generic Modal component that I can use everywhere with content that is specified in another component built for a specific context.
Reply to post is the first place I woul like this to work.
Currently, the Vuex correctly returns Modal visible:true when the reply button is clicked, but the modal does not render and I get the error message showing that the Modal component is not found:
Unknown custom element: <ModalReplyForm> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
I am using vuex to manage the visibility of the modal. Here are the relevant files:
store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
...
Vue.use(Vuex)
export default new Vuex.Store({
state: {
status: '',
...
modalVisible: false,
modalComponent: null
},
mutations: {
...
showModal(state, componentName) {
console.log('showing the modal')
state.modalVisible = true;
state.modalComponent = componentName;
},
hideModal(state) {
console.log('hiding the modal')
state.modalVisible = false;
}
},
actions: {
...
}
},
getters: {
isAuthenticated: state => !!state.user,
authStatus: state => state.status,
user: state => state.user,
token: state => state.token,
posts: state => {
return state.posts;
}
...
}
})
App.vue
<template>
<div id="app">
<app-modal></app-modal>
<NavigationBar />
<div class="container mt-20">
<router-view />
</div>
<vue-snotify></vue-snotify>
</div>
</template>
<script>
import AppModal from '#/components/global/AppModal';
import NavigationBar from '#/components/layout/NavigationBar'
export default {
name: "App",
components: {
AppModal,
NavigationBar
}
};
</script>
<style>
body {
background-color: #f7f7f7;
}
.is-danger {
color: #9f3a38;
}
</style>
Post.vue (houses the button to call the reply modal):
<template>
<div class="row ui dividing header news">
<!-- Label -->
<div class="m-1 col-md-2 ui image justify-content-center align-self-center">
<img v-if="post.avatar_url" :src="post.avatar_url" class="mini rounded"/>
<v-gravatar v-else :email="post.email" class="mini thumbnail rounded image rounded-circle z-depth-1-half"/>
</div>
<!-- Excerpt -->
<div class="col-md-9 excerpt">
...
<!-- Feed footer -->
<div class="feed-footer row">
<div class="small"> {{ post.created_at | timeAgo }}</div>
<button type="button" flat color="green" #click="showModal('ModalReplyForm')">
<i class="fa fa-reply" ></i>
...
<div v-show="postOwner(post)" class="">
<button type="button" flat color="grey" #click="deletePost(post.id)">
<i class="fa fa-trash " ></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
import PostsService from '../../services/PostsService'
import RepliesService from '../../services/RepliesService'
import Replies from '#/components/Reply/Replies'
import ReplyForm from '#/components/Reply/ReplyForm'
export default {
name: "Post",
props: {
post: {
type: Object,
required: true
}
},
components: {
Replies,
ReplyForm
},
computed: {
me() {
return this.$store.getters.user
}
},
methods: {
...mapMutations(['showModal']),
...
}
};
</script>
AppModal.vue - generic Modal component
<template>
<div class="c-appModal">
<div class="c-appModal__overlay" v-if="visible"></div>
<div class="c-appModal__content" v-if="visible" #click.self="hideModal"></div>
<div class="c-appModal__innerContent">
<component :is="component"></component>
</div>
</div>
</template>
<script>
import Vue from 'vue';
import { mapState, mapMutations } from 'vuex';
export default {
name: 'AppModal',
data() {
return {
component: null
}
},
computed: {
...mapState({
visible: 'modalVisible',
modalComponent: 'modalComponent'
}),
},
methods: {
...mapMutations(['hideModal'])
},
watch: {
modalComponent(componentName) {
if (!componentName) return;
Vue.component(componentName, () => import(`#/components/modals/${componentName}`));
this.component = componentName;
}
},
created() {
const escapeHandler = (e) => {
if (e.key === 'Escape' && this.visible) {
this.hideModal();
}
};
document.addEventListener('keydown', escapeHandler);
this.$once('hook:destroyed', () => {
document.removeEventListener('keydown', escapeHandler);
});
},
};
</script>
ModalReplyForm - specific reply modal content
<template>
<div>
<div class="c-modalReply">
<div>
<label for="reply">Your comment</label>
<div class="field">
<textarea name="reply" v-model="reply" rows="2" placeholder="Compose reply"></textarea>
</div>
</div>
<button class="c-modalReply__cancel" #click="hideModal">Cancel</button>
<button class="c-modalReply__post" :disabled="!isFormValid" #click="createReply">Reply</button>
</div>
</div>
</template>
<script>
import RepliesService from '#/services/RepliesService'
import { mapMutations } from 'vuex';
export default {
name: "ModalReplyForm",
// props: {
// post: {
// type: Object,
// required: true
// }
// },
data() {
return {
reply: ""
};
},
computed: {
isFormValid() {
return !!this.reply;
},
currentGroup() {
return this.$store.getters.currentPost;
}
},
methods: {
...mapMutations([
'hideModal'
]),
async createReply () {
let result = await RepliesService.addReply({
reply: {
body: this.reply,
postId: this.post.id
}
});
this.$emit("reply-created");
this.hideModal();
}
}
};
</script>
Unknown custom element: - did you register the
component correctly? For recursive components, make sure to provide
the "name" option.
This message says that you never imported/defined ModalReplyForm, which you have not.
In my own generic modal, I ended up having to import all the components that might appear within the modal itself.
If you add a:
import ModalReportForm from ...
and a:
components: {
ModalReplyForm
}
to AppModal.vue, the modal should then do what you expect.

Set css class for single items in v-for loop

I am playing around with Vue.js and I am trying to change the class of individual items in a v-for route dependent on a checkbox.
<template>
<div>
<ul>
<div :class="{completed: done}" v-for="things in items">
<h6 v-bind:key="things"> {{things}} - <input #click="stateChange" type="checkbox"/></h6>
</div>
</ul>
</div>
</template>
<script>
export default {
name: 'ItemList',
data() {
return {
items: [],
done: false
}
},
mounted() {
Event.$on('itemAdded', (data) => {
this.items.push(data);
})
},
methods: {
stateChange() {
this.done = !this.done;
}
}
}
</script>
<style>
.completed {
text-decoration-line: line-through;
}
</style>
The above code places a line through every item, not just the checked one.
How do I code so only the checked item is crossed out?
Thanks
Paul.
It looks like you have only one done property. You should have a done property for each element in your items array for this to work. Your item should like {data: 'somedata', done: false }
This should work:
<template>
<div>
<ul>
<div :class="{completed: item.done}" v-for="(item,index) in items">
<h6 v-bind:key="things"> {{item.data}} - <input #click="stateChange(item)" type="checkbox"/></h6>
</div>
</ul>
</div>
</template>
<script>
export default {
name: 'ItemList',
data() {
return {
items: [],
}
},
mounted() {
Event.$on('itemAdded', (data) => {
this.items.push({ data, done: false });
})
},
methods: {
stateChange(changeIndex) {
this.items = this.items.map((item, index) => {
if (index === changeIndex) {
return {
data: item.data,
done: !item.done,
};
}
return item;
});
}
}
}
</script>
<style>
.completed {
text-decoration-line: line-through;
}
</style>
#Axnyff
You were very close. Thank you. Here is the little changes I made to get it working.
<template>
<div>
<ul>
<div :class="{completed: item.done}" v-for="item in items">
<h6> {{item.data}} - <input #click="item.done = !item.done" type="checkbox"/></h6>
</div>
</ul>
</div>
</template>
<script>
export default {
name: 'ItemList',
data() {
return {
items: [],
}
},
mounted() {
Event.$on('itemAdded', (data) => {
this.items.push({ data, done: false });
console.log("DATA- ", this.items)
})
},
methods: {
}
}
</script>
<style>
.completed {
text-decoration-line: line-through;
}
</style>