20
votes

So I have a header component that displays either the User's name or "Sign In" depending on whether they are logged in or not. I also have a Login component that does all of the business logic of logging in. They currently do not have a parent / child relationship.

When the User logs in, the header does not refresh or change unless a full page refresh is done in the browser. I've been doing a lot of searching and reading online about different ways to do this. ngOnChanges, NgZone, ApplicationRef, and ChangeDetectorRef seem to be the most popular. I'm trying to implement this behavior in ChangeDetectorRef as this seems to be most relevant to my situation. However, I can't seem to find actual examples of how to use this.

I've coded it up but it does not seem to do anything. Any advice would be appreciated. I'd even accept that I'm taking the wrong approach and need to use another solution besides ChangeDetectorRef.

LoginComponent

import { Component, OnInit } from '@angular/core';
import { Response } from '@angular/http';
import { Router } from '@angular/router';

import { AuthenticationService } from '../service/authentication.service';

@Component({
    selector: 'login-component',
    templateUrl: './login.component.html'
})

export class LoginComponent implements OnInit {
    constructor(private router: Router, 
                private authenticationService: AuthenticationService) { }

    ngOnInit() {
        // Resets the login service.  
        // This is one of the events that should cause the refresh.
        this.authenticationService.logout();
    }

    login() {
        /*
        Authentication code
        This is the other event that should cause the refresh.
        */
    }
}

HeaderComponent

import { ChangeDetectorRef, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';

import { Instance } from '../../entity/instance';

@Component({
    selector: 'header-component',
    templateUrl: './html/header.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})

export class HeaderComponent {

    userName: string;

    constructor(private ref: ChangeDetectorRef) {
        this.ref.markForCheck();
    }

    ngOnInit(): void {
        var currentUser = JSON.parse(localStorage.getItem('currentUser'));

        this.userName = currentUser && currentUser.full_name;

        if (!this.userName) {
            this.userName = "User Name";
        }
    }
}

AppComponent

import { ChangeDetectorRef, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';

import { Instance } from './entity/instance';
import { InstanceService } from './service/instance.service';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.css'],
    changeDetection: ChangeDetectionStrategy.OnPush
})

export class AppComponent implements OnInit {

    instances: Instance[];

    constructor(private instanceService: InstanceService) { }

    ngOnInit(): void {
    }
}

app.component.html

<header-component></header-component>

<router-outlet></router-outlet>
6
You have two options 1) global event emitter 2) use ApplicationRef - Umar Younis
Another one is check for router url and if user landed on login call detectchanges - Umar Younis
I have 2 more solutions: Global Variable Service Example: plnkr.co/edit/J4GvVp?p=info and Message Passing Service Example: plnkr.co/edit/e8Crbf?p=info . And yes, I wrote them :D - John Siu
Why not create a userService that exposes the user details using RXJS observables? - Devon Germano

6 Answers

28
votes

So I ended up taking some of the advice of using my service to emit the change. I read in some places on Stack Overflow that using a service this way was a bad pattern, that emits should only happen from a child component to a parent component. So I'm not sure this is the "proper" way, but it works for me because I want to have multiple components know of this event.

I already had a service that was dealing with my authentication, so all I had to do was give it an emitter, emit at the proper times, and then listen for the emit in my component.

Header Component

export class HeaderComponent {
    userName: string;

    constructor(private authenticationService: AuthenticationService) {
        authenticationService.getLoggedInName.subscribe(name => this.changeName(name));
    }

    private changeName(name: string): void {
        this.userName = name;
    }
}

Authentication Service

@Injectable()
export class AuthenticationService {
    @Output() getLoggedInName: EventEmitter<any> = new EventEmitter();

    login(email: string, password: string): Observable<boolean> {
        if (successfulLogIn(email, password)) {
            this.getLoggedInName.emit(fullName);
            return true;
        } else {
            this.getLoggedInName.emit('Sign In');
            return false;
        }
    }

    logout(): void {
        this.getLoggedInName.emit('Sign In');
    }
}
10
votes

@Pinski was good. But this can be more easy. This is an alternative method to emit and subscribe data.

Header Component

export class HeaderComponent implements OnInit {
    userName: string;

    constructor(private authenticationService: AuthenticationService) {}

   NgOnInit() {
      this.authenticationService.getLoggedInName.subscribe(name => this.userName = name);
   }

}

Authentication Service

@Injectable()
export class AuthenticationService {
    public getLoggedInName = new Subject(); //Alternate method to Emitting data across Components. Subject() is doing both Emitting data and Subscribing it in another component. So its the best way to compare with Emitting using Output.

    login(email: string, password: string): Observable<boolean> {
        if (successfulLogIn(email, password)) {
            this.getLoggedInName.next(fullName); //next() method is alternate to emit().
            return true;
        } else {
            this.getLoggedInName.next('Sign In');
            return false;
        }
    }

    logout(): void {
        this.getLoggedInName.next('Sign In');
    }
}

Give a try to Subject(). Happy coding.

4
votes

You can do this -->

Header Component -->

 ngOnInit() {
this.subscription = this.emitterService.getEmitter('userDetails').subscribe((user: Object) => {
            if(user)
                this.userName = user["name"];
        });
    }
ngOnDestroy() {
        // prevent memory leak when component is destroyed
        this.subscription.unsubscribe();
    }

Login Service -->

this.emitterService.getEmitter('userDetails').emit(userDetails);

the value which you are emitting from login component will be caught in your menu component.

1
votes

Session.ts

export class Session {


isLoggedIn(): boolean{
    return localStorage.getItem('username') == null ? false : true;
}
setLoggedInUser(username: string, icon: string): void {
    localStorage.setItem('username', username);
    localStorage.setItem('icon', icon);
}
logout(){
    localStorage.clear();
}

}

Header Component

    import { Component, OnInit, Input } from '@angular/core';
import { Session } from '../Session';
@Component({
  selector: 'AppHeader',
  templateUrl: './app-header.component.html',
  styleUrls: ['./app-header.component.scss']
})
export class AppHeaderComponent implements OnInit {
  @Input()
  session: Session;
  constructor() { }

  ngOnInit(): void {
    this.session = new Session();
  }
  logout(){
    this.session.logout();    
  }
}

HTML

<header>
<mat-toolbar>        
    <span>Example</span>
    <span class="example-spacer"></span>
    <button *ngIf="session.isLoggedIn()" routerLink="quests" mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
        <mat-icon>error</mat-icon>
    </button>
    <button *ngIf="session.isLoggedIn()" routerLink="stats" mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
        <mat-icon>assessment</mat-icon>
    </button>
    <button *ngIf="session.isLoggedIn()" routerLink="equipment" mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
        <mat-icon>accessibility</mat-icon>
    </button>
    
    <button *ngIf="!session.isLoggedIn()" routerLink="login" mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
        <mat-icon>login</mat-icon>
    </button>
    <button *ngIf="!session.isLoggedIn()" routerLink="register" mat-icon-button class="example-icon" aria-label="Example icon-button with share icon">
        <mat-icon>add</mat-icon>
    </button>        
    <button *ngIf="session.isLoggedIn()" routerLink="login" (click)="logout()" mat-icon-button class="example-icon favorite-icon" aria-label="Example icon-button with heart icon">
        <mat-icon>power_settings_new</mat-icon>
    </button>
</mat-toolbar>
0
votes

I am late here, but I am also trying to update my header component after login, I have followed your approach but it is not updating my Header component.

user.service.ts

import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';

@Injectable()
export class UserService {
constructor(public router: Router) {}
public getLoggedInName = new Subject(); 

async storeData(data) {
    this.getLoggedInName.next(data);
    localStorage.setItem('userData', JSON.stringify(data));
    return this.router.navigate(['']); // after login we navigate to home component, but I want to display the email in header component
}

getData() {
   return JSON.parse(localStorage.getItem('userData'));
}

logOut() {
   localStorage.setItem('userData', '');
   localStorage.clear();
   return this.router.navigate(['']);
}
}

login.component.ts

public socialSignIn(socialPlatform: string) {
      let socialPlatformProvider;
      if (socialPlatform === 'facebook') {
         socialPlatformProvider = FacebookLoginProvider.PROVIDER_ID;
      } else if (socialPlatform === 'google') {
         socialPlatformProvider = GoogleLoginProvider.PROVIDER_ID;
      }

      this.socialAuthService.signIn(socialPlatformProvider).then(userData => {
         this.apiConnection(userData);
      });
  }

  apiConnection(data){
      this.userPostData.email = data.email;
      this.userPostData.name = data.name;
      this.userPostData.provider = data.provider;
      this.userPostData.provider_id = data.id;
      this.userPostData.provider_pic = data.image;
      this.userPostData.token = data.token;
      this.user.storeData(data);

  }

socialSignIn() method we call on click on facebook icon.

nav-menu.component.ts my header component where I am expecting my userData variable would have the logged in user details.

import { Component, OnInit } from '@angular/core';
import { UserService } from '../services/user.service';

@Component({
  selector: 'app-nav-menu',
  templateUrl: './nav-menu.component.html',
  styleUrls: ['./nav-menu.component.css']
})
export class NavMenuComponent implements OnInit {
  isExpanded = false;
  isLoggedIn = false;
  public userData: any;

  constructor(public user: UserService){
    this.setUserSessionData();
  }

  ngOnInit() {
    this.user.getLoggedInName.subscribe(name => this.userData = name);
 }      

  public logOut(){
    debugger;
    this.user.logOut();
  }

  public setUserSessionData(){
    this.userData = this.user.getData();
    this.isLoggedIn = !this.userData;
  }
}

my nav-menu.component.html where I will be showing the logged in user email.

 <button class="nav-text" clrDropdownTrigger>
                {{userData?.email}}
                <clr-icon shape="caret down"></clr-icon>
 </button>
0
votes

I was facing the similar issue. After password reset when I tried to login all the API calls failed as headers were not being sent in the call until full page refresh.

For me the cookie were being store in different path when I was trying to login immediately after reset password. Fixed it by specifying the path while setting cookie.

Something like:

this.cookies.set(JWT_TOKEN, Token, 0.33,'/');