Pass a property parameter to the Vue component - javascript

I'm using Vue+Laravel, I want to make a modal window with 2 tabs. I have two buttons, each button must open certain connected tab.
Buttons are places in blade template:
<button #click="openModal('login')">
<button #click="openModal('register')">
//Vue component also is placed in blade template
<auth-component :show="showModal" :active_tab="activeTab" #close="showModal = false"></auth-component>
/resources/assets/js/app.js:
I want to pass openModal() parameter to a component...
const app = new Vue({
el: '#app',
data: {
showModal: false,
activeTab: '',
},
methods: {
openModal(tab) {
this.activeTab = tab;
this.showModal = true;
}
},
});
... and use that parameter as Vue prop to set certain bootstrap-tab as active inside Vue-component:
AuthComponent.vue:
<div class="modal-mask" #click="close" v-show="show">
<div class="modal-wrapper">
<div class="modal-container" #click.stop>
<b-card no-body>
<b-tabs card>
<b-tab title="Login" :active="active_tab === login">
<b-tab title="Register" :active="active_tab === register">
</b-tabs>
</b-card>
</div>
</div>
</div>
...
props: ['show', 'active_tab'],
But my current code does not work. There are no console errors. Just first tab (login) is always active after modal is opened.
UPDATE: I moved to
<b-tab title="Login" :active="active_tab === login">
<b-tab title="Register" :active="active_tab === register">
and I see that in Vue console needed tab get active: true property. But while active: true Register tab is anyway display: none. So I guess setting active property as true isn't enough to actually make tab content visible.
JSFiddle component code

You could try wrapping them in <b-tabs v-model="tabIndex"></b-tabs> and then have a computed method that determines the index based off the prop.
EDIT
computed: {
tabIndex: {
get: function() {
if (this.activeTab === 'register') {
return 1;
} else {
return 0;
}
},
set: function(newValue) {
return newValue;
}
}
}

Try passing openModal.bind(undefined,'login') so you return a function with the bound params instead of invoking that function.
Also, the tabs component is set with the v-model attribute. It uses numeric indexes to set the active tab.
Maybe make a lookup object to simplify things:
const tab_map = { login: 0, register: 1 }
Then you can setup openModal.bind(undefined, tab_map.login)
For the v-model setup on tabs, check this example out: https://bootstrap-vue.js.org/docs/components/tabs/#external-controls

Related

Using v-for inside component that uses <slot/> to create re-usable and dynamic Tab menu

So I'm trying to create a dynamic tab menu with Vue 3 and slots. I got the tabs working, I have BaseTabsWrapper and BaseTab components. I need to be able to v-for with BaseTab component inside of a BaseTabsWrapper Component. Like this:
<section
id="content"
class="w-full mx-2 pr-2"
v-if="incomingChatSessions && incomingChatSessions.length"
>
<BaseTabsWrapper>
<BaseTab
v-for="chatSession in incomingChatSessions"
:key="chatSession.id"
:title="chatSession.endUser.name"
>
<p>{{ chatSession }}</p>
</BaseTab>
</BaseTabsWrapper>
</section>
An important caveat from the answers that I have found is that the incomingChatSessions object is asynchronous and coming from a websocket (I have tested that this object is working fine and bringing all the data correctly aka is never an empty object).
Inside of BaseTabsWrapper template. Important parts:
<template>
<div>
<ul
class="tag-menu flex space-x-2"
:class="defaultTagMenu ? 'default' : 'historic'"
role="tablist"
aria-label="Tabs Menu"
v-if="tabTitles && tabTitles.length"
>
<li
#click.stop.prevent="selectedTitle = title"
v-for="title in tabTitles"
:key="title"
:title="title"
role="presentation"
:class="{ selected: title === selectedTitle }"
>
<a href="#" role="tab">
{{ title }}
</a>
</li>
</ul>
<slot />
</div>
</template>
And the script:
<script>
import { ref, useSlots, provide } from 'vue'
export default {
props: {
defaultTagMenu: {
type: Boolean,
default: true,
},
},
setup(props) {
const slots = useSlots()
const tabTitles = ref(
slots.default()[0].children.map((tab) => tab.props.title)
)
const selectedTitle = ref(tabTitles.value[0])
provide('selectedTitle', selectedTitle)
provide('tabTitles', tabTitles)
return {
tabTitles,
selectedTitle,
}
},
}
</script>
This is the Tab component template:
<template>
<div v-show="title === selectedTitle" class="mt-4">
<slot />
</div>
</template>
<script>
import { inject } from 'vue'
export default {
props: {
title: {
type: String,
default: 'Tab Title',
},
},
setup() {
const selectedTitle = inject('selectedTitle')
return {
selectedTitle,
}
},
}
</script>
The important part in my script and the one that is giving me a lot of trouble is this one:
const tabTitles = ref(
slots.default()[0].children.map((tab) => tab.props.title)
)
What I'm doing here is creating an array of tab titles based on the property "title" of each slot but when I load the page this array always have just one title, even if I'm fetching more title elements from the API. One thing that I have noticed is that if I force a re-render of the page from my code then the tabTitles array have the correct amount of elements and I got all the correct amount of tabs on my menu. I have tested that everything is working fine with the way I control asynchronicity with the data coming from the websocket in order to hidrate the "incomingChatSessions" array but as much as I try tabTiles always gets just one element no matter what.
i would do something like that :
computed(
() => slots.default()[0].children.map((tab) => tab.props.title)
)
it should update the computed property when the component is updated (like slot changes)

Vue.js - How to prevent re-render whole component?

I want to prevent my whole component (a modal) re-render. When user login, my web application shows a modal including some messages. Once user clicks next, the content in this modal changes. However, the modal will shows the pop-up animation again. The modal use same modal, but change the content.
This is absolute how Vue works, however, if the firstPage changes, the modal pop-up again... How could I only re-render the content part, not the whole modal?
<template>
<div>
<b-modal v-model="modalShow">
<p v-if="firstPage">Hello</p>
<p v-else>{{content}}</p>
</b-modal>
</div>
</template>
<script>
export default {
data() {
return {
modalShow: false,
}
},
computed() {
content() {
return this.$store.state.content
},
firstPage() {
return this.$store.state.firstPage
}
}
}
</script>
The re-render issue is unlikely to be related to the children, could you eloborate the example more? As you can see in the example, content of the children changes without opening the modal second time, it should be related to unwanted change on the "modalShow", how and where are you changing this data.
window.onload = () => {
new Vue({
el: '#app',
data() {
return {
modalShow: false,
firstPage: true,
}
},
methods: {
toggle() {
this.modalShow = !this.modalShow;
setTimeout(() => this.firstPage = !this.firstPage, 300);
}
}
})
}
<link href="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.css" rel="stylesheet"/>
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.js"></script>
<script src="https://unpkg.com/babel-polyfill/dist/polyfill.min.js"></script>
<div id="app">
<div>
<b-button #click="toggle">Launch demo modal</b-button>
<b-modal v-model="modalShow">
<p class="my-4" v-if="firstPage">1st Component</p>
<p class="my-4" v-else>2nd Component</p>
</b-modal>
</div>
</div>
After reading your question I imagine you have an login button.. than you can do following - add a click event to your button like this:
<b-button #click="login = !login> LOGIN </b-button> //use this if you want to switch your modal each time on click
<b-button #click="login = false"> LOGIN </b-button> //use this if you only want to change it one time after click
Here you are switching your boolean if it's firstly true after clicking your button it will be false.
Than you have to define your property in your data return like this:
data() {
return {
login: true,
}
}
and afterwards you can check it in your template, like you have done it before:
<p v-if="login == true">Login is true</p>
<p v-if="login == false">Login is false</p>
<!-- OR -->
<p v-if="login == true">Login is true</p>
<p v-else>Login is false</p>
You can also add a method to your click event and check it there if you don't want to have code like login = !login in your template!
Please let me know if this helps you!

Bootstrap Vue Modal Component

I'm looking to make a global Bootstrap Vue Modal component that I can use for all of my modal needs.
The issue I am facing is that it keeps giving me an error regarding prop mutation. You cannot update them directly in the child component. Which makes sense.
This is what I have so far:
<template>
<b-modal
v-model="show"
size="lg"
scrollable
centered
header-class="event-close-modal-header"
#hide="$emit('close')"
#cancel="$emit('close')"
#ok="$emit('close')"
#show="handleShow"
>
<template #modal-title="{}">
<span class="event-close-modal-header-mc2">{{ title }}</span>
</template>
<b-container fluid class="pb-4">
<h1>Content goes here</h1>
<slot />
</b-container>
</b-modal>
</template>
<script>
export default {
props: {
show: {
type: Boolean,
default: false,
required: true,
},
title: {
type: String,
required: true,
},
},
data() {
return {};
},
};
</script>
And in my parent I have something like this:
<ModalHelper
:show.sync="modalOne" // modalOne is a data property
title="One"
#close="modalOne = false"
/>
<button #click="modalOne = true">Open Modal One</button>
Error:
You can't use props in v-model. Try watch it, and copy new value into new variable in data:
data() {
return {
showModal: false
}
},
watch: {
show(newValue) {
this.showModal = newValue;
}
}
now use showModal in your code instead of "show".
However, this is no good approach.
You should open-close bootstrap vue modals using:
$bvModal.show()
$bvModal.hide()
Vue Bootstrap Modal

Trying to avoid mutating a prop that goes 3 levels deep by using $emit

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)"/>

VueJS How to pass data to a modal component using eventbus

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

Categories