Vue Watch not Triggering on Page Refresh/Reload - vue.js

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)
},
}

Related

Vuejs 3 Stuck at loading page

Hello I have a problem showing/loading page. Some routes are working, and those routes that work are calling API for data, the ones that not work are not calling API for data.
So I am calling API for data(I don't have to, and I don't want to), and then the page loads.
I am watching the selected language computed:{lang(){...}} because in some situations I need to get translated data from API.
Here is the code with added API call.
asyncDataStatus.js
export default{
data(){
return{
asyncDataStatus_ready: false
}
},
methods: {
asyncDataStatus_fetched(){
this.asyncDataStatus_ready = true
this.$emit('ready')
}
},
}
App.vue
<template>
<router-view v-show="showPage" #ready="onPageReady" />
</template>
<script>
export default {
import asyncDataStatus from '#/mixins/asyncDataStatus'
export default {
methods:{
onPageReady(){
this.showPage = true
NProgress.done()
}
},
mixins: [asyncDataStatus],
created(){
NProgress.configure({
speed: 200,
spinner: false
})
this.$router.beforeEach(() => {
this.showPage = false
NProgress.start()
})
}
}
</script>
Contact.vue
<template>
<router-view v-show="showPage" #ready="onPageReady" />
</template>
<script>
import store from '../store';
import asyncDataStatus from '#/mixins/asyncDataStatus'
export default {
mixins: [asyncDataStatus],
computed: {
lang() {
return this.$i18n.locale
},
watch: {
async lang(newLang, oldLang) {
if(newLang !== oldLang) {
// If I remove this line `store.dispatch` line (which is not needed in this file), then `this.asyncDataStatus_fetched()` won't happen
await store.dispatch('impressionModule/getImpressions')
this.asyncDataStatus_fetched()
}
},
created() {
this.asyncDataStatus_fetched()
}
}
}
</script>

How to get vuex pagination to be reactive?

I am not sure what the problem is. I am creating a Vue/Vuex pagination system that calls my api to get my list of projects. The page initially loads all the projects in when the page is mounted. The Vuex does the inital call with axios. Vuex finds the projects and the number of pages. Once the user clicks on the pagination that is created with the pagination component, it should automatically change the projects for page 2...3 etc.
The problem I have is that it is not reactive until you press the page number twice. I am using vue 3. I have tried not using Vuex and that was successful. I am trying to create a single store that does all the axios calls. Thanks for all your help!
Vuex store
import { createStore } from 'vuex';
import axios from 'axios';
/**
* Vuex Store
*/
export const store = createStore({
state() {
return {
projects: [],
pagination: {}
}
},
getters: {
projects: state => {
return state.projects;
}
},
actions: {
async getProjects({ commit }, page = 1) {
await axios.get("http://127.0.0.1:8000/api/guest/projects?page=" + page)
.then(response => {
commit('SET_PROJECTS', response.data.data.data);
commit('SET_PAGINATION', {
current_page: response.data.data.pagination.current_page,
first_page_url: response.data.data.pagination.first_page_url,
prev_page_url: response.data.data.pagination.prev_page_url,
next_page_url: response.data.data.pagination.next_page_url,
last_page_url: response.data.data.pagination.last_page_url,
last_page: response.data.data.pagination.last_page,
per_page: response.data.data.pagination.per_page,
total: response.data.data.pagination.total,
path: response.data.data.pagination.path
});
})
.catch((e) => {
console.log(e);
});
}
},
mutations: {
SET_PROJECTS(state, projects) {
state.projects = projects;
},
SET_PAGINATION(state, pagination) {
state.pagination = pagination;
}
},
});
Portfolio Component
<template>
<div>
<div id="portfolio">
<div class="container-fluid mt-5">
<ProjectNav></ProjectNav>
<div class="d-flex flex-wrap overflow-auto justify-content-center mt-5">
<div v-for="project in projects" :key="project.id" class="m-2">
<Project :project="project"></Project>
</div>
</div>
</div>
</div>
<PaginationComponent
:totalPages="totalPages"
#clicked="fetchData"
></PaginationComponent>
</div>
</template>
<script>
import Project from "../projects/Project.vue";
import PaginationComponent from "../pagination/PaginationComponent.vue";
import ProjectNav from "../projectNav/ProjectNav.vue";
/**
* PortfolioComponent is where all the projects are displayed.
*/
export default {
name: "PortfolioComponent",
data() {
return {
location: "portfolio",
projects: []
};
},
components: {
Project,
PaginationComponent,
ProjectNav,
},
mounted() {
this.fetchData(1);
},
computed: {
totalPages() {
return this.$store.state.pagination.last_page;
},
},
methods: {
fetchData(page) {
this.$store.dispatch("getProjects", page);
this.projects = this.$store.getters.projects;
},
},
};
</script>
In your fetchData method you are calling the async action getProjects, but you are not waiting until the returned promise is resolved.
Try to use async and await in your fetchData method.
methods: {
async fetchData(page) {
await this.$store.dispatch("getProjects", page);
this.projects = this.$store.getters.projects;
},
},

Vue router and component lifecycle hooks

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);
},

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");
});
}
},

Changing a vuex state from a different component?

I have a component (modal) which relies on a store. The store has the state of the modal component - whether it is active or not.
I need to be able to call this modal to open from other components or even just on a standard link. It opens by adding an .active class.
How can I change the state of the store - either by calling the stores action or calling the modal components method (which is mapped to the store).
Modal Store:
class ModalModule {
constructor() {
return {
namespaced: true,
state: {
active: false,
},
mutations: {
toggleActive: (state) => {
return state.active = ! state.active;
},
},
actions: {
toggleActive({ commit }) {
commit('toggleActive');
},
},
getters: {
active: state => {
return state.active;
}
}
};
}
}
export default ModalModule;
Vue Component:
<template>
<div class="modal" v-bind:class="{ active: active }">
<div class="modal-inner">
<h1>modal content here</h1>
</div>
<div class="modal-close" v-on:click="this.toggleActive">
X
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
export default {
computed: {
...mapGetters('Modal', [
'active',
])
},
methods: {
...mapActions('Modal', [
'toggleActive',
]),
}
}
</script>
And somewhere else I want to be able to have something like:
<button v-on:click="how to change the state??">OPEN MODAL</button>
Edit:
Here's the store:
import Vuex from 'vuex';
import ModalModule from './ModalModule';
class Store extends Vuex.Store {
constructor() {
Vue.use(Vuex);
super({
modules: {
Modal: new ModalModule(),
}
});
};
}
You do not need an action for your particular usecase . You just just define a mutation as you are just changing the boolean value of a property in a state. Actions are for async functionality. You usecase is just synchronous change of Boolean value
So you can do
<button v-on:click="$store.commit('toggleActive')">OPEN MODAL</button>
EDIT:
Just export a plain object
const ModalModule = {
namespaced: true,
state: {
active: false,
},
mutations: {
toggleActive: (state) => {
return state.active = ! state.active;
},
},
actions: {
toggleActive({ commit }) {
commit('toggleActive');
},
},
getters: {
active: state => {
return state.active;
}
}
}
export default ModalModule;// export the module
Even remove the class based definition of the store
import Vue from 'vue'
import Vuex from 'vuex';
import ModalModule from './ModalModule';
Vue.use(Vuex);
export const store = new Vuex.Store({
modules: {
ModalModule
}
});
And change it like this in you component for mapping of the mutation (<MODULE_NAME>/<MUTATION_NAME>)
...mapMutations([
'ModalModule/toggleActive'
])
You can access the store from your components via this.$store. There you can call your actions and mutations. So
<button v-on:click="$store.commit('your mutation name', true)">OPEN MODAL</button>