I'm trying to implement a vue2 modal as described in the vue docs at https://v2.vuejs.org/v2/examples/modal.html.
It looks something like this:
<tbody v-if="fields.length">
<tr v-for="(item, index) in fields">
<td>#{{ item.name }}</td>
<td><input type="checkbox" id="checkbox" v-model="item.active"></td>
<td>
<button id="show-modal" #click="showModal = true">Show Modal</button>
</td>
</tr>
<modal :item="item" v-if="showModal" #close="showModal = false">
<h3 slot="header">Hello World</h3>
</modal>
</tbody>
Vue.component('modal', {
template: '#modal-template',
data: function() {
return {
item: ''
}
}
});
While the button shows up and does pop up the modal, I get a warning that Property or method "item" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option., and when I inspect the modal component in the Vue dev tools, the item is still '', it's not populated by the :item binding.
What's the right way to pass the item object into the modal so we can use it in the window?
If you're passing item as a value from the parent to a child component, you cannot use data--you must use props instead!
Vue.component('modal', {
template: '#modal-template',
props: ['item'],
data: function() {
return {
}
}
});
There are 2 things:
You don't reference item in the data object instead you should reference it as a prop for the component props: ['item'].
The modal is not inside the loop's scope, which ends at the closing </tr> tag. You could change this so that the click action performs a method that takes the item and assigns it to a variable that is always passed in like a prop. This would be similar to how you are doing it now, you would just change showModal = true to openModal(item) then have a method that would set the appropriate 2 values. Something like this:
openModal(item) {
this.showModal = true;
this.modalItem = item;
}
...
data: () => {
modalItem: null
...
}
Related
Problem : I am trying to create a table component for my app which would be use by other components to render a table. It could have three possible cell values :
Text
HTML
Component
I am able to render all the above values but I am stuck at binding an event listener. What I am trying to achieve is something like this :
Pass a method and event which is to be binded to the component and the table should bind that with respective cell.
So for example :
TABLE JSON
{
"cell-1":{
"type":"html",
"data":"<h4>text-1</h4>",
"method": someMethod
}
}
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit(someMethod)"
v-on:click.native="someMethod"
></span>
</td>
</tr>
</template>
</tbody>
Above is just a snippet of what I am trying, the table loops through the object passed and renders accordingly.
I have already tried
SO Solution 1
SO Solution 2
Please let me know if any more info is required.
The best way is to have the method/handler inside the parent component and then trigger is using the emit functionality such that in
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit('trigger-handler', {method: 'method1', data: {}})"
></span>
</td>
</tr>
</template>
</tbody>
and in
Parent.vue
<table-component #trigger-handler="triggerHandler" />
inside script
export default {
data() {
},
methods: {
triggerHandler(payload) {
// payload is actually the object passed from the child
this[payload.method](payload.data); // call a specific method
},
method1(data) {
},
method2(data) {
},
method3(data) {
}
}
}
EDIT: Here's a repo I made for easier parsing.
I have a Component that lists products in a datatable. The first column of the table is a link that shows a modal with a form of the product that was clicked (using its ID). I'm using the PrimeVue library for styling and components.
<template>
<Column field="id" headerStyle="width: 5%">
<template #body="slotProps">
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" />
<a href="#" #click.stop="toggleModal(slotProps.data.id)">
<span class="pi pi-external-link"> </span>
</a>
</template>
</Column>
</template>
<script>
import ProductForm from "./forms/ProductForm";
export default {
data() {
return {
activeModal: 0,
}
},
components: { ProductForm },
methods: {
toggleModal: function (id) {
if (this.activeModal !== 0) {
this.activeModal = 0;
return false;
}
this.activeModal = id;
},
showModal: function (id) {
return this.activeModal === id;
},
},
</script>
The modal is actually a sub component of the ProductForm component (I made a template of the Modal so I could reuse it). So it's 3 components all together (ProductList -> ProductForm -> BaseModal). Here's the product form:
<template>
<div>
<BaseModal :show="show" :header="product.name">
<span class="p-float-label">
<InputText id="name" type="text" :value="product.name" />
<label for="name">Product</label>
</span>
</BaseModal>
</div>
</template>
<script>
import BaseModal from "../_modals/BaseModal";
export default {
props: ["product", "show"],
components: { BaseModal },
data() {
return {};
},
};
</script>
When the modal pops up it uses the ProductForm subcomponent. Here is the BaseModal component:
<template>
<div>
<Dialog :header="header" :visible.sync="show" :modal="true" :closable="true" #hide="doit">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
methods: {
doit: function () {
let currentShow = this.show;
this.$emit("showModel", currentShow)
},
},
data() {
return {
};
},
};
</script>
I'm passing the product object, and a show boolean that designates if the modal is visible or not from the first component (ProductList) all the way down through the ProductForm component and finally to the BaseModal component. The modal is a PrimeVue component called Dialog. The component actually has it's own property called "closable" which closes the modal with an X button when clicked, that is tied to an event called hide. Everything actually works. I can open the modal and close it. For some reason I have to click the another modal link twice before it opens after the initial.
The issue is when I close a modal, I get the 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: "show" error. I've tried everything to emit to the event and change the original props value there, but the error persists (even from the code above) but I'm not sure if because I'm 3 components deep it won't work. I'm pretty new to using props and slots and $emit so I know I'm doing something wrong. I'm also new to laying out components this deep so I might not even be doing the entire layout correctly. What am I missing?
Well you are emitting the showModel event from BaseModal but you are not listening for it on the parent and forwarding it+listening on grandparent (ProductForm)
But the main problem is :visible.sync="show" in BaseModal. It is same as if you do :visible="show" #update:visible="show = $event" (docs). So when the Dialog is closed, PrimeVue emits update:visible event which is picked by BaseModal component (thanks to the .sync modifier) and causes the mutation of the show prop inside BaseModal and the error message...
Remember to never use prop value directly with v-model or .sync
To fix it, use the prop indirectly via a computed with the setter:
BaseModal
<template>
<div>
<Dialog :header="header" :visible.sync="computedVisible" :modal="true" :closable="true">
<slot />
</Dialog>
</div>
</template>
<script>
export default {
props: {
show: Boolean,
header: String,
},
computed: {
computedVisible: {
get() { return this.show },
set(value) { this.$emit('update:show', value) }
}
},
};
</script>
Now you can add same computed into your ProductForm component and change the template to <BaseModal :show.sync="computedVisible" :header="product.name"> (so when the ProductForm receives the update:show event, it will emit same event to it's parent - this is required as Vue event do not "bubble up" as for example DOM events, only immediate parent component receives the event)
Final step is to handle update:show in the ProductList:
<ProductForm :product="slotProps.data" :show="showModal(slotProps.data.id)" #update:show="toggleModal(slotProps.data.id)"/>
I'm building a small vue application where among other things it is possible to delete an entry of a music collection. So at this point I have a list of music albums and next to the entry I have a "delete" button. When I do the following:
<li v-for="cd in cds">
<span>{{cd.artist}} - {{cd.album}}</span> <button v-on:click="deleteAlbum(cd.ID)">Delete</button>
</li>
and then in my methods do:
deleteAlbum(id){
this.$http.delete('/api/cds/delete/'+id)
.then(function(response){
this.fetchAll()
// });
},
this works fine so far, but to make it more nice, I want the delete functionality to appear in a modal/popup, so I made the following changes:
<li v-for="cd in cds">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button #click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})" class="btn">Delete</button>
</div>
<delete-modal v-if="showDelete" #close="showDelete = false" #showDeleteModal="cd.ID = $event"></delete-modal>
</li>
so, as seen above I created a <delete-modal>-component. When I click on the delete button I want to pass the data from the entry to <delete-modal> component with the help of an eventbus. For that, inside my methods I did this:
showDeleteModal(item) {
this.showDelete = true
eventBus.$emit('showDeleteModal', {item: item})
}
Then, in the <delete-modal>, inside the created()-lifecycle I did this:
created(){
eventBus.$on('showDeleteModal', (item) => {
console.log('bus data: ', item)
})
}
this gives me plenty of empty opened popups/modals!!??
Can someone tell me what I'm doing wrong here?
** EDIT **
After a good suggestion I dumped the eventBus method and pass the data as props to the <delete-modal> so now it looks like this:
<delete-modal :id="cd.ID" :artist="cd.artist" :album="cd.album"></delete-modal>
and the delete-modal component:
export default {
props: ['id', 'artist', 'album'],
data() {
return {
isOpen: false
}
},
created(){
this.isOpen = true
}
}
Only issue I have now, is that it tries to open a modal for each entry, how can I detect the correct ID/entry?
I am going to show you how to do it with props since it is a parent-child relation.I will show you a simple way of doing it.You need to modify or add some code of course in order to work in your app.
Parent component
<template>
<div>
<li v-for="cd in cds" :key="cd.ID">
<div class="cd-wrap">
<span>{{cd.artist}} - {{cd.album}}</span>
<button
#click="showDeleteModal({id: cd.ID, artist: cd.artist, album: cd.album})"
class="btn"
>
Delete
</button>
</div>
<delete-modal v-if="showDelete" :modal.sync="showDelte" :passedObject="objectToPass"></delete-modal>
</li>
</div>
</template>
<script>
import Child from 'Child'
export default {
components: {
'delete-modal': Child
},
data() {
return {
showDelete: false,
objectToPass: null,
//here put your other properties
}
},
methods: {
showDeleteModal(item) {
this.showDelete = true
this.objectToPass = item
}
}
}
</script>
Child Component
<template>
/* Here put your logic component */
</template>
<script>
export default {
props: {
modal:{
default:false
},
passedObject: {
type: Object
}
},
methods: {
closeModal() { //the method to close the modal
this.$emit('update:modal')
}
}
//here put your other vue.js code
}
</script>
When you use the .sync modifier to pass a prop in child component then,there (in child cmp) you have to emit an event like:
this.$emit('update:modal')
And with that the modal will close and open.Also using props we have passed to child component the object that contains the id and other stuff.
If you want to learn more about props, click here
In my vue.js app, I need to display a list of items which the user can click.
When clicked, each of these items should then fire a modal, containing additional information about the item that the user just clicked.
What I have so far in my Items.vue component is:
<template>
<div id="content">
<li v-for="item in items" class="list-item pa2 ba">
<div class="f5 pt2 pb2">
<span>{{item.name}}</span>
</div>
</li>
</div>
</template>
<script>
import Items from '#/api/items';
export default {
name: 'items',
asyncComputed: {
items: {
async get() {
const items = await Items.getAll();
return items.data;
},
default: []
}
},
components: {}
}
</script>
Now, I could simply add a modal component to the v-for loop, thus creating one modal for each item, but this does not seem ideal if, for example, I have a list of thousands of items.
This led me to believe that the modal should probably be placed at the root of the app (in my case App.vue), like this:
<template>
<div id="app">
<modal></modal>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
and then somehow fired with custom data whenever I needed it.
However, I'm not sure how to proceed. How do I fire this modal with custom information from inside the v-for loop, which is in a child component relative to App.vue?
These two links helped me figure this out:
https://v2.vuejs.org/v2/examples/modal.html
https://laracasts.com/discuss/channels/vue/passing-data-form-v-for-loop-to-modal-component
#In your Parent Component
You don't have to create a modal for each item within the v-for loop, simply include the modal-component at the beginning of your parent and then work with v-if="..." and props.
<template>
<div>
<modal v-if="modalVisible" #close="modalVisible = false" :data="modalData"/>
<div v-for="item in items">
<button type="button" #click="openModal(item)">Open Modal</button>
</div>
</div>
</template>
and then in your script:
import modal from './Modal'
export default {
components: {
modal
},
data() {
return {
modalVisible: false,
modalData: null
}
},
methods: {
openModal(data) {
this.modalData = data
this.modalVisible = true
},
}
#In your child (modal) component
In your modal you can now do the following:
Template:
<template>
{{ data.foo }}
<button #click="$emit('close')">Cancel</button>
</template>
Script
<script>
export default {
props: ['user']
};
</script>
Hope that helps :-)
Using vue.js 2, inside a for loop, I need to render a row only if the current iterated item passes some test.
The test is complex so a simple v-if="item.value == x" wont do.
I've written a function named testIssue that accepts the item and returns true or false and tried to use that is av-if like this:
<template v-for="issue in search_results.issues">
<tr v-if="testIssue(issue)">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
However, testIssue is never called.
If I change the line to <tr v-if="testIssue">, the function is called but then I dont have the issue variable that I need to test.
I also tried <tr v-if="releaseApp.testIssue(issue)">
How can I call a function in a v-if declaration inside a for loop and pass the current item?
First of all you can't do v-for on a <template> tag as you can have only one element per template.
You can add a v-if on the same element as v-for, and it just won't render the element that doesn't pass the v-if. If it's a spearate component per row then it's better to do v-for in parent component and pass the issue by props to child component.
Parent:
<child-comp v-for="issue in search_results.issues" v-if="testIssue(issue)">
</child-comp>
Child:
<template>
<tr>
{{issue}}
</tr>
</template>
Your scenario works with a similar example in this fiddle.
But you can try it this way also:
You can create a custom directive named v-hide and pass issue to it as its value.
Then in the directive you can test for testIssue() and set the particular element's display to none
<template v-for="issue in search_results.issues">
<tr v-hide="issue">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
directive:{
hide: {
bind(el, binding, Vnode){
var vm = Vnode.context;
var issue = binding.value;
if(vm.testIssue(issue)){
el.style.display = 'none';
}
}
}
},
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
You can also try creating a computed item that makes use of the filter method so that you have an array where all elements pass the test function before actually rendering (in this case, just returning odd numbers):
https://codepen.io/aprouja1/pen/BZxejL
computed:{
compIssues(){
return this.search_results.issues.filter(el => el%2===1)
}
},