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!
Related
In a <b-table> I would like to create an action on each items so I have a button:
<b-table :items="data" :fields="fields">
<template v-slot:cell(actions)="data">
<b-button v-on:click="doIt(data.index)">Do It</b-button>
</template>
</b-table>
Then I have a Form in a sidebar
<b-sidebar id="do-it-form" title="Do it" right>
...
</b-sidebar>
In my methods I would like to respond to the action:
methods: {
doIt(id) {
sidebar.form.id = id
sidebar.show().onSubmit(() => {
axio...
refresh(<b-table>)
})
}
}
Of course, this last part is not valid. On Bootstrap Vue manual I didn't find how to interact from Vue to Bootstrap components. Any clue?
You can emit an event on $root, which can be used to toggle the sidebar. The second parameter being the id of the sidebar you wish to open.
this.$root.$emit('bv::toggle::collapse', 'my-sidebar-id')
<b-collapse> and <b-sidebar> listens for the same event, which is why it says collapse in the event.
new Vue({
el: '#app',
methods: {
openSidebar() {
this.$root.$emit('bv::toggle::collapse', 'my-sidebar')
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.js"></script>
<div id="app">
<b-sidebar id="my-sidebar" right>
My sidebar
</b-sidebar>
<b-btn #click="openSidebar">
Open sidebar
</b-btn>
</div>
Alternatively you can bind a boolean property to the v-model on the sidebar and set the boolean to true when you want to open the sidebar.
new Vue({
el: '#app',
data() {
return {
isSidebarOpen: false
}
},
methods: {
openSidebar() {
this.isSidebarOpen = true
}
}
})
<link href="https://unpkg.com/bootstrap#4.5.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.17.1/dist/bootstrap-vue.js"></script>
<div id="app">
<b-sidebar v-model="isSidebarOpen" right>
My sidebar
</b-sidebar>
<b-btn #click="openSidebar">
Open sidebar
</b-btn>
</div>
Also, you can use one of the sidebar built-in public methods show, hide or toggle. All you need is add a reference to your sidebar
<b-sidebar ref="mySidebar" id="do-it-form">
...
</b-sidebar>
and then in any of your methods where/when it's needed, you can simply call any of them
this.$refs.mysidebar.show();
this.$refs.mysidebar.hide();
this.$refs.mysidebar.toggle();
You can also just assign a boolean value to the visible prop on the b-sidebar component and toggle the boolean value as you like.
<b-sidebar ref="mySidebar" id="do-it-form" :visible="showSidebar">
...
</b-sidebar>
And toggling it:
data: {
showSidebar: false, //starts off invisible
},
methods: {
toggleSidebar(){
this.showSidebar = !this.showSidebar
}
}
This approach at first glance is not the best in an approach where you have other components updating the sidebar visiblity. This use case is for situations when all updates to the sidebar visibility are made from a central store by using a central boolean value.
e.g.
const state = {
showSidebar: null
}
const mutations: {
toggleSidebar(state, payload){
if (payload) { //payload incase you want to force a value and not just toggle
state.showSidebar = payload;
} else {
state.showSidebar = !state.showSidebar;
}
}
}
And in your components:
computed: {
showSidebar(){
return this.$store.state.showSidebar
}, //starts off invisible
},
methods: {
toggleSidebar(){
this.$store.commit("toggleSidebar");
}
}
Your updated sidebar component would look like this:
<b-sidebar ref="mySidebar" id="do-it-form" :visible="showSidebar" #change="updateSidebar">
...
</b-sidebar>
And the method:
methods: {
updateSidebar(value){
this.$store.commit("toggleSidebar", value);
}
}
I have a 'views' page that imports two components, one of which is a NavBar that will display a loading animation until the other component is fully loaded in.
The way I'm trying to accomplish this, is I am trying to define a 'loading' var in the view, pass that var into the NavBar AND releases components. IF I change the loading to false from the releases component that should propagate over to the NavBar (to stop the loading animation).
views/Release.vue
<template>
<div>
<NavBar v-bind:loading="this.loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases v-bind:loading="this.loading"></Releases>
<Footer></Footer>
</div>
</div>
</template>
<script>
import Releases from "../components/Releases.vue";
import NavBar from "./components/NavBar.vue";
export default {
name: "releases",
data () {
return {
loading: 'loading'
}
},
components: {
NavBar,
Releases,
}
};
</script>
components/NavBar.vue
<template>
<div>
<div id="nav">
<a href='/link1'>Link 1</a>
<a href='/link2'>Link 2</a>
<a href='/link3'>Link 3</a>
<pulse-loader :loading="this.loading"></pulse-loader>
</div>
</div>
</template>
<script>
import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
export default {
name: 'NavBar',
props: ['loading'],
components: {
PulseLoader
},
};
</script>
I have left out Releases.vue from this post for brevity, but no matter where I set
this.loading=false
It does not seem to propagate over to NavBar component.
What am I doing wrong here? Not sure If I need to use $emit for something like this?
No, you should NOT modify the prop loading from Releases.vue.
In Releases.vue when data loaded, call $emit:
this.loadReleases()
.then(() => {
// Your logic.
this.$emit('loaded', true);
});
In the view Release.vue
<template>
<div>
<NavBar :loading="loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases #loaded="updateLoading"></Releases>
<Footer></Footer>
</div>
</div>
</template>
<script>
import Releases from "../components/Releases.vue";
import NavBar from "./components/NavBar.vue";
export default {
name: "releases",
data () {
return {
loading: true,
}
},
components: {
NavBar,
Releases,
},
methods: {
updateLoading(val) {
this.loading = !val; // loading = false;
},
},
};
</script>
Please, use : instead of v-bind, # instead of v-on for making the code clear. And it's no need to use this on the template.
Do not use this in your templates.
<NavBar v-bind:loading="loading"></NavBar>
<div id="vue-main">
<h1><b>Releases</b></h1>
<Releases v-bind:loading="loading"></Releases>
<Footer></Footer>
<pulse-loader :loading="loading"></pulse-loader>
In fact, eveything in the template refers to this component, and you can't refer to anything else directly from the template.
$emit is for sending data up to the parent, and the main use case for that is to tell the parent to update a property that then flows back down to the component. Your use case is updating children, and using v-bind is appropriate, as the NavBar owns the data.
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
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 :-)
I am using a dropdown menu components in vuejs to make normal dropdown menu.
My code is for dropdown component is :
<template>
<span class="dropdown" :class="{shown: state}">
toggle menu
<div class="dropdown-menu" v-show="state">
<ul class="list-unstyled">
<slot></slot>
</ul>
</div>
</transition>
</span>
</template>
<script>
export default {
name: 'dropdown',
data () {
return {
state: false
}
},
methods: {
toggleDropdown (e) {
this.state = !this.state;
}
}
}
</script>
Now I am importing the dropdown component in my VUE app at various place by using following code in the template
<dropdown>
<li>
Action
</li>
</dropdown>
Now that is working fine but I want that only one dropdown should be active at the same time.
I have done little research and found that i can use plugins like https://github.com/davidnotplay/vue-my-dropdown but I don't want to use that. Again i have also studied how the above example works but I want to implement this dropdown functionality in such a way that my dropdown component would take care of all event related to dropdown. So can you help me how to achieve that?
I know it's quite an old question but I think the best way to do that without any external plugins is to add a click listener to mounted lifecycle hook (and remove it on beforeDestroy hook) and filter the clicks on your component so it only hides when clicked outside.
<template>
<span class="dropdown" :class="{shown: state}">
toggle menu
<div class="dropdown-menu" v-show="state">
<ul class="list-unstyled">
<slot></slot>
</ul>
</div>
<transition/>
</span>
</template>
<script>
export default {
name: 'dropdown',
data () {
return {
state: false
}
},
methods: {
toggleDropdown (e) {
this.state = !this.state
},
close (e) {
if (!this.$el.contains(e.target)) {
this.state = false
}
}
},
mounted () {
document.addEventListener('click', this.close)
},
beforeDestroy () {
document.removeEventListener('click',this.close)
}
}
</script>
Have a look at vue-clickaway.(Link)
Sometimes you need to detect clicks outside of the element (to close a modal window or hide a dropdown select). There is no native event for that, and Vue.js does not cover you either. This is why vue-clickaway exists. Please check out the demo before reading further.
In Vue 3 the following should work
Note that the #click.stop on the dropdown trigger and on the dropdown content prevents the document event from executing the close function.
<template>
<div class="dropdown" :class="{'is-active': dropdown.active.value}">
<div class="dropdown-trigger">
<button class="button" #click.stop="dropdown.active.value = !dropdown.active.value">
Toggle
</button>
</div>
<div class="dropdown-menu" role="filter">
<div class="dropdown-content" #click.stop>
<!-- dropdown items -->
</div>
</div>
</div>
</template>
<script>
import { defineComponent, ref, onMounted, onBeforeUnmount } from "vue";
export default defineComponent({
name: "Dropdown",
setup(){
const dropdown = {
active: ref(false),
close: () => {
dropdown.active.value = false
}
}
onBeforeUnmount(() => {
document.removeEventListener('click', dropdown.close)
})
onMounted(() => {
document.addEventListener('click', dropdown.close)
})
return {
dropdown
}
}
})
</script>
This example uses bulma but off course you don't need to.
You can make use of the blur event, e.g. if you add a method:
close() {
setTimeout(() => {
this.state = false;
}, 200);
}
And set the blur event for your link:
toggle menu
Then the dropdown will get hidden whenever your toggle link loses focus.
The setTimeout is necessary because Javascript click events occur after blur, which would result in your dropdown links being not clickable. To work around this issue, delay the menu hiding a little bit.