I've faced counter-intuitive vue router behavior and want to know what I'm missing.
Here's a code for demonstration
// main.js
import Vue from "vue";
import Router from "vue-router";
import FirstPage from "#/components/FirstPage";
import SecondPage from "#/components/SecondPage";
import App from "./App.vue";
const routes = [
{
path: "/",
component: FirstPage
},
{
path: "/second",
component: SecondPage
}
];
const router = new Router({
mode: "history",
routes
});
Vue.use(Router);
new Vue({
render: (h) => h(App),
router
}).$mount("#app");
// App.vue
<template>
<router-view />
</template>
<script>
export default {};
</script>
// FirstPage.vue
<template>
<div>
<h1>first page</h1>
<router-link to="/second">second</router-link>
</div>
</template>
<script>
export default {
created() {
console.log("first created", this.$route.path);
},
destroyed() {
console.log("first destroyed", this.$route.path);
},
};
</script>
// SecondPage.vue
<template>
<div>
<h1>second page</h1>
<router-link to="/">home</router-link>
</div>
</template>
<script>
export default {
created() {
console.log("second created", this.$route.path);
},
destroyed() {
console.log("second destroyed", this.$route.path);
},
};
</script>
When navigating from the first page to second I expect logs like
first created /
first destroyed /
second created /second
But instead I get
first created /
second created /second
first destroyed /second
Codesandbox
I.e. second page component is created BEFORE the first one is destroyed. So the first component in destroyed hook has access to the another $route, which is wrong in my opinion. Why does it happen this way? Thanks in advance!
This behavior is generated because the component lifecycle hooks are merged with the In-Component Guards from the router :
first component :
created() {
console.log("first created", this.$route.path);
},
beforeRouteLeave(to, from, next) {
console.log("leaving first");
// this runs before running the destroyed hook
},
destroyed() {
console.log("first destroyed", this.$route.path);
},
second component :
beforeRouteEnter (to, from, next) {
console.log("creating the second");
// this guard creates the second component before running the beforeRouteLeave from
// the first one which will executed and then the destroyed hook is executed
},
created() {
console.log("second created", this.$route.path);
},
destroyed() {
console.log("second destroyed", this.$route.path);
},
Related
This is my first time learning to use vuex and I'm building a simple authorization with hardcoded value. I want to redirect user to Products.vue if the entered password in Confirm.vue matches.
Products.vue will contain the secured content that the user cannot access before entering the correct password.
The result keeps on giving error of
uncaught TypeError: Cannot read property 'commit' of undefined in my Confirm.vue file
Confirm.vue
<template>
<div>
<p>Please enter password</p>
<input
type="password"
name="password"
v-model="input.password"
placeholder="Password"
/>
<button type="button" v-on:click="login()">Go to admin</button>
</div>
</template>
<script>
export default {
name: "Confirm",
data() {
return {
input: {
password: "",
},
};
},
methods: {
login() {
if (this.input.password == "123") {
this.$store.commit("setAuthentication, true");
this.$router.replace({ name: "products" });
} else {
console.log("incorect password");
}
},
},
};
</script>
So my assumption is that $store is not defined, but I already set the constant for it in my Main.js and also created a new Vue.
Main.js
import Vue from "vue";
import Vuex from "vuex";
import App from "./App.vue";
import VueRouter from "vue-router";
import Confirm from "./pages/admin/Confirm.vue";
import Products from "./pages/admin/Products.vue";
Vue.config.productionTip = false;
Vue.use(VueRouter);
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
authenticated: false,
},
mutations: {
setAuthentication(state, status) {
state.authenticated = status;
},
},
});
const router = new VueRouter({
routes: [
{
path: "/",
redirect: {
name: "main",
},
},
{
path: "/admin/confirm",
name: "confirm",
component: Confirm,
},
{
path: "/admin/products",
name: "products",
component: Products,
},
],
});
new Vue({
render: (h) => h(App),
router: router,
store: store,
}).$mount("#app");
Lastly, this is the content of App.vue
App.vue
<template>
<router-view />
</template>
<script>
export default {
name: "app",
};
</script>
You are calling the mutation wrong.
You are using this syntax -
this.$store.commit("setAuthentication, true");
Instead, you should use this
this.$store.commit("setAuthentication", true);
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)
},
}
Using vue-router in a single page application with the code below, the watch $route function in not firing when redirecting to mycomponent.
Also the beforeRouteUpdate in mycomponent is also not firing.
How can I detect when a variable has been tagged on to a route during component load?
App.vue
<template>
<router-view></router-view>
</template>
<script>
import Vue from 'vue'
export default {
name: 'app'
}
</script>
index.js
import Vue from 'vue'
import Router from 'vue-router'
import MyView from '#/views/MyView'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
redirect: '/home',
name: 'Home',
children: [
{
path: '/mycomponent',
name: 'MyComponent',
component: MyComponentView
},
{
path: '/mycomponent/:id',
component: MyComponentView,
props: true
}
]}]})
mycomponent.vue
<template>
<component :is="activeComponent" :id="id"></component>
</template>
<script>
export default {
name: 'MyComponentView',
components: {
...
},
mounted: function() {
#this logs path in browser
console.log('>>mounted route: ' + this.$route.path)
},
watch: {
'$route': function () {
#this does not fire
console.log('route watcher: ' + this.$route.path)
}
},
beforeRouteUpdate (to, from, next) {
#this does not fire
console.log('>>beforeRouteUpdate')
},
data () {
return {
activeComponent: 'somecomponent'
}
}
}
</script>
component1.vue
...
mounted: function() {
Event.$on('vue-tables.row-click', function(data) {
#this logs correct information in browser
console.log('data.row.id: ' + data.row.id)
router.push({path: 'mycomponent', query: {id: data.row.id}})
})
},
...
It doesn't work because beforeRouteUpdate is in component which is going to reload (Look at Life cycle of Vue). When you change the route, watch & beforeRouteUpdate is terminated and you won't see any results. In this scenario you should provide something like this:
MainRouterView.vue
<template>
<router-view/>
</template>
<script>
name: 'MainRouterView',
beforeRouteUpdate (to, from, next) {
console.log('beforeRouteUpdate')
},
</script>
router.js
export default new Router({
routes: [
{
{
path: '/mycomponent',
name: 'MainRouterView',
component: MainRouterView,
children: [
{
path: '/mycomponent/:id',
component: SecondComponent,
}
]
},
}]})
But if you want to stick up with your structure and check the status of the current route, you can replace beforeRouteUpdate to beforeRouteEnter or beforeRouteLeave in the component. You can use global guard beforeEach in router as well.
To better understand how beforeRouteUpdate works, check out this snippet: http://jsfiddle.net/yraqs4cb/
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>
I have this page I want to try out Vue Router with Vue Components. I cant figure out whats wrong. I am getting an error Uncaught TypeError: Cannot read property 'name' of undefined at this line const App = new Vue.extend({})
<body>
<div id="app">
<router-view></router-view>
</div>
<template id="foo"> <h1>This is homepage</h1> </template>
<template id="bar"> <h1>This is Bar page</h1> </template>
</body>
//Vue.js v1.0.28
<script src="{{ asset( 'js/vue.js' ) }}"></script>
// vue-router v0.7.13
<script src="{{ asset( 'js/vue-router.js' ) }}"></script>
<script>
const router = new VueRouter()
const App = new Vue.extend({})
router.map({
'/': {
component: {
template: '#foo'
}
},
'/bar': {
component: {
template: '#bar'
}
},
})
router.start(App, '#app')
</script>
</html>
What am I doing wrong?
EDIT:
Okay, I have managed to get this working.
const Foo = Vue.component('foo', { template: '#foo' });
const Bar = Vue.component('bar', { template: '#bar' });
Vue.use(VueRouter)
const router = new VueRouter()
router.map({
'/foo': {
component: Foo
},
'/bar': {
component: Bar
},
})
var App = Vue.extend({})
router.start(App, 'body')
What I need now is to extract those templates from index.blade.php into their own files like Foo.vue and Bar.vue. How do I do that?
I am using Webpack to compile assets.
You could try to use vue-router v.v2.2.1 and you can check this official example https://github.com/vuejs/vue-hackernews-2.0 and here router code:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
import { createListView } from '../views/CreateListView'
import ItemView from '../views/ItemView.vue'
import UserView from '../views/UserView.vue'
export default new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [
{ path: '/top/:page(\\d+)?', component: createListView('top') },
{ path: '/new/:page(\\d+)?', component: createListView('new') },
{ path: '/show/:page(\\d+)?', component: createListView('show') },
{ path: '/ask/:page(\\d+)?', component: createListView('ask') },
{ path: '/job/:page(\\d+)?', component: createListView('job') },
{ path: '/item/:id(\\d+)', component: ItemView },
{ path: '/user/:id', component: UserView },
{ path: '/', redirect: '/top' }
]
})