I am a beginner in VueJS and I am facing a problem.
Here in main.js I pass the user variable to App.vue via a props.
By default its value is {}
In main.js the getLoginStatus() method listens to the firebase authentication status, when a user logs in or out main.js.this.user changes.
user in main.js is passed to App.vue thanks to template, but when it changes as a result of the getLoginStatus() call it is not changed in App.vue (the child), whereas in main.js it does change.
How can this be done?
Thanks
My main.js :
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import firebase from "firebase";
Vue.config.productionTip = false
var firebaseConfig = {
CONFIGURATION
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
new Vue({
el: '#app',
router,
template: '<App :user="user" ></App>',
components: { App },
data(){
return {
user : {}
}
},
methods:{
getLoginStatus(){
firebase.auth().onAuthStateChanged(function(u) {
if (u) {
this.user = u
console.log("// User is signed in by Phone Number : ", u.phoneNumber)
} else {
this.user = null
console.log("// No user is signed in.")
}
});
console.log("this.user new value : "+this.user);
},
},
updated(){
this.getLoginStatus()
},
created(){
this.getLoginStatus()
}
})
App.vue :
<template>
<div id="app">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="#">Navbar</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<router-link class="nav-link" to="/">Home</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/register">Register/About old</router-link>
</li>
<li class="nav-item">
<router-link class="nav-link" to="/dashboard">Dashboard</router-link>
</li>
<li class="nav-item">
<button class="nav-link btn" #click="logout">Logout</button>
</li>
</ul>
</div>
</nav>
<div class="container mt-5">
<router-view />
</div>
<p v-if="user">logged{{user}}</p>
<p v-else>not logged{{user}}</p>
</div>
</template>
<script>
import firebase from 'firebase'
export default {
name: 'app',
props: ['user'],
methods:{
logout() {
firebase
.auth()
.signOut()
.then(() => {
alert('Successfully logged out');
if(this.$router.currentRoute.path!='/'){
this.$router.push('/')
}
})
.catch(error => {
alert(error.message);
if(this.$router.currentRoute.path!='/'){
this.$router.push('/')
}
});
},
},
}
</script>
You're using an unbound function here:
firebase.auth().onAuthStateChanged(function(u) {
So when setting this.user inside this function, this is not pointing to your Vue instance. You can either .bind(this) your function or just use an arrow function which will auto bind:
firebase.auth().onAuthStateChanged(u => {
Related
I have a 'Documentation' page which has 3 sub-menus in it.
What I'm trying to do is no matter which sub-menu is clicked I want it to go to one main documentation Vue component and then pass a variable into it so a sub-component also loads in the main component.
This is what I have so far:
Nav.vue
<template>
<div>
<nav class="navbar navbar-expand-lg">
<ul class="navbar-nav ml-auto h-100 d-flex align-items-center" style="margin-right: 5rem">
<div class="nav-item dropdown" #mouseover="openDocDropDown" #mouseleave="closeDocDropDown">
<div ref="docDropdown" class="d-flex align-items-center" role="button" data-toggle="dropdown" style="height: 42px; cursor: default">
<i class="far fa-file-alt" style="font-size: 1.9rem" />
Documentation
<i class="ml-2" :class="[docCollapsed ? 'fa-caret-down' : 'fa-caret-up', 'fas']" />
</div>
<div v-if="!docCollapsed" class="dropdown-menu subMenus" aria-labelledby="navbarDropdown">
<router-link to="/documentation" tag="div" class="nav-item d-flex align-items-center p-3" #click.native="docMenuClicked('admin')">
<i class="far fa-file-pdf" style="font-size: 1.9rem" />
<span class="pt-1 pl-1">Administrator documentation</span>
</router-link>
<router-link to="/documentation" tag="div" class="nav-item d-flex align-items-center p-3" #click.native="docMenuClicked('reseller_channel')">
<i class="far fa-file-pdf" style="font-size: 1.9rem" />
<span class="pt-1 pl-1">Reseller/Channel documentation</span>
</router-link>
<router-link to="/documentation" tag="div" class="nav-item d-flex align-items-center p-3" #click.native="docMenuClicked('cust')">
<i class="far fa-file-pdf" style="font-size: 1.9rem" />
<span class="pt-1 pl-1">Customer documentation</span>
</router-link>
</div>
</div>
<router-link to="/contactus" tag="li" :class="[currentPage.includes('/conactus/') ? 'router-link-exact-active router-link-active' : '', 'nav-item mainMenuLink d-flex align-items-center']">
<i class="d-none d-xl-inline icon-nav_contact_us iconSize" /> Contact us
</router-link>
</ul>
</nav>
</div>
</template>
<script>
export default {
name: "TopNav",
props: [ 'hasErrored' ],
data() {
return {
unCollapsed: true,
docCollapsed: true,
docMenuItemClicked: ''
}
},
computed: {
...mapGetters({
user: 'user'
}),
currentPage() {
return this.$route.path;
}
},
methods: {
openUnDropDown() {
this.$refs.unDropdown.visible = true;
this.unCollapsed = false;
},
closeUnDropDown() {
this.$refs.unDropdown.visible = false;
this.unCollapsed = true;
},
openDocDropDown() {
this.$refs.docDropdown.visible = true;
this.docCollapsed = false;
},
closeDocDropDown() {
this.$refs.docDropdown.visible = false;
this.docCollapsed = true;
},
docMenuClicked(data) {
this.docMenuItemClicked = data;
setTimeout(() => {
VueEvent.$emit('document-menu-clicked', { menuClicked: this.docMenuItemClicked });
this.docCollapsed = true;
}, 1)
}
}
}
</script>
DocumentationMain.vue
<template>
<div>
Main Document Page
{{ menuClicked }}
<ResellChanDocs v-if="menuClicked === 'reseller_channel'" />
<CustDocs v-else-if="menuClicked === 'cust'" />
<AdminDocs v-else />
</div>
</template>
<script>
import ResellChanDocs from './DocumentationResellerChannel';
import CustDocs from './DocumentationCustomer';
import AdminDocs from './DocumentationAdministrator';
export default {
name: 'MainDocumentation',
components: {
ResellChanDocs,
CustDocs,
AdminDocs
},
data() {
return {
menuClicked: 'admin'
}
},
beforeMount() {
VueEvent.$on('document-menu-clicked', (data) => {
this.menuClicked = data.menuClicked;
});
}
}
</script>
routes.js
import Home from './components/Home.vue';
import ContactUs from './components/ContactUs';
import Documentation from './components/DocumentationMain';
export const routes = [
{ path: '', component: Home },
{ path: '/contactus', component: ContactUs },
{ path: '/documentation', component: Documentation, props: true },
{ path: '*', redirect: '/' }
];
Gives
Issues
Is there an easier way of doing this?
When I click a menu my 'DocumenationMain' doesn't seem to register the VueEvent.$emit
When I click a link the second time, the correct component is rendered
When I click a link, all my other menus highlight blue
I get the feeling I'm doing this wrong
Just use Dynamic Route Matching
export const routes = [
{ path: '', component: Home },
{ path: '/contactus', component: ContactUs },
{ path: '/documentation/:doctype', component: Documentation, props: true },
{ path: '*', redirect: '/' }
];
nav.vue
<router-link to="/documentation/reseller" />
<router-link to="/documentation/cust" />
<router-link to="/documentation/admin" />
Documentation.vue
props: {
doctype: {
type: String,
default: 'admin'
}
}
Router should now pass :doctype value (from the URL) into Documentation component doctype prop - use it to decide what sub-component to activate inside...
Hello I'm trying to set up view-spinner, But an error appears:
vue.esm.js:591 [Vue warn]: Property or method "loading" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
That's my main JS (app.js):
import Vue from 'vue';
import App from './App.vue'
import axios from 'axios'
import router from './router'
import store from './vuex/store';
import PulseLoader from 'vue-spinner/src/PulseLoader.vue'
require('../scss/style.scss');
Vue.config.productionTip = false;
Vue.prototype.$http = axios;
new Vue({
el: '#vueApp',
router,
store,
template: '<App/>',
components: {
App,
PulseLoader
},
});
My .vue
<template>
<div class="app">
<pulse-loader :loading="loading" :color="color" :size="size"></pulse-loader>
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<router-link class="nav-item" tag="li" to="/" active-class="active">
<a class="nav-link">Home</a>
</router-link>
<router-link class="nav-item" tag="li" to="/wow" active-class="active">
<a class="nav-link">Wow</a>
</router-link>
</ul>
</div>
</nav>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'app',
data: () => {
return {
color: '#000000',
size: '1000px'
}
}
}
</script>
<style lang="scss">
</style>
Thanks for your help.
in your data object add loading property and import the module as follow :
import PulseLoader from "vue-spinner/src/PulseLoader.vue";
export default{
data: () => {
return {
color: '#000000',
size: '1000px',
loading:true
}
},
components:{PulseLoader}
...
}
or you could use computed a property like :
computed:{
loading(){ return true;}
}
you could do whatever you want and return a boolean value with loading name
It looks like you are setting up vue-spinner in the wrong place, i.e main.js instead of App.vue.
This works (see CodeSandbox)
main.js
import Vue from "vue";
import App from "./App";
// import PulseLoader from "vue-spinner/src/PulseLoader.vue";
Vue.config.productionTip = false;
/* eslint-disable no-new */
new Vue({
el: "#vueApp",
template: "<App/>",
components: {
App,
// PulseLoader
}
});
App.vue
<template>
<div id="app">
<pulse-loader :loading="loading" :color="color" :size="size"></pulse-loader>
...
</div>
</template>
<script>
import PulseLoader from "vue-spinner/src/PulseLoader.vue";
export default {
name: "App",
data() {
return {
loading: true,
color: "#2c3e50",
size: "10px"
};
},
components: {
PulseLoader,
...
}
};
</script>
<style>
...
I'm trying to implement a web application with aurelia and typescript.
I started from the aurelia-skeleton-typescript-webpack project as basis.
In the nav-bar i have also inserted a spinner for choose various city location, which should call a method which in turn publishes a message so that in the app.js a subscriber should display the view of the corresponding city.
I have implemented the view nav-bar-spinner.html and the view-model nav-bar-spinner.ts, which creates the spinner in the view nav-bar.html. The nav-bar.html is then inserted in the app.html as a template.
Each spinner item has a method that calls the publishLocation('city'), wich are bindet with the view-model nav-bar-spinner.ts.
Now when i click on a spinner item i receive the error: "Error: publishNavLocation is not a function"
I thing is a binding problem. I make a custom instantiation from object nav-bar-spinner in app.ts.
How i can do this binding correct?
I would be glad for some tips.
Here the code:
app.html
<template>
<require from="nav-bar.html"></require>
<nav-bar router.bind="router"></nav-bar>
<div class="page-host">
<div class="row">
<router-view swap-order="after" class="col-md-12"></router-view>
</div>
</div>
</template>
app.ts
import {Redirect} from 'aurelia-router';
import {Router, RouterConfiguration} from 'aurelia-router';
import {EventAggregator} from 'aurelia-event-aggregator';
import {inject} from 'aurelia-framework';
import {NavBarSpinner} from './nav-bar-spinner';
#inject(EventAggregator)
export class App
{
navBarSpinner;
constructor(private ea: EventAggregator)
{
this.navBarSpinner = new NavBarSpinner('hello world')
}
router : Router;
configureRouter(config: RouterConfiguration, router: Router)
{
config.title = 'bbv AmbientMonitor';
config.map([
{ route: '', name: 'login', moduleId: './login', nav: true, title: 'Anmeldung' },
{ route: 'live-view-all', name: 'live-view-all', moduleId: './live-view-all', nav: true, title: 'Live-Ansicht' },
{ route: 'live-view-zg', name: 'live-view-zg', moduleId: './live-view-zg', nav: true, title: 'Live-Ansicht' },
.
.
.
.
.
.
{ route: 'historical-view', name: 'historical-view', moduleId: './historical-view', nav: true, title: 'Historie-Ansicht'}
]);
this.router = router;
}
attached()
{
this.ea.subscribe('nav::toggleLogin', (data) =>
{
console.log('Subscribe data is: ' + data.nav);
this.router.navigateToRoute(data.nav);
});
}
}
nav-bar.html
<template bindable="router">
<require from="./nav-bar-spinner"></require>
<!-- <require from="nav-bar-spinner.html"></require> -->
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">
<i class="fa fa-home"></i>
<span>${router.title}</span>
</a>
</div>
<div class="collapse navbar-collapse" id="skeleton-navigation-navbar-collapse">
<ul class="nav navbar-nav">
<div class="pull-left">
<compose class="nav navbar-nav" view="nav-bar-spinner.html" view-model.bind="navBarSpinner"></compose>
</div>
<li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
<a data-toggle="collapse" data-target="#skeleton-navigation-navbar-collapse.in" href.bind="row.href">${row.title}</a>
</li>
</ul>
<!-- <ul class="nav navbar-nav navbar-right">
<li class="loader" if.bind="router.isNavigating">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</li>
</ul> -->
</div>
</nav>
</template>
nav-bar-spinner.html
<template bindable="navBarSpinner">
<li class="dropdown">
<div as-element="compose" view-model.bind="navBarSpinner"></div>
Standort <span class="caret"></span>
<ul class="dropdown-menu">
<li>Zug</li>
<li>Zürich</li>
<li>Bern</li>
<li>Luzern</li>
<li>München</li>
</ul>
</li>
</template>
nav-bar-spinner.ts
import { EventAggregator } from 'aurelia-event-aggregator';
import { inject } from 'aurelia-framework';
import { View } from "aurelia-framework";
#inject(EventAggregator)
export class NavBarSpinner {
ea;
constructor(msg) {
this.ea = new EventAggregator();
}
publishNavLocation(navToCity) {
this.ea.publish('nav::toggleLogin', {nav: navToCity});
console.log("Method call publishLocationZug()");
}
}
Your problem lies with the following line:
view-model.bind="navBarSpinner"
Aurelia doesn't process this correctly. It is the name of the class, but you need to address it differently in an HTML attribute.
view-model.bind="nav-bar-spinner"
This tells Aurelia to look for a class named NavBarSpinner.
P.S. I also recommend you look into how Aurelia Dependency Injection works, you have quite some unnecessary (and false) code right now.
Thanks for the tips. After reading better the aurelia doc i resolved my problem.
Here my changes for typescript:
Chanded inject with autoinject
import { autoinject } from 'aurelia-framework';`
-
-
-
#autoinject( EventAggregator )
export class NavBarSpinner {
constructor(private ea: EventAggregator) {}
-
-
-
and in nav-bar.html i inserted the nav-bar-spinner.html with the binding view-model="nav-bar-spinner"
<div class="pull-left">
<compose class="nav navbar-nav" view-model="nav-bar-spinner"></compose>
</div>
and removed the other unnecessary bindings and requirements.
I have a property in my AuthService wich tell you if you are log in or not.
export class AuthService {
token: string;
isLogIn = new Subject<boolean>();
When I login correctly I set the isLogIn to true in the other hand when I click logout I set the property to false. I have this logic in my AppComponent(the first component been loaded).
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in.
this.authService.user = new User(user.displayName, user.email, user.photoURL, user.uid);
console.log(user);
this.isLoading = false;
this.authService.isLogIn.next(true);
} else {
// No user is signed in.
console.error('no user is login');
this.authService.isLogIn.next(false);
}
});
In my HeaderComponent I have:
export class HeaderComponent implements OnInit {
isActivated = false;
constructor(private authService: AuthService) { }
ngOnInit() {
this.authService.isLogIn.asObservable().subscribe(
(data: boolean) => {
this.isActivated = data;
}
);
// this.isActivated = this.authService.isLogin();
}
}
the template:
<ng-template [ngIf]="!isActivated">
<a class="nav-item nav-link text-white anchor-hover"
routerLink="/login"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Ingresar
</a>
</ng-template>
<ng-template [ngIf]="isActivated">
<!--DROPDOWN PROFILE-->
<div class="btn-group" role="group" *ngIf="authService.user">
<button id="btnGroupDrop1" type="button" class="btn btn-light dropdown-toggle pointer" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{authService.user.displayName}}
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" href="#">Dropdown link</a>
<a class="dropdown-item" href="#">Dropdown link</a>
</div>
</div>
<a class="nav-item nav-link text-white anchor-hover"
routerLink="/dashboard"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Dashboard
</a>
</ng-template>
What I'm approching here is that when the user is log in I show him a dashboard button and hide the login button but this is only seen if you refresh the page or chage the current route. I don't want to change the route or refresh the page to see differents UI components depending if you are log in or not.
Now I let you the entire files:
AppRouting Module:
import { NgModule } from '#angular/core';
import { Routes, RouterModule } from '#angular/router';
import {HomeComponent} from './shared/home/home.component';
import {LoginComponent} from './public/login/login.component';
import {DashboardComponent} from './auth/dashboard/dashboard.component';
const routes: Routes = [
{path: '', component: HomeComponent},
{path: 'login', component: LoginComponent},
{path: 'dashboard', component: DashboardComponent}
];
#NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule{}
AppComponent.ts
import {Component, OnInit} from '#angular/core';
import * as firebase from 'firebase';
import {AuthService} from './public/services/auth.service';
import {User} from "./auth/models/user.model";
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
isLoading = true;
constructor(private authService: AuthService) {}
ngOnInit() {
// this.authService.isLogIn.next(localStorage.length > 0);
firebase.auth().onIdTokenChanged(
(data) => {
console.log('TOKEN HAS CHANGED', data);
if(data) {
data.getIdToken().then(
(tk) => {
this.authService.token = tk;
}
);
} else {
console.log('You don\'t have a token yet, please login...');
// TODO this means that the user is new or have logout.
}
}
);
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in.
this.authService.user = new User(user.displayName, user.email, user.photoURL, user.uid);
console.log(user);
this.isLoading = false;
this.authService.isLogIn.next(true);
} else {
// No user is signed in.
console.error('no user is login');
this.authService.isLogIn.next(false);
}
});
// check localstorage, so we can see if there is a logged user
if (this.authService.getLocalUserInfo()) {
this.authService.isLogIn.next(true);
} else {
this.authService.isLogIn.next(false);
}
}
}
AppComponent Template:
<div>
<app-header></app-header>
</div>
<div>
<router-outlet></router-outlet>
</div>
AppHeader.ts
import { Component, OnInit } from '#angular/core';
import {AuthService} from '../../public/services/auth.service';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
isActivated = false;
constructor(private authService: AuthService) { }
ngOnInit() {
this.authService.isLogIn.asObservable().subscribe(
(data: boolean) => {
this.isActivated = data;
}
);
// this.isActivated = this.authService.isLogin();
}
}
AppHeader Template
<nav class="navbar navbar-expand-lg navbar-dark bg-custom-nav px-5">
<a class="navbar-brand" routerLink="/">MovieApp</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="container-fluid">
<div class="collapse navbar-collapse d-lg-flex justify-content-lg-between row" id="navbarNavAltMarkup">
<div class="navbar-nav px-sm-4 px-lg-0">
<a class="nav-item nav-link text-white"
routerLink="/"
routerLinkActive="active font-weight-bold"
[routerLinkActiveOptions]="{exact: true}">Inicio</a>
</div>
<div class="col-lg-8">
<app-search-bar></app-search-bar>
</div>
<div class="row">
<ng-template [ngIf]="!isActivated">
<a class="nav-item nav-link text-white anchor-hover"
routerLink="/login"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Ingresar
</a>
</ng-template>
<ng-template [ngIf]="isActivated">
<!--DROPDOWN PROFILE-->
<div class="btn-group" role="group" *ngIf="authService.user">
<button id="btnGroupDrop1" type="button" class="btn btn-light dropdown-toggle pointer" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{authService.user.displayName}}
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" href="#">Dropdown link</a>
<a class="dropdown-item" href="#">Dropdown link</a>
</div>
</div>
<a class="nav-item nav-link text-white anchor-hover"
routerLink="/dashboard"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Dashboard
</a>
</ng-template>
</div>
</div>
</div>
</nav>
AuthService.ts
import * as firebase from 'firebase';
import {Router} from '#angular/router';
import { Injectable } from "#angular/core";
import {Subject} from "rxjs/Subject";
import {User} from "../../auth/models/user.model";
#Injectable()
export class AuthService {
token: string;
isLogIn = new Subject<boolean>();
user: User;
constructor(private router: Router){}
signinWithFacebook() {
const provider = new firebase.auth.FacebookAuthProvider();
provider.addScope('user_location');
return firebase.auth().signInWithPopup(provider)
.then(
(res) => {
console.log(res);
this.getTokenId();
localStorage.setItem('userInfo', JSON.stringify(firebase.auth().currentUser.providerData[0]));
const userInfo = firebase.auth().currentUser.providerData[0];
this.user = new User(userInfo.displayName, userInfo.email, userInfo.photoURL, userInfo.uid);
this.isLogIn.next(true);
this.router.navigate(['/dashboard']);
}
);
}
getTokenId() {
firebase.auth().currentUser.getIdToken()
.then(
(tk) => {
return this.token = tk;
}
);
}
logout() {
return firebase.auth().signOut();
// handle in component
}
getLocalUserInfo(): boolean {
if(localStorage.getItem('userInfo')) {
const transformStoredUser = JSON.parse(localStorage.getItem('userInfo'));
this.user = new User(transformStoredUser.displayName, transformStoredUser.email, transformStoredUser.photoURL, transformStoredUser.uid);
return true;
} else {
return false;
}
}
isLogin():boolean {
if (localStorage.getItem('userInfo')) {
return true;
}
return false;
}
}
You can find the full project at: https://github.com/lucasdavidferrero/movieApp
One more thing, I've used the async pipe but the template doesn't reflect the chages. In the github project you'll find that I'm using async in the template but still the same bug. I hope someone could help me.
I'm going to try to help/answer, mind you there is a lot of code to sift through, so I may have missed something obvious. Anyway to hopefully neaten up your code and point you to the right direction you may want to look into the AsyncPipe.
I want to first modify your AuthService to get inline with some better practices, by first changing isLogIn = new Subject<boolean>(); to private _isLogIn = new Subject<boolean>();. We don't want to share the Subject to other components that will modify it, so everywhere in our AuthService you'll just need to add the underscore... Finally to export our Subject to be used we can add a get function like this:
get isLogIn(): Observable<boolean> {
return this._isLogIn.asObservable();
}
And now we don't need to chain the asObservable() to where ever we use it.
Also we want to move where you watch for changes in your user state from the AppComponent to your AuthService. Then you can instantiate the service in the constructor of your AppModule so your service will start to watch for changes in the auth state as soon as your app starts up.
You may also run into an issue where the resolve of the auth state may be falling out of zone, so while this may not be necessary you can/should import NgZone from core and pass it into your constructor of your AuthService constructor(private router: Router, private _zone: NgZone) { } so when you update the value of the auth state you can wrap it in this function:
this._zone.run(() => {
// your code here
}
Now if I was to write your header.component.ts I would have done something like this:
export class HeaderComponent implements OnInit {
constructor(private authService: AuthService) { }
ngOnInit() {}
get isActivated(): Observable<boolean> {
return this.authService.isLogIn;
}
}
So here I am just passing the observable straight to our template with a get function. So in the template I would implement the AsyncPipe like so:
<div *ngIf="!(isActivated | async) else user">
<a class="nav-item nav-link text-white anchor-hover" routerLink="/login"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Ingresar
</a>
</div>
<ng-template #user>
<!--DROPDOWN PROFILE-->
<div class="btn-group" role="group" *ngIf="authService.user">
<button id="btnGroupDrop1" type="button" class="btn btn-light dropdown-toggle pointer" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{authService.user.displayName}}
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="btnGroupDrop1">
<a class="dropdown-item" href="#">Dropdown link</a>
<a class="dropdown-item" href="#">Dropdown link</a>
</div>
</div>
<a class="nav-item nav-link text-white anchor-hover"
routerLink="/dashboard"
routerLinkActive="active font-weight-bold">
<i class="fa fa-sign-in pr-2" aria-hidden="true"></i>Dashboard
</a>
</ng-template>
The AsyncPipe will automatically subscribe and unsubscribe to your Observable and handle the refresh/zone updates.
Now I gave some overview/basic changes I would do to make sure the code behaves as expected. I invite you to try this and see if your *ngIfs work more "neatly." However you may also want to look up if else in Angular... => https://stackoverflow.com/a/43006589/5076023
I've spent some time trying get the wiring for this working properly and can't. I don't know what I'm doing wrong. The best reference I've found for this issue so far is Aurelia Binding Click Trigger in Nav Bar. I tried that approach but am still getting the same error:
Uncaught Error: authenticate is not a function(…) in aurelia-binding.js:1965 (getFunction)
Here's what my setup looks like:
nav-bar.html
<template bindable="router, authenticate">
<nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse-main">
<span class="sr-only">Toggle Navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">
<i class="fa fa-home"></i>
<span>${router.title}</span>
</a>
</div>
<div class="collapse navbar-collapse" id="navbar-collapse-main">
<ul class="nav navbar-nav">
<li repeat.for="row of router.navigation | authFilter: authenticated" class="${row.isActive ? 'active' : ''}">
<a data-toggle="collapse" data-target="#navbar-collapse-main.in" href.bind="row.href">${row.title}</a>
</li>
</ul>
<ul if.bind="authenticated" class="nav navbar-nav navbar-right">
<li>${userName}</li>
<li>Logout</li>
</ul>
<ul if.bind="!authenticated" class="nav navbar-nav navbar-right">
<li><a id="loginLink" click.trigger="authenticate()">Login</a></li>
<li> </li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="loader" if.bind="router.isNavigating">
<i class="fa fa-spinner fa-spin fa-2x"></i>
</li>
</ul>
</div>
</nav>
</template>
app.html
<template>
<require from="./views/shared/nav-bar.html"></require>
<require from="bootstrap/css/bootstrap.css"></require>
<nav-bar router.bind="router" authenticate.call="authenticate()"></nav-bar>
<div class="page-host">
<div class="container-fluid">
<router-view></router-view>
</div>
</div>
</template>
app.ts
import {inject, computedFrom} from "aurelia-framework";
import {Router, RouterConfiguration} from 'aurelia-router'
import {AuthService, AuthenticateStep} from 'aurelia-authentication';
import {log} from "./services/log";
#inject(AuthService)
export class App {
authService: AuthService;
router: Router;
userName: string;
constructor(auth) {
this.authService = auth;
}
configureRouter(config: RouterConfiguration, router: Router) {
config.title = 'AppName';
config.addPipelineStep('authorize', AuthenticateStep);
config.map([
{ route: ['', 'welcome'], name: 'welcome', moduleId: 'views/welcome', nav: true, title: 'Welcome' },
{ route: "orgTypes", name: "orgTypes", moduleId: "views/orgTypes", nav: true, auth: true, title: "Organization Types" },
{ route: "credits", name: "credits", moduleId: "views/credits", nav: true, auth: true, title: "Application Credits" }
]);
this.router = router;
}
authenticate() {
return this.authService.authenticate('identityServer')
.then((response) => {
log.info("login successful");
});
};
#computedFrom('authService.authenticated')
get authenticated() {
return this.authService.authenticated;
}
}
What is the proper setup to get a method in the App VM to bind in a subview?
Edit 1: Following FabioLuz second comment.
What Fabio has suggested is valid and should be working. You might have other issues preventing it from functioning.
Can you check it by simplifying App.authenticate() like this?
Just to rule out possible errors of underlying layer.
authenticate() {
log.info("login successful");
}
Another guess:
Is ./services/log a static object? Assuming it is not, injection might be missing for it.
Since you are using TypeScript, autoinject might help you to avoid similar pitfalls.
import {autoinject, computedFrom} from "aurelia-framework";
import {AuthService, AuthenticateStep} from 'aurelia-authentication';
import {log} from "./services/log";
#autoinject()
export class App {
authService: AuthService;
logger: log;
constructor(auth: AuthService, logger: log) {
this.authService = auth;
this.logger = logger;
}
}
What is the proper setup to get a method in the App VM to bind in a subview?
I know 3 possible solutions to achieve that (there may be more). I've created a gist showing these in action.
https://gist.run/?id=b9e8fee11e338e08bc5da7d4df68e2db
Use the dropdown to switch between navbar implementations. :)
1. HTML Only Element + bindables
This is your current scenario. See nav-bar-function.html in the demo.
2. <compose> + inheritance
Composition can be useful for some dynamic scenarios, but try not to overuse it. [Blog post]
When no model is provided, composed element inherits parent's viewmodel context.
Normally I would not recommend using it in your case. However, considering your issues with Solution 1., you could use this option for debug purposes. If you get the same error with <compose> as well, you may have a problem with App.authenticate() function itself.
Try it out in your solution by replacing
<nav-bar router.bind="router" authenticate.call="authenticate()"></nav-bar>
with
<compose view="./nav-bar.html"></compose>
This way, nav-bar.html behaves as a part of App component. See nav-bar-compose.html in the demo.
3. Custom Element + EventAggregator
You can use pub/sub communication between components* to avoid tight-coupling. Related SO answer: [Accessing Custom Component Data/Methods], and [Docs]
*components: custom elements with viewmodel classes
I hope it will help! :)