0
votes

My component structure looks roughly like this. My app component has a nav bar and router outlet. The nav bar has logo, some generic links and some specific links to be shown on user login and authentication only. The router outlet loads the home component or the wall component based on the routing url. The home component contains the login component which contains the customary user id, password and submit button. On submit, and upon successful login, the login component emits an event. Now, how do I catch that event in the home component (parent)?

If I were to use the home selector directly under app, I could catch the event, bubble it up to app and then make the hidden links in the nav bar visible.

I am unaware how to catch the event emitted by login component in home component since it is loaded in the router output.

<!-- app.html -->
<div>
  <nav>
    <!-- Product logo -->
    <!-- Some generic links -->
    <!-- some hidden icons to be shown on authentication -->
  </nav>
  <router-outlet></router-outlet>
</div>

<!-- home.html -->
<div>
  <login></login>
</div>

<!-- login.html -->
<div>
  <!-- user name and password -->
  <!-- submit button - the associated ts file raises an event on successful login -->
</div>

<!-- wall.html -->
<div>
  <!-- Content to be displayed on authentication -->
</div>

Thanks, Shilpa

3
Why not create a an auth service that you inject into both components. In the auth service, you setup an event emitter that you fire when authenticating. Any component that inject your service should be able to subscribe to the event from your auth serviceMatthew Brown
I can provide an example if neededMatthew Brown
Matthew, thanks very much. If you could please provide sample code for subscribing to the event emitted by the service, that would be most helpful.Shilpa Nagavara
I tried all answers provided by Mark Rajcok here --> stackoverflow.com/questions/34376854/… Event Emitter, Observable and Subject. The problem is... even though the objects (event emitter, observable and subject) are defined and instantiated at the beginning of the service, they become null at the place where the event is supposed to be fired. So, I get an error saying 'Cannot read property 'emit' of undefined' Because of this the subscription in the observing component is failing. What am I doing wrong?Shilpa Nagavara
I'd need a plunker of the full class implementation to help you there, but take a look at how I do it belowMatthew Brown

3 Answers

0
votes

I figured out the issue, although I still don't have a solution. Turns out the method being called on map cannot access the class level objects. If I call emit the event on authentication, it works perfectly fine. I tried passing the event emitter object to the mapped method but that isn't helping either. How do I pass the object to mapped method scope?

The code in my service looks like this:

authenticate(authRequest: login): Observable<user> {
        let url: string = this.apiUrl + AppSettings.LOGIN_SERVICE;
        let headers = new Headers({
            'Content-Type': AppSettings.CONTENT_TYPE_HEADER
        });

        let options = new RequestOptions({ headers: headers });

        return this._http.post(url, authRequest, options) // ...using post request
            .map(this.authenticated)
            .catch(this.handleError);

        //.catch(this.handleError);
    }

    private authenticated(res: Response) {
        let body = res.json();
        if (body.StatusCode === 200) {
            localStorage.setItem('auth_token', res.headers.get("access-token"));
          //This is the event declared at the class level.
          //The following line of code is the one giving the error - cannot execute emit on  undefined
            this.authEvent.emit(true);
            return body.Data || {};
        }
        else {
            return {};
        }
    }
0
votes

Sorry for delay, here is how I do it:

auth.service.ts

export class AuthService {

    authChanged: EventEmitter<any> = new EventEmitter();

    postLogin(f: ILoginForm) {
      return this.http.post('/login', f)
      .map(res => res.json())
      .subscribe(data => this._checkLoginResponse(data));
    }

    /**
     * Check Login Result
     * @param data
     */
    private _checkLoginResponse(data: any) {
      // If Successful Login
      if (data.data && data.meta && data.meta.token) {

        // Set User & Token
        localStorage.setItem('inctoken', data.meta.token);
        localStorage.setItem('incuser', JSON.stringify(data.data));

        // Emit Auth & User Events
        this.authChanged.emit(this.getUser());

        // Show OK Login Flash Message
        this.flash.success('You have been logged in successfully.');

        // Navigate Home
        this.injector.get(Router).navigate(['/']);
      }
    }

    /**
     * Logout of Interface
     * @returns boolean
     */
    logout(withMsg = false): boolean {
      // # Remove Token & User from LS
      localStorage.removeItem('inctoken');
      localStorage.removeItem('incuser');

      // Emit Auth Events
      this.authChanged.emit(false);

      // Show Flash Message
      if (withMsg)
        this.flash.info('You have been logged out.', 'OK.');

      // Redirect to Login Page
      this.injector.get(Router).navigate(['/login']);
      return true;
    }
}

Then any component can inject AuthService and know when the authedUser changes, logs out, etc. Take my Navbar for example:

navbar.component.ts

export class NavbarComponent {

  /**
   * Authed User
   */
  authedUser: IUser;


  constructor(private authService: AuthService) {
    this.authService.authChanged
      .subscribe((user?: IUser) => {
        // user will be false if logged out
        // or user object if logged in. 
        this.authedUser = user;
      });
    }
}

Then in my navbar template I can choose to display things based on auth status:

<nav class="navbar">

  <!-- Truncated -->
  <ul class="menu-item"
      *ngIf="authedUser">
      <li><img [src]="authedUser.avatar"></li>
      <!-- Whatever -->
  </ul>

  <ul class="menu-item"
      *ngIf="!authedUser">
      <li><a [routerLink]="['/login']">Login</a></li>
      <!-- Whatever -->
  </ul>

</nav>  

Obviously a lot of these classes are truncated for brevity.

If you're curious about how the token gets sent in the header, here is a light example of the HttpClient class I created (truncated for brevity):

http.service.ts

export class HttpClient {

  constructor(private http: Http) {
    // ...
  }

  /**
   * Get
   * @param url
   * @returns {Observable<any>}
   */
  get(url): Observable<any> {

    // Create New Headers
    let headers = new Headers();

    // Set Authorization Header
    this._createAuthorizationHeader(headers);

    // Create Observable to Return to Calling Service.
    // We Dont Just Return the HTTP Observable Because
    // We Need to Subscribe Here to Catch The Errors That
    // May Be Thrown, Otherwise, Every Service Would Need
    // To Call The Handle Errors Method
    return Observable.create((observer) => {

      // Fire Http GET Request, Subscribe & Catch Errors
      return this.http.get(this.baseUrl + url, {
        headers: headers
      }).subscribe(
        data => observer.next(data),    // Emit Data Returned
        err => this.handleError(err),   // Catch Errors
        () => observer.complete()       // Emit Completed
      );
    });
  }

  /**
   * Create Authorization Header
   * @param {Headers} headers
   */
  private _createAuthorizationHeader(headers: Headers) {

    // If We Have A Token, Append It.  The
    // API Server Will Determine Its Validity
    if (localStorage.getItem('inctoken')) {
      headers.append('Authorization', 'Bearer: ' + localStorage.getItem('inctoken'));
    }
  }
}

Then in my other components I can just inject my HttpClient class and use it to have token automatically placed in headers, I.E.

some.component.ts

export class SomeComponent {

  constructor(private http: HttpClient) {
    // ...
  }

  private _getSomeData() {
    return this.get('/someurl')
      .map(res => res.json();
  }
}
0
votes

Chanced upon this solution --> http://plnkr.co/edit/KfcdDi?p=info

//alert.service.ts

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

@Injectable()
export class AlertService {
    private subject = new Subject<any>();
    private keepAfterNavigationChange = false;

    constructor(private router: Router) {
        // clear alert message on route change
        router.events.subscribe(event => {
            if (event instanceof NavigationStart) {
                if (this.keepAfterNavigationChange) {
                    // only keep for a single location change
                    this.keepAfterNavigationChange = false;
                } else {
                    // clear alert
                    this.subject.next();
                }
            }
        });
    }

    success(message: string, keepAfterNavigationChange = false) {
        this.keepAfterNavigationChange = keepAfterNavigationChange;
        this.subject.next({ type: 'success', text: message });
    }

    error(message: string, keepAfterNavigationChange = false) {
        this.keepAfterNavigationChange = keepAfterNavigationChange;
        this.subject.next({ type: 'error', text: message });
    }

    getMessage(): Observable<any> {
        return this.subject.asObservable();
    }
}

//login.component

private loggedIn(user1: user) {
        this.currentUser = user1;
        this._alertService.alert("login", true);
     
    }

//app.component

ngOnInit(): void {
        this.authenticated = this._authService.isLoggedIn();
        this._alertService.getMessage().subscribe(data => this.setData(data));
    }

private setData(data: any) {
        if (!this.authenticated) {
            if (data && (data.type === 'login') && data.success === true) {
                this.authenticated = true;
            }
            else {
                this.authenticated = false;
            }
        }
    }

<!-- app.html -->
    <nav class="navbar navbar-color">
        <div class="container-fluid" id="nav_center">
            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed nav-expand-button" 
                        data-toggle="collapse" data-target="#navbar-collapse1" aria-expanded="false"
                        *ngIf="authenticated">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <button type="button" class="nav-features nav-expand-button"
                        (click)="isCollapsed = !isCollapsed" *ngIf="authenticated">
                    <span class="sr-only">Navigate features</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="#">Outili</a>
            </div>
            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="navbar-collapse1" *ngIf="authenticated">
                <!--*ngIf="showNotification">-->
                <ul class="nav navbar-nav navbar-right">
                    <li class="navbar-icons">
                        <a href="#" class="navbar-a">
                            <span class="glyphicon glyphicon-inbox navbar-icons"></span>
                        </a>
                    </li>
                    <li class="dropdown navbar-icons">
                        <a href="#" class="dropdown-toggle navbar-a" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
                            <span class="glyphicon glyphicon-user navbar-icons"></span>
                            <span class="caret navbar-icons"></span>
                        </a>
                        <ul class="dropdown-menu">
                            <li><a href="#">Profile</a></li>
                            <li><a href="#">Settings</a></li>
                            <li role="separator" class="divider"></li>
                            <li (click)="logout()"><button type="button" class="btn">Logout</button></li>
                        </ul>
                    </li>
                </ul>
            </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>

The alert service here does exactly what I am looking for.