I am having trouble implementing dynamic modal component in Vue.
A common approach I follow to display a set of data fetched from the db is I dump each of the rows in an HTML table element by iterating over each of the row of the db result. Something like this:
As you can see in the screenshot, each of the rows has one or more buttons which are dynamically generated by the loop.
In order to bind a modal component to each of the buttons (say the Remove buttons in this scenario) I do something like this.
HTML:
<div id="app">
<?php foreach($result as $x): ?>
<modal v-if="showModal">I am Modal $x</modal>
<btn @trigger="onShowModal">Button $x</btn>
<?php endforeach; ?>
</div>
So if I have three rows in my result, the aforementioned block of code will take the shape of something like this:
<div id="app">
<modal v-if="showModal">I am Modal 1</modal>
<btn @trigger="onShowModal">Button 1</btn>
<modal v-if="showModal">I am Modal 2</modal>
<btn @trigger="onShowModal">Button 2</btn>
<modal v-if="showModal">I am Modal 3</modal>
<btn @trigger="onShowModal">Button 3</btn>
</div>
And here is what I do in the JavaScript end:
JavaScript:
Vue.component('btn',{
template: `<button @click="$emit('trigger')"><slot></slot></button>`,
});
Vue.component('modal',{
template: `<p><slot></slot></p>`,
});
new Vue({
el: '#app',
data: {
showModal: false
},
methods: {
onShowModal(){
this.showModal = true;
}
}
});
The problem with this approach is, when I click any one of the Remove buttons, I get a 'stack' of modals instead of the modal I wanted to view. And that's because I am setting showModal
to true
and if you see the populated HTML block you will see that each of the modals is set to v-if="showModal"
.
Click here to see a demo
And as I am beginning to understand frontend-backend relationship, I am learning that this pattern is appearing more frequently in an application. How do I fix this issue (with a very vue-beginner-friendly level)?
The root issue is that you are referencing the same data property showModal
on all of the modal components.
You could make another component to encapsulate the button and modal component pair. This way there is a separate showModal
data property for each modal
and btn
pair:
Vue.component('btn',{
template: `<button @click="$emit('trigger')"><slot></slot></button>`,
});
Vue.component('modal',{
template: `<p><slot></slot></p>`,
});
Vue.component('modal-btn', {
template: `
<div>
<modal v-if="showModal">
<slot>I am a modal</slot>
</modal>
<btn @trigger="showModal = true">Show Modal</btn>
</div>
`,
data() {
return { showModal: false }
}
});
new Vue({
el: '#app',
data: {
showModal: false
},
methods: {
onShowModal(){
this.showModal = true;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<modal-btn></modal-btn>
<modal-btn>I am another modal</modal-btn>
<modal-btn>I'm a third modal</modal-btn>
</div>
You might need a more data centric approach to what you are trying. Something like,
<!DOCTYPE html>
<html>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
</head>
<body>
<div id="app">
<template v-for="item in items">
<modal v-if="item.show">I am a dummy Modal for {{item.name}}</modal>
<btn @trigger="item.show = !item.show">Button for {{item.id}}</btn>
</template>
</div>
<script>
Vue.component('btn',{
template: `<button @click="$emit('trigger')"><slot></slot></button>`,
});
Vue.component('modal',{
template: `<p><slot></slot></p>`,
});
new Vue({
el: '#app',
data: {
items: [
{id: 1, name: 'item 1', show: false},
{id: 2, name: 'item 2', show: false},
{id: 3, name: 'item 3', show: false}
],
showModal: false
},
methods: {
onShowModal(){
this.showModal = true;
}
}
});
</script>
</body>
</html>
You could then also get away with a single modal
component that receives the applicable data it should display via a prop
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With