I'm trying to create a simple reactive navigation based on if a user is authenticated or not. A login method on a login view sets a token in localstorage. If set, I want to display the logout button. I've tried computed, standard methods and props to no avail.
When I login, the navigation does not update. HOWEVER, if I refresh the page (reinitialize the app), it does show the logout button.
What exactly am I doing incorrectly?
I have been trying for hours to grasp the concept of Vue JS and am on the brink of quitting. What would have taken me minutes to do server side has taken me hours client side. WHERE IS THE REACTIVITY?
Nav.vue
<template>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="#">App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarColor01" aria-controls="navbarColor01" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarColor01">
<ul class="navbar-nav">
<li class="nav-item active">
<a class="nav-link" href="#">
<router-link to="/">Home</router-link>
<span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<router-link to="/about" class="nav-link">About</router-link>
</li>
</ul>
<ul class="navbar-nav ml-auto">
<li class="nav-item" v-if="hasAuth()"><a #click="logout()" class="nav-link">Log Out</a></li>
<template v-else>
<li class="nav-item">
<router-link to="/register" class="nav-link">Register</router-link>
</li>
<li class="nav-item">
<router-link to="/login" class="nav-link">Login</router-link>
</li>
</template>
</ul>
</div>
</nav>
</template>
<script>
export default {
name: 'Nav',
data: () => {
return {
auth: false
}
},
methods: {
logout: function () {
localStorage.removeItem('user-token');
this.$router.push({ path: 'login' });
},
hasAuth: function () {
this.auth = (localStorage.getItem('user-token')) ? true : false;
return this.auth
}
},
};
</script>
App.vue
<template>
<div id="app">
<Nav></Nav>
<router-view/>
</div>
</template>
<script>
import Nav from '#/components/Nav.vue';
export default {
components: {
Nav,
},
}
</script>
While Vue.js is reactive, localStorage is not. Vue cannot possibly know if the localStorage is modified or not. There is no local change event available with local storage.
To solve this problem, use Vuex combined with Local Storage for persistent data. The point where you save the token to local storage, at that time also save a copy inside Vuex store.
Another component like Nav should read data from Vuex store which is reactive. When you refresh the page, initialize Vuex store with the data from localStorage.
This way you get a perfect reactive system.
Related
When I log out I delete the token. Here is how I check if the token is deleted:
const isLoggedOut = () => {
let token = window.localStorage.getItem('token')
if (token === undefined || token === null || token.length === 0) {
console.log("user is logged out")
return true
}
else {
console.log("user is logged in")
return false
}
}
Inside the navbar I want the Login button to be hidden if the user is logged in.
This is how the navbar looks like:
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<router-link class="navbar-brand" :to="{ name: 'Home' }">Home</router-link>
<ul class="navbar-nav">
<li class="nav-item">
<router-link class="text-white" :to="{ name: 'Login' }" v-if="isLoggedOut">Login</router-link>
</li>
<li class="nav-item">
<router-link class="text-white ml-2" :to="{ name: 'Register' }">Register</router-link>
</li>
<li class="nav-item">
<router-link class="text-white ml-2" :to="{ name: 'Dashboard' }">Dashboard</router-link>
</li>
<li class="nav-item">
<button class="text-white ml-2" #click="logOut">Logout</button>
</li>
</ul>
</nav>
Later edit, this is the App.vue file without the styles. I log in and Login button still appears in the navbar.
<script setup>
import { reactive,ref, computed } from 'vue'
import { useRouter } from "vue-router"
const router = useRouter()
const token = ref(null);
const setToken = (token) => {
token.value = token;
window.localStorage.setItem('token', token)
}
const clearToken = () => {
token.value = null;
window.localStorage.removeItem('token')
}
const isLoggedOut = computed(() => token.value === null)
const logOut = () => {
clearToken()
router.push({name:'Login'})
}
</script>
<template>
<nav class="navbar navbar-expand-sm bg-dark navbar-dark">
<router-link class="navbar-brand" :to="{ name: 'Home' }">Home</router-link>
<ul class="navbar-nav">
<!-- Show Login button only if user is logged out -->
<li class="nav-item">
<router-link class="text-white" :to="{ name: 'Login' }" v-if="isLoggedOut">Login</router-link>
</li>
<li class="nav-item">
<router-link class="text-white ml-2" :to="{ name: 'Register' }">Register</router-link>
</li>
<li class="nav-item">
<router-link class="text-white ml-2" :to="{ name: 'Dashboard' }">Dashboard</router-link>
</li>
<li class="nav-item">
<button class="text-white ml-2" #click="logOut">Logout</button>
</li>
</ul>
</nav>
<router-view/>
</template>
window.localStorage.getItem('token') is not reactive. If the value changes, you will not be notified.
Instead of getting logged in state from localStorage, you can manage it through internal state which will then manage the local storage.
for example:
const token = ref(null);
const setToken = (value) => {
token.value = value;
window.localStorage.setItem('token', token)
}
const clearToken = () => {
token.value = null;
window.localStorage.removeItem('token')
}
const isLoggedIn = computed(()=>token.value !== null)
then you can use isLoggedIn as a reactive (computed) value.
You can store this in a separate auth.js and export the methods and token.
example
The button is still visible because the component (App.vue in your case) is not reloaded, meaning the check whether you have a token or not is not performed, respectively the computed is not going to be updated because as mentioned above window.localStorage is not reactive.
One way to overcome this is to use a store (pinia/vuex/composable that will hold the value all the time) and router hooks, like beforeRouteEnter or the global one - beforeEach.
You can also try to make localStoragereactive like in this example here
Which might be overkill.
Just make sure you moved token functions and operations in a separated file (or even as a composable)
I am new to Vue and straggling with generating sidebar list where each list element on click open its components. I am using vue router. I understand the theory how it should work, but obviously I am missing something because I can't solve it. I don't know how to dynamically change the path.
I generated sidebar list using router-link
<template>
<div class="sidebarListItems">
<router-link href="#"
:to="calcRut"
class="list-group-item list-group-item-action bg-light"
v-for="title in mapTitles"
:key="title.map"
:title="title.map"
#click="callMap">
{{title.map}}
</router-link>
</div>
</template>
<script>
import bus from "./js/bus"
import mapNames from "./json/popis_karata.json"
export default {
name: "PageSidebarList",
props: ["title"],
data(){
return {
mapTitles:mapNames,
ruth=""
}
},
methods: {
callMap(event){
bus.$emit("openMap")
},
calcRuth(){
for (var i=0; i<this.routes.length; i++){
var newruth = this.routes[i].path
this.ruth = newruth
}
}
},
computed: {
routes(){
return this.$router.options.routes
}
}
When I put my path directly as a string (:to="/zup" or :to="/reg") it's working, but I would like that those paths are dynamically generated depending on which list element I clicked.
Here's how I do it. Try extracting the v-for on the level above. If you don't want to use an actual element, try <template>
<ul class="flex flex-col w-full space-y-1">
<li v-for="item in items" :key="item.link">
<router-link class="flex items-center h-8 px-4 rounded-lg hover:bg-white" :class="{ 'bg-white': routeMatches(item) }" :to="{ name: item.link }">
<div class="text-sm text-gray-700">{{ item.name }}</div>
</router-link>
</li>
</ul>
Edit, also format your to="" correctly to be :to="{name: 'namehere'}"
how about this solution with a switch and a programmatic router change
<template>
<div id="app">
<ul class="nav">
<li class="nav-item" #click="routeChanger('home')">Home</li>
<li class="nav-item" #click="routeChanger('page1')">Page1</li>
<li class="nav-item" #click="routeChanger('page2')">Page2</li>
<li class="nav-item" #click="routeChanger('page3')">Page3</li>
<li class="nav-item" #click="routeChanger('page4')">Page4</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
methods: {
routeChanger(routeParam) {
switch (routeParam) {
case "home":
this.$router.push("/home");
break;
case "page1":
this.$router.push("/page1");
break;
//...
default:
this.$router.push("/404");
break;
}
}
}
};
</script>
<style>
</style>
Maybe my answer would be useful to someone so I am posting my solution.
I didn't found solution to loop separately navbar elements from one data file and router-lik :to path attribute from routes file.
It work if I use routes file for everything.
<router-link
v-for="route in $router.options.routes"
:key="route.path"
:to="route.path"
class="list-group-item list-group-item-action bg-light">
{{route.title}}
</router-link>
I am building a single page application using vue.js with materialize and I have a navbar with tabs to navigate to different pages on my website. Materialize's tabs has an active property that displays which tab is currently selected. Every thing this far works perfectly.
This is materialize's tabs reference
The Issue
If you refresh the web page the tabs active property 'resets' to its default position. So I am trying to figure out how to persist the state of the navbar's active property and then reassign it to router-link.
My Code
<ul id="tabs" class="tabs tabs-transparent" :class="{ 'navbar--color': !changeNavColor }">
<li class="tab" id="home">
<router-link
id="home"
class="link"
:class="{ 'navbar--color': !changeNavColor }"
to="/"
>Home</router-link>
</li>
<li class="tab" id="services">
<router-link
id="services"
class="link"
:class="{ 'navbar--color': !changeNavColor }"
to="Services"
>Service</router-link>
</li>
<li class="tab" id="Preapproved">
<router-link
id="Preapproved"
class="link"
:class="{ 'navbar--color': !changeNavColor }"
to="PreApproved"
>Get Pre-Approved</router-link>
</li>
<li class="tab">
<router-link
id="cars"
class="link"
:class="{ 'navbar--color': !changeNavColor }"
to="Cars"
>inventory</router-link>
</li>
<li class="tab">
<router-link
id="testimonials"
class="link"
:class="{ 'navbar--color': !changeNavColor }"
to="Testimonials"
>Testimonals</router-link>
</li>
</ul>
if (localStorage.currentTab) {
this.currentTab = localStorage.currentTab;
var activeTab = document.getElementById(this.currentTab);
activeTab.classList.add("active");
console.log("saved");
}
$(document).ready(function() {
$("a").click(function(event) {
this.currentTab = event.target.id;
console.log(this.currentTab);
});
});
$(document).ready(function() {
$(".tabs").tabs();
});
window.addEventListener("scroll", this.onScroll);
},
beforeDestroy() {
window.removeEventListener("scroll", this.onScroll);
},
watch: {
currentTab(newTab) {
console.log("watch");
localStorage.currentTab = this.currentTab;
}
}
any help is appreciated
So I think you can use hash mechanism by using window.location.hash. You can put the data-toggle attribute in every tag anchor. And just check for location.hash !== ''
But the more Vue way would be through client side storage. Check the link its from vue docs. Although it is for the input field but you will get the idea of how to persist info by keeping a reference in client storage and on reload getting your desired state from there onward. Hope it helps.
Remove the indicator that is nested inside the first li, and the navigation works fine.
<ul id="tabs" class="tabs tabs-transparent">
<li id="tab" class="tab">
Home
<li class="indicator"></li> <!-- THIS RANDOM NESTED INDICATOR IS THE ISSUE -->
</li>
<li id="tab" class="tab">
Service
</li>
<li id="tab" class="tab">
Get Pre-Approved
</li>
<li id="tab" class="tab">
inventory
</li>
<li class="indicator" style="right: 488px; left: 0px;"></li> <!-- CORRECT INDICATOR, AUTOMATICALLY GENERATED BY MATERIALIZE -->
</ul>
This is how I ended up implementing this. First all of your router-links or <a> have to have unique id's
mounted() {
if (localStorage.currentTab) {
this.currentTab = localStorage.currentTab;
var element = document.querySelector(this.currentTab);
element.classList.add("active");
}
$(document).ready(function() {
$(".tabs").tabs();
$("a").click(function(event) {
this.currentTab = "#" + event.target.id;
localStorage.currentTab = this.currentTab;
});
});
},
watch: {
currentTab(newTab) {
localStorage.currentTab = this.currentTab;
}
}
Here is my replies.vue file. Here what i am doing is when this replies component is created, i call a fetch() method that loads some data from the server:
//Replies.vue
<template>
<div>
<div v-for="(reply, index) in replies_with_pagination.data">
<reply :attributes="reply" :key="reply.id" :current_user_id="auth_user_id" #iamdeleted="updateCollection(index)"></reply>
</div>
<pagination :current_paginated_dataSet="replies_with_pagination"></pagination>
</div>
</template>
<script>
import Reply from './Reply.vue';
export default{
components: { Reply },
props: ['all_replies','auth_user_id','thread_id'],
data() {
return {
all_reply_items: this.all_replies,
replies_with_pagination: '',
}
},
methods: {
fetch: function(){
let vm = this;
axios.get(route('replies.paginated',{ 'thread' : this.thread_id
}))
.then(function(serverResponse){
vm.replies_with_pagination = serverResponse.data;
});
},
},
created() {
this.fetch();
}
And then i am passing that to the pagination component via the current_paginated_dataSet props. Here is the code for pagination.vue
//Pagination.vue
<template>
<div>
<nav aria-label="...">
<ul class="pagination">
<li class="page-item disabled" v-show="current.prev_page_url">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item active">
<a class="page-link" href="#">2 <span class="sr-only">(current)</span></a>
</li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item" v-show="current.next_page_url">
<a class="page-link" href="#">Next</a>
</li>
</ul>
</nav>
</div>
</template>
<script>
export default{
props: ['current_paginated_dataSet'],
data() {
return {
current: this.current_paginated_dataSet,
}
},
}
</script>
As you can see i am using this property to initialize a data called current in the pagination component. This current_paginated_dataSet property was initialized at the replies component to the value of FALSE, which, upon fetching the data from server was then reset to some object returned by the server.
My problem is the previous and next buttons on the pagination component are not visible. It seems the current data property on the pagination component is not getting the updated value from the replies component which updates after loading data from server. It is always set to the intial value of FALSE that it received from parent root component.
What am i doing wrong here?
EDIT
it works if instead of referring to the current data property i directly refer to the current_paginated_dataSet props in the previous and next buttons, like so:
// this works:
<li class="page-item disabled" v-show="current_paginated_dataSet.prev_page_url">
<a class="page-link" href="#" tabindex="-1">Previous</a>
</li>
But i want to achieve this by referring to the data property current. How can i do that?
I'm just learning angularjs and working on my first "real" project. I've spent a couple hours searching online for an answer, but can't seem to find anything relevant in simple enough terms for me to understand.
I have a directive that just loads a navigation header:
app.directive('navDirective', function() {
return {
templateUrl: '../../views/nav.html'
};
});
And in my index.html file I have simply
<nav-directive> </nav-directive>
Initially I was working with Auth0 and got that working, and then added an ng-show to the nav items so that only login shows when there's no user, and all the others appear when there is a user.
Now, I am trying to get local authentication working, instead of using Auth0. It works, but I have to hit the brower's reload button after logging in before I see the navigation template update.
I think I've figured out that when using Auth0, I actually leave my website to go to Auth0, and then the callback causes my page to reload - so I see my nav items.
When I login locally using passport's local strategy, how can I force the navigation template to reload? Or become aware there is now a user, so change the ng-show's on the list items in the template?
Reading tonight, it seems like I can use $watch to do this, but I can't find a "$watch-for-dummies" page and am confused as to how that works exactly, what I would put in the link function and how I would trigger it.
My nav.html template is
<nav class="navbar navbar-expand-lg navbar-dark bg-dark" ng-controller="NavCtrl">
<a class="navbar-brand" href="#" ng-show="currentUser">User: {{ currentUser.username }}</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse navbarCollapse justify-content-end" id="navbarNavDropdown">
<ul class="nav navbar-nav ">
<li class="nav-item" ng-show="currentUser">
<a class="nav-link nav-item-colapse" ui-sref="home">Home </a>
</li>
<li class="nav-item" ng-show="currentUser">
<a class="nav-link nav-item-colapse" ui-sref="view2">View 2</a>
</li>
<li class="nav-item" ng-show="currentUser">
<a class="nav-link nav-item-colapse" ui-sref="view3">View 3</a>
</li>
<li class="nav-item" ng-show="currentUser">
<a class="nav-link nav-item-colapse" ui-sref="reports">Reports</a>
</li>
<li class="nav-item dropdown" ng-show="currentUser">
<a class="nav-link dropdown-toggle" href="http://example.com" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Admin
</a>
<div class="dropdown-menu" style="right: 0; left: auto;" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item nav-item-colapse" ui-sref="sub1">Sub Task1</a>
<a class="dropdown-item nav-item-colapse" ui-sref="sub2">Sub Task2</a>
<a class="dropdown-item nav-item-colapse" href="http://www.google.com">Do Something Else</a>
</div>
</li>
<li class="nav-item">
<a class="nav-link active" ui-sref="login" ng-show="!currentUser">Login</a>
</li>
<li class="nav-item">
<a class="nav-link" ng-click="logout()" ng-show="currentUser">Logout</a>
</li>
</ul>
</div>
</nav>
and navCtrl is:
app.controller("navCtrl", function($scope, mainSrvc) {
mainSrvc.getUser().then(function(user){
$scope.user = (user.data);
console.log('nav ctrl ran') //except it doesn't when logging in locally
console.log(($scope.user));
});
});
And this point I'm not sure if I'm going about this completely wrong or what.
Any suggestions or pointers?
You can use ng-if="vm.isLoggedIn()" to know if a user is present or not.
<nav ng-controller="LoginController as vm" ng-if="vm.isLoggedIn()"
class="navbar navbar-inverse navbar-static-top" ng-init="vm.init()">
In your login controller return wheather a user is logged in or not after authentication
function LoginController($http, $location, $window, AuthFactory, jwtHelper) {
var vm = this;
vm.isLoggedIn = function () {
if (AuthFactory.isLoggedIn) {
return true;
}
else {
return false;
}
};
vm.login = function () {
if (vm.username && vm.password) {
var user = {
username: vm.username,
password: vm.password
};
$http.post('/api/users/login', user).then(function (response) {
if (response.data.success) {
$window.sessionStorage.token = response.data.token;
AuthFactory.isLoggedIn = true;
var token =$window.sessionStorage.token;
var decodedToken = jwtHelper.decodeToken(token);
vm.loggedInUser = decodedToken.username;
}
}).catch(function (error) {
console.log(error);
})
}
}