0
votes

I have 2 Angular2 components which I would like to be able to share a value.

My App component code is:

<app-header></app-header>

<router-outlet></router-outlet>

<app-footer></app-footer>

My login component typescript code which is loaded in <router-outlet></router-outlet> is:

import { Component, OnInit } from '@angular/core';
import { MatInput } from '@angular/material';
import { Router } from '@angular/router';

import { LoginService } from '../../services/login.service';
import { User } from '../../models/user';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  providers: [ LoginService ],
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
  public user = new User('', '', new Array<string>());
  public errorMsg = '';
  public isLoading = false;

  constructor(
    private loginService: LoginService,
    private router: Router
  ) { }

  ngOnInit() {
    if (this.loginService.getCurrentUser() !== null) {
      this.router.navigate(['home']);
    }
  }

  login() {
    this.isLoading = true;
    const obs = this.loginService.login(this.user);
    obs.subscribe(
      res => {
        if (res !== true) {
          this.errorMsg = 'Incorrect Username / Password';
          this.loginService.loginStatusChange(false);
        } else {
          this.loginService.loginStatusChange(true);
        }
      },
      err => {
        this.isLoading = false;
        this.errorMsg = err._body;
        this.loginService.loginStatusChange(false);
      },
      () => {
        this.isLoading = false;
      }
    );
    obs.connect();
  }
}

My header component typescript is:

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

import { User } from '../../models/user';

import { LoginService } from '../../services/login.service';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  providers: [ LoginService ],
  styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
  public currentUser: string;

  constructor(private loginService: LoginService) { }

  ngOnInit() {
    const currentUser = this.loginService.getCurrentUser();
    if (currentUser !== null) {
      this.currentUser = currentUser.username;
    }
    this.loginService.loginObservable
                  .map((status) => {
                    if (status) {
                      return this.loginService.getCurrentUser();
                    }
                    return null;
                  }
                )
                .subscribe((user) => {
                  const thisUser = this.loginService.getCurrentUser();
                  if (thisUser !== null) {
                    this.currentUser = thisUser.username;
                  }
                });
  }

  logout() {
    this.loginService.logout();
    this.loginService.loginStatusChange(false);
  }
}

And finally my header component view is:

<div id="wrapper">
  <section>
      <div id="topHeader">
          <div class="oLogo">
              <img id="OLogoImg" src="../../assets/images/Luceco-O-Logo-Transparent.png" alt="o-logo" height="20" />
          </div>
      </div>
  </section>
</div>
<div class="container body-content">
  <div id="header">
      <div class="pageWrap">
          <a id="logo" >
              <img id="logoImg" src="../../assets/images/Luceco-Logo-Transparent.png" alt="logo" height="28" />
          </a>
          <ul id="menu">
              <li id="home-menu" class="top-level home-menu">
              <a href="#">Home</a>
              </li>

<--FOLLOWING COMPONENT NEEDS TO BE DISPLAYED AFTER LOGIN -->

              <li *ngIf="currentUser != null" id="logout-menu" class="top-level logout-menu">
                <a href="#" (click)="logout()">Log Out</a>
                </li>
          </ul>
      </div>
  </div>

LoginService:

import { Injectable } from '@angular/core';
import { Http, Headers, RequestOptions, Response } from '@angular/http';
import { Router } from '@angular/router';

import 'rxjs/rx';
import { ConnectableObservable } from 'rxjs/rx';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';

import { User } from '../models/user';

@Injectable()
export class LoginService {
  private authApiUrl = 'http://192.168.1.201/ForkliftHelperAPI/api/Auth';

  private loginBehaviourSubject = new BehaviorSubject<boolean>(false);
  public loginObservable = this.loginBehaviourSubject.asObservable();

  constructor(private router: Router,
              private _http: Http) { }

  loginStatusChange(isLoggedIn: boolean) {
    this.loginBehaviourSubject.next(isLoggedIn);
  }

  login(user: User): ConnectableObservable<any> {
    let result: User;
    const body = JSON.stringify(user);
    const headers = new Headers({
      'Content-Type': 'application/json'
    });
    const options = new RequestOptions({
      headers: headers
    });
    const obsResponse = this._http.post(this.authApiUrl, body, options)
                                .map(res => res.json())
                                .publish();

    obsResponse.subscribe(
                (res: User) => {
                  result = res;
                  if (result) {
                    user.securityGroups = result.securityGroups;
                    sessionStorage.setItem('user', JSON.stringify(user));
                    this.router.navigate(['home']);
                  }
                },
                err => console.log(err)
    );
    return obsResponse;
  }

  logout() {
    sessionStorage.removeItem('user');
    this.router.navigate(['login']);
  }

  getCurrentUser() {
    const storedUser = JSON.parse(sessionStorage.getItem('user'));
    if (!storedUser) {
      return null;
    }
    return new User(storedUser.username, storedUser.password, storedUser.securityGroups);
  }

  isLoggedIn() {
    if (this.getCurrentUser() === null) {
      this.router.navigate(['login']);
    }
  }
}

So basically my issue is that when the login method of the LoginComponent is complete, i want it to set the currentUser variable within the HeaderComponent so that when the next page is produced by the router-outlet, the header displays the log out button correctly. The update happens correctly if you manually refresh the page after the redirect however, the header is not refreshed on redirect, only the contents of the router-outlet.

I have tried using a service as well as using @Input() and @Output() but have had no luck, maybe I have been using them incorrectly.

My main issue seems to be that when the redirects and navigation happens, the header and footer components are not refreshed as it is only the components within <router-outlet></router-outlet> that are affected. I did it this way in order to prevent having to have the header and footer components in every other component but if thats the only way to achieve what i require then so be it.

Any help would be greatly appreciated.

3
would appreciate if you could make a reproduction to see what you didamal
Most likely a dupJota.Toledo
Yes, a service is not used with @Input / @Output, this is to establish a communication between a parent / child component. Have a service holding the currentUser and manage its state with a getter / setter. Your components should then consume this data from the getter through dependency injection.Alex Beugnet
Declare an rxjs Subject in your LoginService, and use that to emit currentUser, subscribe to that Subject in your header component. You'll find plenty of answer here on SO regarding that. For example, see this answer, this is exactly what you are looking for: stackoverflow.com/a/46049546/1791913Faisal

3 Answers

0
votes

First of all, provide your LoginService to your root module instead of providing it in your Header and Login Component.

     @NgModule({
         // other things
      providers: [LoginService,..........],
      bootstrap: [AppComponent]
    })
    export class AppModule { }

You've to use EventEmiiter or Rxjs BehaviorSubject for this kind of component to component communication.

In general changing any value in one component doesn't trigger the change in other component unless you explicitly inform angular to do so. There are couple of mechanisms for doing this.

The best way would be to use a RxJs Subject or BehaviorSubject for this purpose.

You can create a BehaviorSubject in your loginService and the procedure is the following:

LoginService Class:

   import 'rxjs/add/operator/do';
   import 'rxjs/add/operator/share';

  export class LoginService {

       private loginbehaviorSubject = new BehaviorSubject<boolean>(true);
       public loginObservable$ = this.loginbehaviorSubject.asObservable();

      loginStatusChange(isLoggedIn: boolean){
         this.loginbehaviorSubject.next(isLoggedIn);
      }


      login(user: User): ConnectableObservable<any> {
        let result: User;
        const body = JSON.stringify(user);
        const headers = new Headers({
          'Content-Type': 'application/json'
        });
        const options = new RequestOptions({
          headers: headers
        });
        return this._http.post(this.authApiUrl, body, options)
                                    .map(res => res.json())
                                    .do( (res: User) => {
                                          result = res;
                                          if (result) {
                                            user.securityGroups = result.securityGroups;
                                            sessionStorage.setItem('user', JSON.stringify(user));
                                          }
                                        },
                                        err => console.log(err)
                                    )
                                    .share();
      }
      removeUserFromSession(){
          if(sessionStorage.getItem('user')){
              sessionStorage.removeItem('user');
          }       
      }
      logout() {
        this.removeUserFromSession();
        this.router.navigate(['login']);
      }

  }

In the LoginComponent:

  ngOnInit() {
    if (this.loginService.getCurrentUser() !== null) {
      this.router.navigate(['home']);
    } 
  }

  login() {
    this.isLoading = true;
    const obs = this.loginService.login(this.user);
    obs.subscribe(
      res => {
        this.loginService.loginStatusChange(true);
        if (res !== true) {
            this.errorMsg = 'Incorrect Username / Password';
        } else {
            this.router.navigate(['home']);
        }
      },
      err => {
        this.isLoading = false;
        this.errorMsg = err._body;
        this.loginService.loginStatusChange(true);
      },
      () => {
        this.isLoading = false;
      }
    );
}

In the HeaderComponent:

  export class HeaderComponent implements OnInit {
  public currentUser: string;

  constructor(private loginService: LoginService) { }

  ngOnInit() {
    const currentUser = this.loginService.getCurrentUser();
    if (currentUser !== null) {
      this.currentUser = currentUser.username;

    }

    this.loginService.loginObservable$
     .subscribe( (isStatusChanged) => {
         const currentUser = this.loginService.getCurrentUser();
         this.currentUser = currentUser.username;
     });
  }

  logout() {
    this.loginService.logout();
    this.loginService.loginStatusChange(true); // notice this line
  }
}
1
votes

You should go with an EventBus, create a singleton service it must be a provider inside of your main module and don't put it as a provider in another place.

The idea is:

login.component.ts

constructor(public eventBus: EventBus) {}

onLoginSuccess(currentUser: any): void {
   this.eventBus.onLoginSuccess.next(currentUser);
}

header.component.ts

constructor(public eventBus: EventBus) {
   this.eventBus.onLoginSuccess.subscribe((currentUser: any) => this.currentUser = currentUser);
}

eventbus.service.ts

@Injectable()
export class EventBus {
   onLoginSuccess: Subject<any> = new Subject();
}

Of course that you must handle the subscriptions and everything else, this is just a how-to.

When your user has finished with the login, the eventBus will hit the header component with the onLoginSuccess event.

0
votes

Make a singleton instance of LoginService.

@Injectable()
 export class LoginService {
    static instance: LoginService;
    constructor() {
    return LoginService.instance = LoginService.instance || this;
    }
}

Include this singleton service to both route-oulet component and header Component. In header component you can use ngOnChanges or ngDoCheck methods to watch the variable in login service and once its value is changed, the code inside the function will excute without refresh.

ngOnChanges(changes: SimpleChanges) {}

ngDoCheck() {}