Vue 2 component re-render after switching dynamic component inside it - vue.js

I have a main component that import two other components:
admin page
login page
in the main component I have:
<template>
<div id="app">
<component v-bind:is="currentView"></component>
</div>
</template>
<script>
import AdminPage from './components/adminPage/AdminPage'
import LoginPage from './components/LoginPage'
export default {
name: 'app',
components: {
AdminPage, LoginPage
},
data: function() {
return {
currentView: 'LoginPage'
}
},
created() {
this.$bus.$on('eventFromLoginPage', event => {
this.currentView = "AdminPage";
});
}
}
</script>
and in the Login Page I have a method that emit a trigger to the main app:
changeView: function() {
this.$bus.$emit('eventFromLoginPage');
}
The main component is called by the main.js file:
import Vue from 'vue'
import App from './App'
Object.defineProperty(Vue.prototype, '$bus', {
get() {
return this.$root.bus;
}
});
var bus = new Vue({})
new Vue({
el: '#app',
template: '<App/>',
components: { App },
data: {
bus: bus
}
})
The problem is that although when I call the changeView function in the login page, the trigger is sent and the view in the main component changes to the adminPage as desired, but than the main page is re rendered and the view returns to the login page.
The question: Why does the main component re-render after changing the "currentView" state and how can I keep the Admin page as the view.

Calling Vue on something inside a template is weird. Call it on the top-level HTML element.
Only use a bus when events are being handled by something outside the parent-chain of the component emitting the event
Handle events using v-on and a method
AdminPage = {
template: '<div>The admin page</div>'
};
LoginPage = {
template: '<div>The LOGIN page<button #click="doLogin">Login</button></div>',
methods: {
doLogin: function() {
this.$emit('eventFromLoginPage');
}
}
};
App = {
template: '<component v-bind:is="currentView" v-on:eventFromLoginPage="goToAdminPage"></component>',
components: {
AdminPage, LoginPage
},
data: function() {
return {
currentView: 'LoginPage'
}
},
methods: {
goToAdminPage: function() {
this.currentView = 'AdminPage';
}
}
};
new Vue({
el: '#app',
components: {
App
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>
<div id="app">
<App></App>
</div>

Related

vue eventbus - $on wont trigger the bus

I have looked at tutorials and read the papers but I don’t get it why my setup with eventbus does not work.
In main.js
I create a new instance of Vue
/* create a eventbus*/
export const Bus = new Vue();
In page1
import { Bus } from "../main";
I then have a click event that’s triggers a method
methods: {
moveData(inValue) {
let valueToSend = inValue;
console.log("valueToSend");
console.log(valueToSend);
Bus.$emit("emitAlbumTitle", valueToSend);
},
},
And console.log tells there nothing wrong with the method moveData().
In page 2.
I try to listen to the busemit.
import { Bus } from "../main";
data() {
return {
id: this.$route.params.idAlbum,
photoData: [],
albumTitle: "",
};
},
In tried in created(), I have some other things going on there as you see, like an api-call but that should not affect this I think.
async created() {
try {
this.photoData = await CallApi.getPosts(url + this.id);
this.number = this.photoData.length;
Bus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
} catch (err) {
this.error = err.message;
}
},
But nothing in the console.logs in the Bus.$on starts, so that eventbus never starts?
I also have tried in mounted() hook
mounted() {
Bus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
},
But same result.
What am I missing here?
The problem is your Receiver component is not created until you click the link, at which point the event has already been emitted from Sender.
One solution is to delay the event emitted until the next macro tick (using setTimeout without a delay), as the Receiver component would be created in the current macro tick:
export default {
methods: {
async emitValue() {
// wait til next macro tick
await new Promise(r => setTimeout(r));
EventBus.$emit("string-send", this.sendString);
},
}
}
demo
Try same as this works for me. Data will show in console on button click.
Here is the main.js file.
import Vue from 'vue'
import App from './App.vue'
import vuetify from './plugins/vuetify';
Vue.config.productionTip = false
export const eventBus = new Vue();
new Vue({
vuetify,
render: h => h(App)
}).$mount('#app')
It is App.vue file.
<template>
<v-app>
<div id="app">
{{albumTitle}}
<br>
<button class="primary" #click="dataSend">send</button>
</div>
</v-app>
</template>
<script>
import {eventBus} from '#/main'
export default {
name: "App",
data: () => ({
albumTitle: null
}),
created(){
eventBus.$on("emitAlbumTitle", (data) => {
this.albumTitle = data;
console.log("in the $bus");
console.log(data);
});
},
methods: {
dataSend(){
eventBus.$emit("emitAlbumTitle", "some data")
}
},
};
</script>

Vue Watch not Triggering on Page Refresh/Reload

I have a component which is hid based on the route which is active, it kicks off a function which is stored using vuex store.
It works as intended, the sidenav is hidden on login, logout, and register.
However, I noticed when I am on an authenticated page such as admin panel, or dashboard, etc, the component displays correctly, but when/if someone reloads the webpage, the component disappears, only to be displayed when clicking a link to another page.
App.Vue
<template>
<div id="app">
<navbar />
<sidenav v-show="sidenav_toggle" />
<div class="row router-container">
<div class="col router-row">
<router-view/>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import Vuex from 'vuex'
import router from '#/router'
import axios from 'axios'
import AxiosStorage from 'axios-storage'
let sessionCache = AxiosStorage.getCache('localStorage');
import materializecss from '../static/css/main.css'
import materializejs from '../static/materialize-css/dist/js/materialize.js'
import navbar from '#/components/navbar'
import sidenav from '#/components/sidenav'
Vue.use(Vuex)
const state = {
sidenav:{
show: false
}
}
const mutations = {
show_sidenav(state){
state.sidenav.show = true
},
hide_sidenav(state){
state.sidenav.show = false
}
}
const store = new Vuex.Store({
state,
mutations
})
export default {
router,
name: 'App',
watch:{
$route: function(){
if(this.$route.path === '/login' || this.$route.path === '/logout' || this.$route.path === '/register'){
store.commit('hide_sidenav')
console.log('not authd')
}else{
store.commit('show_sidenav')
console.log('authd')
}
},
deep: true,
immediate: true
},
computed: {
sidenav_toggle(){
return store.state.sidenav.show
}
},
data: function(){
return{
}
},
components: {
navbar,
sidenav
},
methods: {
},
created: function(){
}
}
</script>
Your watcher is not called if you land directly on the admin page because the $route property never changes (and watchers only watch for changes).
What you could do is move your watcher function in a method, and call this method in the created hook and in your watcher.
An even better way to do this would be to use vue-router navigation-guards
Example:
export default {
// ...
methods: {
adaptSidebar(path) {
if (['/login', '/logout', '/register'].includes(path)) {
store.commit('hide_sidenav')
} else {
store.commit('show_sidenav')
}
},
},
beforeRouterEnter(from, to, next) {
// As stated in the doc, we do not have access to this from here
next(vm => {
vm.adaptSidebar(to.path)
})
},
beforeRouteChange(from, to, next) {
this.adaptSidebar(to.path)
},
}

Passing data-attribute from Vue instance html tag

Is it possible to declare and pass a data-attribute value from a html tag of the Vue instance, and then have it available in the data object?
index.html:
<div id="app" data-title="My app title"></div>
App.vue:
data () {
return {
appTitle: // whatever is declared in data-title
}
}
This code works for me:
index.html:
<div id="app" data-id="123"></div>
index.js:
(function (el) {
new Vue({
el,
render: h => h(Module),
data: () => Object.assign({}, el.dataset) ,
});
})(document.getElementById('app'));
Module.vue:
export default {
name: 'Module',
data() {
return {
id: this.$parent.id,
};
},
};
Yes it is:
data () {
return {
appTitle: document.getElementById('app').dataset.title
}
}
However, it is possible that the DOM is not available on component initialization. So you should probably put that code into the mounted hook of your component:
<script>
export default {
data () {
return {
appTitle: null
}
},
mounted () {
this.appTitle = document.getElementById('app').dataset.title
}
}
</script>
Here's a different approach that doesn't rely on the DOM API, but cannot be used to get data-attributes from the root (#app) element:
{
el: '#app',
template: `
<div ref="mydiv" data-attribute="data attribute">
Hello from template
<div>
Hello from {{attribute}}
</div>
</div>`,
data(){
return {
attribute: ''
}
},
mounted(){
this.$data.attribute = this.$refs.mydiv.dataset.attribute;
}
});
Here's a pen with a working example

vuejs emit from a component of a component to the parent

I'm using a component2 in my component1. I want to emit a parent function from component2 but only component1 is called in parent.
At the moment I'm emit in component2 and listening in component1. Then emit in component1 again and listening in parent.
My current code:
// Emit the toggle-filter in component2
Vue.component('component2', {
template: `<div #click="$emit('toggle-filter')"></div>`
});
// Capture it in component1, the emit again
Vue.component('component1', {
template: `<div>
<component2 #toggle-filter="toggleFilter"></component2>
</div>`,
methods: {
toggleFilter () {
this.$emit('toggle-filter');
},
}
});
// At last, capture it in parent
new Vue({
el: '#filters',
template: `<div id="filters">
<component1 #toggle-filter="toggleFilter"></component1>
</div>`,
methods: {
toggleFilter () {
console.log('filter toggled');
}
}
});
What I want to achieve is something like below.
// Emit toggle-filter in component2
Vue.component('component2', {
template: `<div #click="$emit('toggle-filter')"></div>`
});
Vue.component('component1', {
template: `<div>
<component2></component2>
</div>`
});
// Then capture it in parent
new Vue({
el: '#filters',
template: `<div id="filters">
<component1 #toggle-filter="toggleFilter"></component1>
</div>`,
methods: {
toggleFilter () {
console.log('filter toggled');
}
}
});
Starting from Vue 2, methods related to cross-component communication has been removed, and it's recommended to centralise data and manage shared state using Vuex. If the example pattern occurs a lot in your app, consider to use Vuex instead.
Vue still allows to create an event hub and let components communicate through the hub globally. But remember that it's considered anti-pattern.
var eventHub = new Vue()
Vue.component('component2', {
template: `<div #click="toggleFilter"></div>`,
methods: {
toggleFilter() {
eventHub.$emit('toggle-filter')
}
}
});
Vue.component('component1', {
template: `<div><component2 ></component2></div>`
}
new Vue({
el: '#filters',
template: `<div id="filters"><component1></component1></div>`,
created() {
eventHub.$on('toggle-filter', this.toggleFilter)
},
beforeDestroy() {
eventHub.$off('toggle-filter', this.toggleFilter)
},
methods: {
toggleFilter () {
console.log('filter toggled');
}
}
});

How to pass property to component used in main.js from other components in vuejs

Basically I want to a loadingbar component globally (included in app template)
Here is my loadingbar component
<template>
<div class="loadingbar" v-if="isLoading">
Loading ...
</div>
</template>
<script>
export default {
name: 'loadingbar',
props: ['isLoading'],
data () {
return {
}
}
}
</script>
<style scoped>
</style>
and in main.js, I have included this component as
import LoadingBar from './components/LoadingBar.vue';
new Vue({
router,
data () {
return {
isLoading: true
};
},
methods: {
},
created: function () {
},
components: {
LoadingBar
},
template: `
<div id="app">
<LoadingBar :isLoading="isLoading"/>
<router-view></router-view>
</div>
`
}).$mount('#app');
My aim is to show loading component based upon the value of variable isLoading. The above code working fine. But I want to use set isLoading variable from other component (so that to decide whether to show loading component). Eg. In post components
<template>
<div class="post container">
</div>
</template>
<script>
export default {
name: 'post',
data () {
return {
posts: []
}
},
methods: {
fetchPosts: function() {
// to show loading bar
this.isLoading = true;
this.$http.get(APIURL+'listpost')
.then(function(response) {
// to hide loading bar
this.isLoading = false;
console.log("content loaded");
});
}
},
created: function() {
this.fetchPosts();
}
}
</script>
<style scoped>
</style>
Of coarse we can't access isLoading directly from main.js so i decided to use Mixin so i put following code in main.js
Vue.mixin({
data: function () {
return {
isLoading: false
};
}
});
This however allow me to access isLoading from any other component but I can't modify this variable. Can any help me to achieve this?
Note: I know i can achieve this by including loadingbar in individual component (I tried that and it was working fine, But i do not want to do that as loadingbar is needed in every component so i was including in main template/component)
You could use Vuex like so:
// main.js
import Vuex from 'vuex'
let store = new Vuex.Store({
state: {
isLoading: false,
},
mutations: {
SET_IS_LOADING(state, value) {
state.isLoading = value;
}
},
getters: {
isLoading(state) {
return state.isLoading;
}
}
})
import LoadingBar from './components/LoadingBar.vue';
new Vue({
router,
store, // notice you need to add the `store` var here
components: {
LoadingBar
},
template: `
<div id="app">
<LoadingBar :isLoading="$store.getters.isLoading"/>
<router-view></router-view>
</div>
`
}).$mount('#app');
// script of any child component
methods: {
fetchPosts: function() {
// to show loading bar
this.$store.commit('SET_IS_LOADING', true);
this.$http.get(APIURL+'listpost')
.then(function(response) {
// to hide loading bar
this.$store.commit('SET_IS_LOADING', false);
console.log("content loaded");
});
}
},