0

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.

DaRoGa
  • 2,196
  • 8
  • 34
  • 62
  • would appreciate if you could make a reproduction to see what you did – amal Oct 11 '17 at 15:49
  • 1
    Possible duplicate of [Angular2 - Share data between components using services](https://stackoverflow.com/questions/35273106/angular2-share-data-between-components-using-services) – Jota.Toledo Oct 11 '17 at 16:02
  • 1
    Most likely a dup – Jota.Toledo Oct 11 '17 at 16:02
  • 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 Oct 11 '17 at 16:04
  • 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: https://stackoverflow.com/a/46049546/1791913 – FAISAL Oct 11 '17 at 16:51

3 Answers3

1

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.

dlcardozo
  • 3,953
  • 1
  • 18
  • 22
  • This seems a good approach however, i am struggling to get it working, would you be able to expand on your answer to show the correct way to subscribe etc? – DaRoGa Oct 12 '17 at 07:54
  • I cant seem to get the HeaderComponent to detect the changes and adapt the display accordingly – DaRoGa Oct 12 '17 at 08:49
0

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
  }
}
asmmahmud
  • 4,844
  • 2
  • 40
  • 47
  • No, because the header is already initialized and `ngOnInit` will not fire again. This will not work. However, I agree with the part to provide service only once. – FAISAL Oct 11 '17 at 16:58
  • You shouldn't downvote your every answer. If you don't like any answer you can send a comment. You know this is utterly discouraging. You should communicate. This is how you will be able get a proper answer. – asmmahmud Oct 11 '17 at 17:00
  • I have downvoted because this answer does not solve the issue. Dont mean to discourage at all :) upvoted, now atleast correct the answer following this approach: https://stackoverflow.com/a/46049546/1791913 – FAISAL Oct 11 '17 at 17:02
  • Have you tried to use EventEmitter or `BehaviorSubject` for the communication purpose? BTW, you should give a snippet of your `LoginService` class. – asmmahmud Oct 11 '17 at 17:06
  • This doesnt update the header. am i missing something? I will update my code with the new version and include the login service code – DaRoGa Oct 12 '17 at 07:34
  • I've checked your updated code and updated my answer accordingly – asmmahmud Oct 12 '17 at 09:00
  • currentUser used in the .subscribe() in HeaderComponent is always null – DaRoGa Oct 12 '17 at 09:49
  • @asmmahmud i seem to have it working. however, my main problem now is that if somebody refreshes the page, the loginObservable$ status is reset to false and therefore it disappears. is there a way around this? – DaRoGa Oct 12 '17 at 10:20
  • this can also be fixed but that is another issue. You can ask another answer for that. – asmmahmud Oct 12 '17 at 10:23
  • I have changed all sessionStorage to localStorage. is it this easy or is there something else to it? as this doesnt work – DaRoGa Oct 12 '17 at 10:29
  • SessionStorage should also work. There is some other issue surely. – asmmahmud Oct 12 '17 at 10:31
0

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() {}