Calling a child method from a button on a parent vue component - vue.js

I want to call the toggleDetails method of postDetails from the parent component's img tag and I don't seem to understand how to make it work.
Parent:
<div v-for="post in loggedInUser.posts" :key="post._id">
<postDetails :post="post"></postDetails>
<img #click.prevent="toggleDetails" class="grid-item" :src="post.imgUrl" />
</div>
</div>
Child:
<template>
<section v-if="this.isDetailsOpen">
{{this.post.desc}}
</section>
</template>
<script>
export default {
props: {
post: Object,
},
data() {
return {
isDetailsOpen: false
};
},
methods: {
toggleDetails() {
this.isDetailsOpen = !this.isDetailsOpen;
}
}
}
</script>

If you only want to emit an event from parent to child, you can do something like this:
Parent component:
<postDetails
ref="post"
:post="post"
></postDetails>
<img
#click.prevent="toggleDetails()"
class="grid-item"
:src="post.imgUrl"
/>
methods: {
toggleDetails () {
this.$refs.post.toggleDetails()
}
}
Child component:
methods: {
toggleDetails () {
this.isDetailsOpen = !this.isDetailsOpen
}
}
But to be able to send events between any components you would have to create an eventBus (eventHub).

Related

Vue: Toggle Button with Data from Child to Parent

I have a parent component that has numerous containers. Each container has an image and some buttons.
I have over simplified the parent and child components below. When a button is clicked that is within the child component, I would like to toggle the class on an element that is in the parent container. I would like to effect each image individually, not globally. How do I do this?
parent:
<template>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
<div>
<div :class="{ active: mock }">
<img src="/path">
</div>
<toggleButtons/>
</div>
</template>
<script>
import toggleButtons from './toggleButtons'
export default {
name: "parent",
components: {
toggleButtons
}
};
</script>
child:
<template>
<div class="switch-type">
<a #click="mock = false">Proto</a>
<a #click="mock = true">Mock</a>
</div>
</template>
<script>
export default {
name: "toggleButtons",
data() {
return {
mock: false
}
}
};
</script>
Oversimplified example of how you can pass data from child to parent:
Child:
<a #click="$emit('mockUpdated', false)">Proto</a>
<a #click="$emit('mockUpdated', true)">Mock</a>
Parent (template):
<toggleButtons #mockUpdated="doSomething" />
Parent(methods):
doSomething(value) {
// value will be equal to the second argument you provided to $emit in child
}
EDIT: (toggling the class for each individual container):
I would probably create a new component for the container (container.vue), pass a path as a prop :
<template>
<div>
<div :class="{ active: mock }">
<img :src="path">
</div>
<toggleButtons #mockUpdated="doSomething" />
</div>
</template>
<script>
export default {
props: {
path: String,
},
data() {
return {
mock: false
}
},
methods: {
doSomething(value) {
this.mock = value;
}
}
}
</script>
and then in Parent.vue, you can import the container component and use it like:
<template>
<Container path="/path-to-file.jpg" />
<Container path="/path-to-file.jpg" />
</template>
There is nothing to do with slots in your case. You need "emit event" to parent from button, and pass mock data to update img states. Slot is a very different thing. There are multiple ways to achive your goal. One way can be something like this:
parent
<AppImage v-for="img in imageData" :key="img.id" :image="img"/>
data() {
iamges: ["yourPath1", "yourPath2"]
},
computed: {
imageData() {
// adding ID is almost always a good idea while creating Vue apps.
return this.images.map(x => {
id: Math.floor(Math.random() * 1000),
path: x
})
}
}
Image.vue
<img :path="image.path" :class={ active: mock} />
<toggleButtons #toggled=changeImageState />
props: [image],
data() {
mock: ''
},
methods: {
changeImageState(event) {
this.mock = event
}
}
ToggleButtons.vue
<a #click="toggleMock(false)">Proto</a>
<a #click="toggleMock(true)">Mock</a>
emits: ['toggled']
methods: {
toggleMock(val) {
this.$emits('toggled', val)
}
}
Please read the code and let me know if you have any question.

Vue modal component using in parent component

I'm building simple modal component with Vue. I want to use this component in a parent components and to be able to toggle it from the parent.
This is my code now of the modal component:
<script>
export default {
name: 'Modal',
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
<template>
<div v-if="modalOpen" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
I use the v-if to toggle the rendering and it works with the button i created inside my modal component.
However my problem is: I don't know how to toggle it with simple button from parent component. I don't know how to access the modalOpen data from the modal component
Ok, let's try to do it right. I propose to make a full-fledged component and control the opening and closing of a modal window using the v-model in parent components or in other includes.
1) We need declare prop - "value" in "props" for child component.
<script>
export default {
name: 'Modal',
props: ["value"],
data() {
return {
modalOpen: true,
}
},
methods: {
modalToggle() {
this.modalOpen = !this.modalOpen
},
},
}
</script>
2) Replace your "modalToggle" that:
modalToggle() {
this.$emit('input', !this.value);
}
3) In parent components or other includes declare "modal=false" var and use on component v-model="modal" and any control buttons for modal var.
summary
<template>
<div v-if="value" class="modal">
<div class="body">
body
</div>
<div class="btn_cancel" #click="modalToggle">
<i class="icon icon-cancel" />
</div>
</div>
</template>
<script>
export default {
name: "Modal",
props: ["value"],
methods: {
modalToggle() {
this.$emit("input", !this.value);
}
}
};
</script>
Example:
Vue.component('modal', {
template: '<div v-if="value" class="modal"><div class="body">modal body</div><div class="btn_cancel" #click="modalToggle">close modal<i class="icon icon-cancel" /></div></div>',
props: ["value"],
methods: {
modalToggle() {
this.$emit('input', !this.value);
}
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app',
data:() =>({
modal: false
}),
methods: {
openModal() {
this.modal = !this.modal;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div #click="openModal">Btn modal</div>
<modal v-model="modal"></modal>
</div>
With your current implementation I would suggest you to use refs
https://v2.vuejs.org/v2/guide/components-edge-cases.html#Accessing-Child-Component-Instances-amp-Child-Elements
So in your parent component add ref="child" to modal (child) component and then open your modal by calling this.$refs.child.modalToggle()
You can use an "activator" slot
You can use ref="xxx" on the child and access it from the parent

How can I reset data in child component from parent component on vue.js 2?

My parent component like this :
<template>
<div ref="modal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content modal-content-data">
<form id="form-data">
...
<location-select .../>
...
</form>
</div>
</div>
</div>
</template>
<script>
import LocationSelect from './LocationSelect.vue'
export default{
name: 'CartModal',
components:{
LocationSelect,
},
mounted(){
$(this.$refs.modal).on('hidden.bs.modal', () => {
Object.assign(this.$data, this.$options.data())
})
}
}
</script>
If modal hidden, it will reset data in parent component and it works
I want to reset data also in child component
I try like this :
<template>
<select class="form-control" v-model="selected" ...>
...
</select>
</template>
<script>
export default{
name: 'LocationSelect',
...
created() {
$(this.$parent.$refs.modal).on('hidden.bs.modal', () => {
Object.assign(this.$data, this.$options.data())
})
}
};
</script>
But it does not work
The child component no reset the data
How can I solve this problem?
The main problem with this code is the handler in LocationSelect is being added before this.$parent.$refs.modal exists. A ref does not exist until the component is mounted.
The easiest way to solve this would be to move the code to mounted.
mounted() {
$(this.$parent.$refs.modal).on('hidden.bs.modal', () => {
Object.assign(this.$data, this.$options.data())
})
}
Or you could leave it in created and use nextTick.
created() {
this.$nextTick(() => {
$(this.$parent.$refs.modal).on('hidden.bs.modal', () => {
Object.assign(this.$data, this.$options.data())
})
})
}
Another way to handle this would be to add a ref to the LocationSelect component and add a method that clears it that can be called from the parent. In LocationSelect add this method:
methods:{
clear(){
Object.assign(this.$data, this.$options.data())
}
}
In the parent template add a ref:
<location-select ref="locationSelect" ... />
And in your parent's mounted:
mounted(){
$(this.$refs.modal).on('hidden.bs.modal', () => {
Object.assign(this.$data, this.$options.data())
this.$refs.locationSelect.clear()
})
}
However, the most idiomatic way to handle this with Vue would be to modify the component to support v-model and then it would be automatically cleared when the parent is cleared.
<template>
<select class="form-control" v-model="selected" ...>
...
</select>
</template>
<script>
export default {
props: ["value"],
name: 'LocationSelect',
computed:{
selected:{
get(){ return this.value },
set(v) { this.$emit('input', v) }
}
},
};
</script>
And then in the parent template:
<location-select v-model="someDataValue" ... />
If you did this, then when the parent is clear, the child is automatically cleared as well.

How to listen all child components rendered in a parent component?

synonym.vue
<template>
<div id="synonym-container">
<div></div>
<div id="synonym-group-list-wrapper">
<ul id="synonym-group-list">
<li v-for="synonymGroup of synonymGroupList" :key="synonymGroup._id">
<carousel :synonyms="synonymGroup.synonyms"></carousel>
</li>
</ul>
</div>
</div>
</template>
<script>
import Carousel from './synonym-carousel.vue'
import $ from 'jquery'
export default {
components: {
'carousel': Carousel,
},
mounted() {
console.log($('.synonym-list'));
},
}
</script>
synonym-carousel.vue
<template>
<div class="synonym-group">
<ul class="synonym-list">
<li></li>
</ul>
</div>
</template>
<script>
export default {
}
</script>
My goal is that I want to get $('.synonym-list').width() in synonym.vue file so I can change it for all child component in parent component, but I don't know how to manage it.I checked document theres no hook for it. If u have any idea please tell me, thanks!
Use events.
<carousel #update="onCarouselUpdate" :synonyms="synonymGroup.synonyms"></carousel>
...
export default {
components: { ... },
mounted () { ... },
methods: {
onCarouselUpdate () {
console.log('Carousel Updated!')
}
}
}
synonym-carousel:
export default {
updated () {
this.$emit('update')
}
}
docs: https://v2.vuejs.org/v2/guide/components.html#Custom-Events
synonym.js
<li v-for="(synonymGroup, i) of synonymGroupList" :key="synonymGroup._id">
<carousel #update="onCarouselUpdate" :synonyms="synonymGroup.synonyms" :isLast="i === (synonymGroupList.length - 1)? true: false"></carousel>
</li>
export default {
components: { ... },
mounted () { ... },
methods: {
onCarouselUpdate () {
console.log('Carousel Updated!')
}
}
}
synonym-carousel:
export default {
mounted() {
if(this.isLast) {
this.$emit('update');
}
}
Based on #CodinCat answer, this is my final resolution. It has the advantage that it does not trigger update for each list-item (since v-for is used) but only for the last one, so when 'everything is ready'.

Click button in child component to pass data to parent (via one other child)

Is it possible to pass data by clicking on the button in nested child component, pass it to anoter child component that is level above and then to parent component?
My components are nested this way:
Parent
----Child
---------Child 2 (with the button that should trigger action)
So Child 2 look like this:(I added the addList() method that should push the variable to the list that is in the parent)
<template>
<li class="movie">
<div class="movie__image">
<img class="movie__poster" :src="imageURL" :alt="movie.Title"/>
<button class="movie__more" :href="`https://www.imdb.com/title/${movie.imdbID}`" target="_blank">Add</button>
</div>
</li>
</template>
<script>
export default {
name: 'movie-list-item',
props: {
movie: {
type: String,
required: true
}
},
methods: {
addList(event) {
this.$emit('clicked', this.movie.imdbID)
}
}
</script>
Child 1
<template>
<ul class="grid">
<movie-list-item li v-for="movie in movies" :movie="movie" :key="movie.imdbID" />
</ul>
</template>
<script>
import MovieListItem from './MovieListItem'
export default {
name: 'movie-list',
components: {
MovieListItem
},
props: {
movies: {
type: Array,
required: true
}
}
}
</script>
Parent:(I want to push the item from child 2 component to picked list.
<template>
<div>
<movie-list :movies="movies"></movie-list>
</div>
</template>
<script>
import MovieList from './components/MovieList'
export default {
name: 'app',
components: {
MovieList,
},
data () {
return {
show:false,
movies: [],
picked:[]
}
}
</script>
Without using any kind of state management, you'll have to emit two time.
Otherwise, you can use Vuex and have a state management.
Child1
<template>
<ul class="grid">
<movie-list-item li v-for="movie in movies" :movie="movie" :key="movie.imdbID" #clicked="handleClick(movie.imdbID)" />
</ul>
</template>
<script>
import MovieListItem from './MovieListItem'
export default {
.....
methods: {
handleClick(imdbID) {
this.$emit("clicked", imdbID);
}
}
}
</script>
Parent
<template>
<div>
<movie-list :movies="movies" #clicked="handleClick($event)"></movie-list>
</div>
</template>
<script>
import MovieList from './components/MovieList'
export default {
...
data () {
return {
show:false,
movies: [],
picked:[]
}
},
methods: {
handleClick(imdbID) {
this.picked.push(imdbID);
}
}
</script>