I'm making an application with Vue.js, currently working on the login/logout functionality. Basically I have in my navbar a button that is supposed to say Login when the user hasn't logged in and logout when the user session is active. The user session information is being stored in the browsers sessionStorage, so basically my question is, what's the best way to handle the communication between the components?
Here's my main component, which has a variable called "currentUser" that will pass to child components as prop for further usage.
<template>
<div id="application">
<div id="navbar">
<navbar :current-user.sync="currentUser"></navbar>
</div>
<router-view :current-user.sync="currentUser"></router-view>
</div>
</template>
<script>
var nav = require('./src/components/navbar.vue');
module.exports = {
components: {
'navbar': nav
},
ready: function(){
this.currentUser = {
userId: window.sessionStorage.getItem('userId'),
username: window.sessionStorage.getItem('user'),
scope: window.sessionStorage.getItem('scope')
}
},
data: function(){
return {
currentUser: {
userId: '',
username: '',
scope: ''
}
}
}
}
</script>
Then we have the navbar component that will basically show/display links according to the users permissions
<template>
<nav>
<div class="nav-wrapper blue darken-4">
DINAF
<ul id="nav-mobile" class="right hide-on-med-and-down">
<li>
<a v-link="'/organization/map'">Mapa de organizaciones</a>
</li>
<li>
<a v-link="'/'">Listar organizaciones</a>
</li>
<li >
<a v-link="{path: '/organization/new'}">Agregar organización</a>
</li>
<li v-if="!isLoggedIn">
<a v-link="{path: '/login'}">Login</a>
</li>
<li v-else>
<span>Welcome {{currentUser.scope}} </span><a v-on:click="logout()" class="waves-effect waves-light btn blue darken-1">Logout</a>
</li>
</ul>
</div>
</nav>
</template>
<script>
var Vue = require('vue');
var config = require('../../config.js');
module.exports = {
name: 'navbar',
props: ['currentUser'],
ready: function(){
this.isLoggedIn = this.currentUser.scope != ''
console.log(this.isLoggedIn)
},
methods: {
logout: function(){
this.$http.get(config.baseUrl() + '/v1/logout').then(function(response){
window.sessionStorage.removeItem('user');
window.sessionStorage.removeItem('userId');
window.sessionStorage.removeItem('scope');
this.currentUser = {
userId: '',
username: '',
scope: ''
}
this.$route.router.go('/');
},function(error){
console.log(error);
})
}
},
data: function(){
return {
isLoggedIn: false,
}
}
};
</script>
And the login component that logs in and saves the information into the local storage
<template >
<div class="loginContainer">
<div class="row">
<div class="col s6 offset-s3 ">
<div class="card blue-grey darken-2 white-text">
<div class="card-content">
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">email</i>
<input id="username" type="text" v-model="user.username" data-error="Ingrese su nombre de usuario" class="validate">
<label for="username">Username</label>
</div>
</div>
<div class="row">
<div class="input-field col s12">
<i class="material-icons prefix">lock</i>
<input id="password" type="password" v-model="user.password" class="validate">
<label for="password"><i class="fa fa-lock" aria-hidden="true"></i> Contraseña</label>
</div>
</div>
</div>
<div class="card-action">
<a type="submit" id="loginButton" class="waves-effect waves-light btn blue-grey darken-4" v-on:click="logIn">Iniciar Sesión</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
var swal = require('sweetalert');
var config = require('../../config.js');
module.exports = {
name: 'login',
props: ['currentUser'],
methods: {
logIn: function(){
this.$http.post(config.baseUrl() + '/v1/login', this.user).then(function(response){
this.currentUser = {
username: response.json().user,
userId: response.json()._id,
scope: response.json().scope
}
window.sessionStorage.setItem('user', this.currentUser.username);
window.sessionStorage.setItem('userId', this.currentUser.userId);
window.sessionStorage.setItem('scope', this.currentUser.scope);
console.log(this.currentUser)
this.$route.router.go('/');
},function(error){
swal('Error', 'Usuario o password incorrecto', 'error');
});
}
},
data: function(){
return {
user: {},
}
}
}
</script>
The problem I have right now is that when you log in, the sessionStorage is being updated but not the data that's on the navbar component so the login in the navbar never changes.
Related
I have a navbar and login components in vue. So my target is when if the user login, on the dropdown in navbar, I want to show Profile as an item, and if the user is not login I want to show Sign In/Register item as an item. So I want to manipulate the dropdown that you see on the picture below:
And here is my Navbar component:
<template>
<nav class="navbar navbar-expand-lg shadow">
<div class="container navbar-container">
<div class="navbar navbar-profile">
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="dropdownProfile" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">
<i class="fa fa-fw fa-user"></i>
</button>
<ul class="dropdown-menu dropdown-menu-left">
<!-- User is not authenticated so show login/register-->
<li v-if="authenticated"><router-link :to="{ name: 'login' }">Login/Register</router-link></li>
<!-- User is authenticated so show profile-->
<li v-if="authenticated"><router-link :to="{ name: 'profile' }">Profile</router-link></li>
</ul>
</div>
</div>
</div>
</nav>
</template>
<script>
export default {
data() {
return {
authenticated: false,
authenticationFailed: false,
};
},
};
</script>
So in the code I didnt make anything with the authenticated/authenticationFailed values because I dont know how can I check it from the Login component. And here is my login component:
<template>
<div id="login" class="card-container">
<div class="card col-lg-6 col-md-6 col-sm-12 col-xs-12">
<div class="headers">
<h3 class="card-header text-center active">LOGIN</h3>
<router-link :to="{name: 'register'}"><h4 class="card-header text-center non-active">REGISTER</h4></router-link>
</div>
<div class="card-body">
<div class="form-row">
<div class="form-group">
<input type="text" class="form-control" v-model="email" placeholder="Email" id="email" #keyup.enter="signIn" >
</div>
<div class="form-group">
<input type="password" class="form-control" v-model="password" placeholder="Password" id="password" #keyup.enter="signIn">
</div>
<div class="form-group form-check">
<input type="checkbox" id="rememberMeSignIn" class="form-check-input">
<label class="form-check-label" for="rememberMeSignIn">Remember Me</label>
</div>
<div class="form-group links">
<router-link :to="{name: 'forget-password'}">Forget Password?</router-link>
</div>
</div>
<div class="button-area">
<button v-on:click="signIn()" type="submit" class="btn button">Sign In</button>
</div>
</div>
</div>
</div>
</template>
<script>
import authHelper from "../helpers/authHelper";
import GENERAL_APP_CONSTANTS from "../constants/GeneralAppConstants";
export default {
name: 'Login',
beforeMount() {
this.authenticated = authHelper.validAuthentication();
},
mounted() {
EventBus.$on(GENERAL_APP_CONSTANTS.Events.CheckAuthentication, () => {
this.authenticated = authHelper.validAuthentication();
if (this.authenticated) {
this.email = this.password = "";
this.authenticationFailed = false;
}
});
EventBus.$on(GENERAL_APP_CONSTANTS.Events.LoginFailed, () => {
this.authenticationFailed = true
});
},
data () {
return {
authenticated: false,
authenticationFailed: false,
email: '',
password: '',
rememberMe: false,
}
},
methods: {
signIn: function () {
authHelper.signIn(this.email, this.password, () => {
this.$router.push({name: 'home'});
});
}
}
}
</script>
I tried to make it as simple as possible and if you can help me with this , I would be really glad.
PS: this is my index.vue that you can see the relation between Navbar and Login page and my login component is displaying when router-view changed:
<div id="app">
<Topnav/>
<Navbar/>
<router-view></router-view>
<Footer/>
</div>
I have four components Dashboard.vue(parent component) it contains Three child components(DisplayBooks.vue,sortBooksHightoLow,sortBooksLowtoHigh).
Now i want to import one more component called Cart.vue inside the Dashboard.vue .By default only DisplayBooks.vue component is only visible,if i click on cart-icon inside the Dashboard component it displays the Cart.vue component and hides the DisplayBooks.vue component .
How to acheive this thing please help me to fix this thing..
Dashboard.vue
<template>
<div class="main">
<div class="navbar navbar-default navbar-fixed-top">
<div class="navbar-header">
<img src="../assets/education.png" alt="notFound" class="education-image" />
</div>
<ul class="nav navbar-nav">
<li>
<p class="brand">Bookstore</p>
</li>
</ul>
<div class="input-group">
<i #click="handlesubmit();" class="fas fa-search"></i>
<div class="form-outline">
<input type="search" v-model="name" class="form-control" placeholder='search...' />
</div>
</div>
<ul class="nav navbar-nav navbar-right" id="right-bar">
<li><a> <i class="far fa-user"></i></a></li>
<p class="profile-content">profile</p>
<li><a><i class="fas fa-cart-plus"></i></a></li>
<p class="cart-content" >cart <span class="length" v-if="booksCount">{{booksCount}}</span></p>
</ul>
</div>
<div class="mid-body">
<h6>Books<span class="items">(128items)</span></h6>
<select class="options" #change="applyOption">
<option disabled value="">Sort by relevance</option>
<option value="HighToLow">price:High to Low</option>
<option value="LowToHigh">price:Low to High</option>
</select>
</div>
<div v-if="flam==false">
<h2>Hello</h2>
</div>
<DisplayBooks v-show="flag==='noOrder'" #update-books-count="(n)=>booksCount=n"/>
<sortBooksLowtoHigh v-show="flag==='lowToHigh'" />
<sortBooksHightoLow v-show="flag==='highToLow'" />
<Cart />
</div>
</template>
<script>
import sortBooksLowtoHigh from './sortBooksLowtoHigh.vue'
import sortBooksHightoLow from './sortBooksHightoLow.vue'
import DisplayBooks from './DisplayBooks.vue'
import Cart from './Cart.vue'
export default {
components: {
DisplayBooks,
sortBooksLowtoHigh,
sortBooksHightoLow,
Cart
},
data() {
return {
booksCount:0,
flag: 'noOrder',
brand: 'Bookstore',
name: '',
flam: true,
visible: true,
}
},
methods: {
flip() {
this.flam = !this.flam;
},
applyOption(evt) {
if (evt.target.value === "HighToLow") {
this.flag = 'highToLow';
} else this.flag = 'lowToHigh';
},
}
}
</script>
Cart.vue
<template>
<div class="main">
<div v-for="book in books" :key="book.id" class="container">
<div class="content">
<h5>My Cart({{booksCount}})</h5>
</div>
<div class="mid-section">
<img v-bind:src="book.file" alt="not found">
<p class="title-section">{{book.name}}</p>
</div>
<div class="author-section">
<p>by {{book.author}}</p>
</div>
<div class="price-section">
<h6>Rs.{{book.price}}</h6>
</div>
<button class="close-btn" #click="handlesubmit();" type="submit">close</button>
<div class="btn-grps">
<button class="btn" type="submit" >Place Order</button>
</div>
</div>
</div>
</template>
<script>
import service from '../service/User'
export default{
data(){
return {
booksCount:0,
books: [{
id: 0,
file: 'https://images-na.ssl-images-amazon.com/images/I/41MdP5Tn0wL._SX258_BO1,204,203,200_.jpg',
name: 'Dont Make me think',
author: 'Sai',
price: '1500'
},]
}
},
methods:{
}
}
</script>
It's recommended to use vur-router, but for a simple situation you could define a property called shownComp then update when you click on cart icon :
data() {
return {
shownComp:'DisplayBooks',
booksCount:0,
flag: 'noOrder',
then <li #click="shownComp='Cart'"><a><i class="fas fa-cart-plus"></i></a></li>
and :
<DisplayBooks v-show="flag==='noOrder' && shownComp==='DisplayBooks'" #update-books-count="(n)=>booksCount=n"/>
<Cart v-show=" shownComp==='Cart'" />
...
I want to change the boolean from false to true from one object(todo), when I do #click="updateTodoItem(todo.id, todo.title, todo.body)
thats how 1 object looks: {id: 1, title: "", body: "", done: false}
UPDATE (finished):
I figured it out and updated code here. I can only pass done one argument as payload to the store actions.
Home.vue
<template>
<div class="container">
<section class="hero is-link">
<div class="hero-body">
<div class="container">
<h1 class="title">
Hello Dudes
</h1>
<h2 class="subtitle">
Hamster ToDo List
</h2>
</div>
</div>
</section>
<section class="section">
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">All Tasks</p>
<p class="title">{{getTotalTodos}}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tasks has to be done</p>
<p class="title">{{getTodosFalse}}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tasks finished</p>
<p class="title">{{getTodosTrue}}</p>
</div>
</div>
<!-- <div class="level-item has-text-centered">
<div>
<p class="heading">Likes</p>
<p class="title">789</p>
</div>
</div> -->
</nav>
</section>
<section class="section">
<div class="columns">
<div class="column is-half">
<strong>Add ToDo</strong>
<div class="control">
<input class="input is-info" v-model="todoTitle" type="text" placeholder="Add Task Title">
<input class="input is-info" v-model="todoBody" type="text" placeholder="Add Task Body">
<button class="button is-link" #click="addTodoItem({title: todoTitle, body: todoBody, done: false}); empyInput()">Add</button>
</div>
</div>
</div>
<div class="tabs is-small">
<ul>
<li class="is-active"><a>All Tasks </a></li> <span class="tag is-link">{{getTotalTodos}}</span>
<li><a>Tasks still not done</a></li><span class="tag is-link">{{getTodosFalse}}</span>
<li><a>Finished Tasks</a></li><span class="tag is-link">{{getTodosTrue}}</span>
<!-- <li><a>Documents</a></li> -->
</ul>
</div>
<article class="media" v-for="todo in todoItems" :key="todo.id">
<figure class="media-left">
<!-- <p class="image is-64x64">
<img src="https://bulma.io/images/placeholders/128x128.png">
</p> -->
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{todo.title}}</strong>
<br>
{{todo.body}}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item">
<span class="icon is-small"><i class="fas fa-edit"></i></span>
</a>
<a class="level-item" v-if="todo.done==false" #click="updateTodoToTrue(todo.id)">
<span class="icon is-small"><i class="fas fa-check"></i></span>
</a>
<a class="level-item" v-if="todo.done==true" #click="updateTodoToFalse(todo.id)">
<span class="icon is-small"><i class="fas fa-undo"></i></span>
</a>
<a class="level-item" #click="deleteTodoItem(todo.id)">
<span class="icon is-small"><i class="fas fa-trash"></i></span>
</a>
</div>
</nav>
</div>
<div class="media-right">
<button class="delete" #click="deleteTodoItem(todo.id)"></button>
</div>
</article>
</section>
</div>
</template>
<script>
// # is an alias to /src
import {mapGetters} from 'vuex';
import {mapActions} from 'vuex';
export default {
name: 'Home',
data() {
return{
todoTitle: '',
todoBody: '',
}
},
// updated() {
// this.$store.dispatch('getTodoItems');
// },
computed: {
...mapGetters(['todoItems', 'getTotalTodos', 'getTodosTrue', 'getTodosFalse'])
},
methods: {
...mapActions(['addTodoItem', 'deleteTodoItem', 'updateTodoToTrue', 'updateTodoToFalse']),
// addTodo() {
// return this.$store.actions.addTodoItem;
empyInput() {
this.todoTitle = '';
this.todoBody = '';
}
}
}
</script>
store
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todoItems: []
},
// Keep in mind that response is an HTTP response
// returned by the Promise.
// The mutations are in charge of updating the client state.
mutations: {
UPDATE_TODO_ITEMS (state, payload) {
state.todoItems = payload;
},
ADD_TODO_ITEMS (state, payload) {
state.todoItems.push(payload)
},
DELETE_TODO_ITEMS (state, payload) {
state.todoItems.splice(payload, 1)
},
changeTodoStatus: (state, payload) => {
let index = state.todoItems.findIndex(el => {
return el.id == payload.id
})
state.todoItems[index].done = payload.done
}
},
actions: {
getTodoItems ({ commit }) {
axios.get('http://127.0.0.1:8000/api/').then((response) => {
commit('UPDATE_TODO_ITEMS', response.data)
});
},
addTodoItem ({ commit }, todoItem) {
axios.post('http://127.0.0.1:8000/api/', todoItem).then((response) => {
commit('ADD_TODO_ITEMS', response.data)
});
},
deleteTodoItem ({ commit }, todoItemId) {
axios.delete('http://127.0.0.1:8000/api/' + todoItemId + '/').then((response) => {
commit('DELETE_TODO_ITEMS', response.data)
});
},
updateTodoToTrue ({ commit }, todoItemId) {
var payload = {
done: true
};
axios.patch('http://127.0.0.1:8000/api/' + todoItemId + '/', payload).then((response) => {
commit('changeTodoStatus', response.data)
});
},
updateTodoToFalse ({ commit }, todoItemId) {
var payload = {
done: false
};
axios.patch('http://127.0.0.1:8000/api/' + todoItemId + '/', payload).then((response) => {
commit('changeTodoStatus', response.data)
});
},
},
getters: {
todoItems: state => state.todoItems,
getTotalTodos: state => state.todoItems.length,
getTodosTrue: state => state.todoItems.filter(todo => todo.done).length,
getTodosFalse: state => state.todoItems.filter(todo => !todo.done).length
},
modules: {
}
})
My problem was that I tried to pass down 3 arguments to the actions payload in the store. The action accepts accepts only 1 argument which is the payload. I changed the axios method from PUT to PATCH.
Home.vue
<template>
<div class="container">
<section class="hero is-link">
<div class="hero-body">
<div class="container">
<h1 class="title">
Hello Dudes
</h1>
<h2 class="subtitle">
Hamster ToDo List
</h2>
</div>
</div>
</section>
<section class="section">
<nav class="level">
<div class="level-item has-text-centered">
<div>
<p class="heading">All Tasks</p>
<p class="title">{{getTotalTodos}}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tasks has to be done</p>
<p class="title">{{getTodosFalse}}</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Tasks finished</p>
<p class="title">{{getTodosTrue}}</p>
</div>
</div>
<!-- <div class="level-item has-text-centered">
<div>
<p class="heading">Likes</p>
<p class="title">789</p>
</div>
</div> -->
</nav>
</section>
<section class="section">
<div class="columns">
<div class="column is-half">
<strong>Add ToDo</strong>
<div class="control">
<input class="input is-info" v-model="todoTitle" type="text" placeholder="Add Task Title">
<input class="input is-info" v-model="todoBody" type="text" placeholder="Add Task Body">
<button class="button is-link" #click="addTodoItem({title: todoTitle, body: todoBody, done: false}); empyInput()">Add</button>
</div>
</div>
</div>
<div class="tabs is-small">
<ul>
<li class="is-active"><a>All Tasks </a></li> <span class="tag is-link">{{getTotalTodos}}</span>
<li><a>Tasks still not done</a></li><span class="tag is-link">{{getTodosFalse}}</span>
<li><a>Finished Tasks</a></li><span class="tag is-link">{{getTodosTrue}}</span>
<!-- <li><a>Documents</a></li> -->
</ul>
</div>
<article class="media" v-for="todo in todoItems" :key="todo.id">
<figure class="media-left">
<!-- <p class="image is-64x64">
<img src="https://bulma.io/images/placeholders/128x128.png">
</p> -->
</figure>
<div class="media-content">
<div class="content">
<p>
<strong>{{todo.title}}</strong>
<br>
{{todo.body}}
</p>
</div>
<nav class="level is-mobile">
<div class="level-left">
<a class="level-item">
<span class="icon is-small"><i class="fas fa-edit"></i></span>
</a>
<a class="level-item" v-if="todo.done==false" #click="updateTodoToTrue(todo.id)">
<span class="icon is-small"><i class="fas fa-check"></i></span>
</a>
<a class="level-item" v-if="todo.done==true" #click="updateTodoToFalse(todo.id)">
<span class="icon is-small"><i class="fas fa-undo"></i></span>
</a>
<a class="level-item" #click="deleteTodoItem(todo.id)">
<span class="icon is-small"><i class="fas fa-trash"></i></span>
</a>
</div>
</nav>
</div>
<div class="media-right">
<button class="delete" #click="deleteTodoItem(todo.id)"></button>
</div>
</article>
</section>
</div>
</template>
<script>
// # is an alias to /src
import {mapGetters} from 'vuex';
import {mapActions} from 'vuex';
export default {
name: 'Home',
data() {
return{
todoTitle: '',
todoBody: '',
}
},
// updated() {
// this.$store.dispatch('getTodoItems');
// },
computed: {
...mapGetters(['todoItems', 'getTotalTodos', 'getTodosTrue', 'getTodosFalse'])
},
methods: {
...mapActions(['addTodoItem', 'deleteTodoItem', 'updateTodoToTrue', 'updateTodoToFalse']),
// addTodo() {
// return this.$store.actions.addTodoItem;
empyInput() {
this.todoTitle = '';
this.todoBody = '';
}
}
}
</script>
STORE
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todoItems: []
},
// Keep in mind that response is an HTTP response
// returned by the Promise.
// The mutations are in charge of updating the client state.
mutations: {
UPDATE_TODO_ITEMS (state, payload) {
state.todoItems = payload;
},
ADD_TODO_ITEMS (state, payload) {
state.todoItems.push(payload)
},
DELETE_TODO_ITEMS (state, payload) {
state.todoItems.splice(payload, 1)
},
changeTodoStatus: (state, payload) => {
let index = state.todoItems.findIndex(el => {
return el.id == payload.id
})
state.todoItems[index].done = payload.done
}
},
actions: {
getTodoItems ({ commit }) {
axios.get('http://127.0.0.1:8000/api/').then((response) => {
commit('UPDATE_TODO_ITEMS', response.data)
});
},
addTodoItem ({ commit }, todoItem) {
axios.post('http://127.0.0.1:8000/api/', todoItem).then((response) => {
commit('ADD_TODO_ITEMS', response.data)
});
},
deleteTodoItem ({ commit }, todoItemId) {
axios.delete('http://127.0.0.1:8000/api/' + todoItemId + '/').then((response) => {
commit('DELETE_TODO_ITEMS', response.data)
});
},
updateTodoToTrue ({ commit }, todoItemId) {
var payload = {
done: true
};
axios.patch('http://127.0.0.1:8000/api/' + todoItemId + '/', payload).then((response) => {
commit('changeTodoStatus', response.data)
});
},
updateTodoToFalse ({ commit }, todoItemId) {
var payload = {
done: false
};
axios.patch('http://127.0.0.1:8000/api/' + todoItemId + '/', payload).then((response) => {
commit('changeTodoStatus', response.data)
});
},
},
getters: {
todoItems: state => state.todoItems,
getTotalTodos: state => state.todoItems.length,
getTodosTrue: state => state.todoItems.filter(todo => todo.done).length,
getTodosFalse: state => state.todoItems.filter(todo => !todo.done).length
},
modules: {
}
})
HERE is a SANDBOX with the issue on it
I have a hoverable dropdown inside a Navbar
When I move to a different page, the dropdown is still open
I have tried this in plain Bulma, the issue still remains
I am on Nuxt.js using SSR
I am using nuxt-link / equivalent of Vue router-link to navigate to a different page.
Here is the my default.vue file
<template>
<div class="ch-container">
<header class="ch-header">
<nav
class="navbar is-fixed-top"
role="navigation"
aria-label="main navigation"
>
<div class="navbar-brand">
<nuxt-link class="navbar-item" to="/">
<img
alt="CH Logo"
src="https://i.imgur.com/v35Kfc9.png"
width="28"
height="28"
/>
</nuxt-link>
<a
role="button"
class="navbar-burger burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<nuxt-link class="navbar-item" to="/news">
News
</nuxt-link>
<nuxt-link class="navbar-item" to="/resources">
Resources
</nuxt-link>
<nuxt-link class="navbar-item" to="/tickers">
Tickers
</nuxt-link>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link">
More
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
FAQ
</a>
<nuxt-link class="navbar-item" to="/contact">
Contact
</nuxt-link>
<hr class="navbar-divider" />
<a class="navbar-item">
Feature Request
</a>
</div>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<a href="#">
<fa :icon="faMoon" />
</a>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link is-arrowless">
<fa :icon="faExclamationCircle" />
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
No new notifications
</a>
</div>
</div>
<div class="navbar-item">
<div class="buttons">
<nuxt-link class="button is-primary" to="/signup">
<strong>Sign up</strong>
</nuxt-link>
<nuxt-link id="login" class="button is-light" to="/login">
Log in
</nuxt-link>
</div>
</div>
</div>
</div>
</nav>
</header>
<main class="ch-main">
<nuxt />
</main>
<footer class="ch-footer is-hidden-mobile">
<div class="level">
<div class="level-left">
<div class="level-item">
<a href="#">
<span class="icon">
<fa :icon="faFacebookSquare" />
</span>
</a>
<a href="#">
<span class="icon">
<fa :icon="faTwitterSquare" />
</span>
</a>
<a href="#">
<span class="icon">
<fa :icon="faRedditSquare" />
</span>
</a>
</div>
</div>
<div class="level-right">
<div class="level-item">
©ch, All Rights Reserved
</div>
<div class="level-item">
<nuxt-link to="/contact">Contact</nuxt-link>
</div>
<div class="level-item">
<nuxt-link to="/terms-of-service">Terms</nuxt-link>
</div>
<div class="level-item">
<nuxt-link to="/privacy-policy">Privacy</nuxt-link>
</div>
</div>
</div>
</footer>
</div>
</template>
<script>
import {
faFacebookSquare,
faTwitterSquare,
faRedditSquare,
} from '#fortawesome/free-brands-svg-icons'
import { faMoon, faExclamationCircle } from '#fortawesome/free-solid-svg-icons'
export default {
computed: {
faFacebookSquare() {
return faFacebookSquare
},
faTwitterSquare() {
return faTwitterSquare
},
faRedditSquare() {
return faRedditSquare
},
faMoon() {
return faMoon
},
faExclamationCircle() {
return faExclamationCircle
},
},
mounted() {
// Get all "navbar-burger" elements
const $navbarBurgers = Array.prototype.slice.call(
document.querySelectorAll('.navbar-burger'),
0
)
// Check if there are any navbar burgers
if ($navbarBurgers.length > 0) {
// Add a click event on each of them
$navbarBurgers.forEach((el) => {
el.addEventListener('click', () => {
// Get the target from the "data-target" attribute
const target = el.dataset.target
const $target = document.getElementById(target)
// Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu"
el.classList.toggle('is-active')
$target.classList.toggle('is-active')
})
})
}
},
}
</script>
<style></style>
Here is a GIF illustrating the problem
How to close the dropdown after you move to a different page?
Ok, the whole thing makes it complicated, because the hover is triggered by css and therefore the dropdown can always be seen when the mouse is over it. You have to overwrite this state and solve it with vue events. We also have to put a watcher on the route to reset the state.
CodeSandbox - Example
<template>
<div class="container">
<nav class="navbar" role="navigation" aria-label="main navigation">
<div class="navbar-brand">
<!-- <a class="navbar-item" href="https://bulma.io"> -->
<img src="https://bulma.io/images/bulma-logo.png" width="112" height="28">
<a
role="button"
class="navbar-burger burger"
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</a>
</div>
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item">Home</a>
<a class="navbar-item">Documentation</a>
<div
#mouseover="toggleDropdown(true)"
#mouseleave="toggleDropdown(false)"
class="navbar-item has-dropdown is-hoverable"
>
<a class="navbar-link">More</a>
<div class="navbar-dropdown" :style="{display: showDropdown ? 'block' : 'none' }">
<nuxt-link class="navbar-item" to="/about">About</nuxt-link>
<nuxt-link class="navbar-item" to="/jobs">Jobs</nuxt-link>
</div>
</div>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary">
<strong>Sign up</strong>
</a>
<a class="button is-light">Log in</a>
</div>
</div>
</div>
</div>
</nav>
<Nuxt/>
</div>
</template>
<script>
export default {
data() {
return {
routeChange: false,
showDropdown: false
};
},
watch: {
$route() {
this.routeChange = true;
this.showDropdown = false;
}
},
methods: {
toggleDropdown(payload) {
if (this.showDropdown !== payload) this.routeChange = false;
if (!this.routeChange) {
this.showDropdown = payload;
}
}
}
};
</script>
This worked form me Bootstrap 5, in touch devices it uses click, on devices with mouse it uses hover
<template>
<span
v-if="item"
class="primary-navigation-list-dropdown"
#mouseover="isTouchscreenDevice ? null : openDropdownMenu()"
#mouseleave="isTouchscreenDevice ? null : closeDropdownMenu()"
>
<nuxt-link
to="#"
#click.prevent.native="openDropdownMenu"
v-click-outside="closeDropdownMenu"
:title="item.title"
:class="[
item.cssClasses,
{ show: isDropdownMenuVisible }
]"
:id="`navbarDropdownMenuLink-${item.id}`"
:aria-expanded="[isDropdownMenuVisible ? true : false]"
class="
primary-navigation-list-dropdown__toggle
nav-link
dropdown-toggle"
aria-current="page"
role="button"
data-toggle="dropdown"
>
{{ item.label }}
</nuxt-link>
<ul
:class="{ show: isDropdownMenuVisible }"
:aria-labelledby="`navbarDropdownMenuLink-${item.id}`"
class="
primary-navigation-list-dropdown__menu
dropdown-menu-list
dropdown-menu"
>
<li
v-for="item in item.children" :key="item.id"
class="dropdown-menu-list__item"
>
<NavLink
:attributes="item"
class="dropdown-menu-list__link dropdown-item"
/>
</li>
</ul>
</span>
</template>
<script>
import NavLink from '#/components/Navigation/NavLink';
export default {
name: "DropdownMenu",
props: {
item: {
type: Object,
required: true,
},
},
data() {
return {
isDropdownMenuVisible: false,
isTouchscreenDevice: false
};
},
mounted() {
this.detectTouchscreenDevice();
},
methods: {
openDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = !this.isDropdownMenuVisible;
} else {
this.isDropdownMenuVisible = true;
}
},
closeDropdownMenu() {
if (this.isTouchscreenDevice) {
this.isDropdownMenuVisible = false;
} else {
this.isDropdownMenuVisible = false;
}
},
detectTouchscreenDevice() {
if (window.PointerEvent && ('maxTouchPoints' in navigator)) {
if (navigator.maxTouchPoints > 0) {
this.isTouchscreenDevice = true;
}
} else {
if (window.matchMedia && window.matchMedia("(any-pointer:coarse)").matches) {
this.isTouchscreenDevice = true;
} else if (window.TouchEvent || ('ontouchstart' in window)) {
this.isTouchscreenDevice = true;
}
}
return this.isTouchscreenDevice;
}
},
components: {
NavLink
}
};
</script>
<style scoped lang="scss">
.primary-navigation-list-dropdown {
&__toggle {
color: $white;
&:hover {
color: $blue;
}
}
&__menu {
margin-top: 0;
}
&__dropdown {
}
}
.dropdown-menu-list {
&__item {
}
&__link {
&.active,
&.nuxt-link-exact-active {
border-bottom: 1px solid $blue;
}
}
}
</style>
NavLink.vue
<template>
<component
:is="attributes"
v-bind="linkAttributes(attributes.path)"
:title="attributes.title"
:class="[ attributes.cssClasses ]"
class="nav-link active_"
aria-current="page"
prefetch
>
{{ attributes.label }}
</component>
</template>
<script>
export default {
name: 'NavLink',
props: {
attributes: {
type: Object,
required: true
}
},
methods: {
linkAttributes(path) {
if (path.match(/^(http(s)?|ftp):\/\//) || path.target === '_blank') {
return {
is: 'a',
href: path,
target: '_blank',
rel: 'noopener'
}
}
return {
is: 'nuxt-link',
to: path
}
}
}
}
</script>
click-outside.js
import Vue from 'vue'
Vue.directive('click-outside', {
bind: function (el, binding, vnode) {
el.clickOutsideEvent = function (event) {
if (!(el == event.target || el.contains(event.target))) {
vnode.context[binding.expression](event);
}
};
document.body.addEventListener('click', el.clickOutsideEvent)
},
unbind: function (el) {
document.body.removeEventListener('click', el.clickOutsideEvent)
},
});
I’m not using Vue, but using Meteor (where similar route-calling trips up the Bulma drop-down removal).
Slightly hacky way to fix it, but the dropdown is made visible because the .has-dropdown element has the .is-hoverable class. So to fix, on any click on the dropdown’s items, I run this:
// Remove the hover effect = dropdown disappears
$(".has-dropdown").removeClass("is-hoverable");
// Tiny time later, put it back, so hover-drop works anew
setTimeout(function() {
$(".has-dropdown").addClass("is-hoverable");
}, 100);
Doesn’t really matter that it would hit all the dropdowns if you have more than one, because resetting them all is harmless when routing to a new page. But if you’re fussy you could target just the closest dropdown.
As Bulma’s mobile view doesn’t activate an on-hover effect anyway, this doesn’t break in the mobile “burger menu” version of my navbar.
Works OK for my project on Chrome, Safari, Firefox.
I am new to Vue and created a basic authentication application.
AuthService.js
module.exports = {
isLoggedIn: function() {
if (localStorage.getItem("authUser") != null) {
return true;
} else {
return false;
}
},
Logout: function() {
localStorage.removeItem("authUser");
},
}
App.vue
<template>
<div id="app" >
<top-progress ref="topProgress"></top-progress>
<div class="nav is-light is-fixed-top">
<div class="container">
<span class="nav-toggle" v-on:click="toggleNav" v-bind:class="{ 'is-active': isActive }">
<span></span>
<span></span>
<span></span>
</span>
<div class="nav-right nav-menu" v-bind:class="{ 'is-active': isActive }">
<router-link v-ripple to="/" class="nav-item r-item"><i class="fa fa-home"></i>Home</router-link>
<router-link v-ripple to="faq" class="nav-item r-item"><i class="fa fa-file"></i>Features</router-link>
<router-link v-ripple to="dashboard" class="nav-item r-item"><i class="fa fa-dashcube"></i>Dashboard</router-link>
<router-link v-ripple to="faq" class="nav-item r-item"><i class="fa fa-quora"></i>Faq</router-link>
<a class="nav-item r-item" v-if="LoggedIn"><i class="fa fa-sign-out" #click.prevent="Logout"></i>Logout</a>
<div class="nav-item" v-if="!LoggedIn">
<p class="control">
<router-link to="login" class="button is-primary is-outlined">
<span class="icon">
<i class="fa fa-download"></i>
</span>
<span> Join Now</span>
</router-link>
</p>
</div>
</div>
</div>
</div>
<br>
<router-view></router-view>
<footer class="footer is-secondary">
<div class="container">
<div class="columns">
<div class="column">
<p>And this right here is a spiffy footer, where you can put stuff.</p>
</div>
<div class="column has-text-right">
<a class="icon" href="#"><i class="fa fa-facebook"></i></a>
<a class="icon" href="#"><i class="fa fa-twitter"></i></a>
</div>
</div>
</div>
</footer>
</div>
</template>
<script>
import {isLoggedIn,Logout} from "./service"
import miniToastr from 'mini-toastr'
import topProgress from 'vue-top-progress'
export default {
name: 'app',
components:{topProgress},
data:function(){
return {
isActive:false,
LoggedIn:false,
}
},
created(){
this.LoggedIn=isLoggedIn();
},
mounted(){
miniToastr.init()
this.$refs.topProgress.start()
setTimeout(() => {
this.$refs.topProgress.done()
}, 2000)
},
methods:{
Logout:function(){
Logout();
this.$router.push("login");
},
}
}
</script>
<style lang="sass">
..//
</style>
After successful login, I am using this.$router.push("home") to navigate to home component but the Login/Logout button not hiding/showing until I refresh the page.
The problem you are having is the created: hook is only called when App.vue is first created. Since this component holders the router-view it is always there as you move around the app — it's never destroyed, so it never needs to be created again. As a result your this.LoggedIn is only updated when you first load the app (or as you discovered, hit refresh).
You need to find a different way to update this.LoggedIn. One obvious possibility it to set it in the logih/logout methods.
Logout:function(){
Logout();
this.LoggedIn = isLoggedIn(); // or simply this.LoggedIn = false
this.$router.push("login");
},
It looks like users will login in a different component, so you will need to send an event from the child component to App.vue and trigger a method on App.vue to set this.LoggedIn when users login.
There are probably other possibilities that will come to mind now that you see why it isn't working.