0

I have a property in my AuthService which tells you if you are logged 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 approaching 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 change the current route. I don't want to change the route or refresh the page to see different 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 changes. 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.

Jason Aller
  • 3,541
  • 28
  • 38
  • 38
Lucas David Ferrero
  • 1,630
  • 14
  • 40

1 Answers1

2

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

MichaelSolati
  • 2,847
  • 1
  • 17
  • 29
  • Also you can handle the `firebase.auth().onAuthStateChanged` in your AuthService, and then instantiate the service in the constructor of your AppModule. I missed that you aren't doing this in the AuthService – MichaelSolati Sep 28 '17 at 02:47
  • I've followed your steps but how can I set values to isLogIn now? With set? – Lucas David Ferrero Sep 28 '17 at 02:54
  • I don't know if you saw my comment, but I missed that you weren't watching the user state in the authservice, so I recommend you moving it there. (See comment above) – MichaelSolati Sep 28 '17 at 02:55
  • I've changed the firebase logic to AuthService and call it from AppModule but still in the header I can't see the elements associated with ngIf. If you want I cant create a repo on github and share with you. – Lucas David Ferrero Sep 28 '17 at 03:12
  • I think you meant to say you can share the repo with me, so please, I'll take a peak and try to help. – MichaelSolati Sep 28 '17 at 03:14
  • Here the link of my repo: https://github.com/lucasdavidferrero/movieApp thanks for your time I appreciate it – Lucas David Ferrero Sep 28 '17 at 03:19
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/155470/discussion-between-michaelsolati-and-lucas-david-ferrero). – MichaelSolati Sep 28 '17 at 03:21