2

I'm having problems subscribing to an observer that's called in a different component.

My Angular 4 app has a navigation bar containing some of the user information. The user information is obtained from a http request to a server on login. This is managed with the LoginService. At logout we set the user to null.

logIn() {
  return this.http
  .get(this.apiUrl)
  .map(response => response.json())
}

logOut() {
  return Observable.of(null)
}

From the login component it's pretty easy to get the user details using the service.

logIn() {
  this.loginService.logIn().subscribe((user) => {
    console.log('Logged in')
    this.user = user
    console.log(this.user)
  )
}

But I also need to get them from another component to set the values in the navbar. I think I need to use the navbar component to subscribe to the LoginService logIn() and logOut() functions, merge these observables, and if either of them trigger an event, update the user. But I can't subscribe to them and 2 days later I'm still stuck on this.

I've made a plunk to show the problem. When the login button is clicked, the user in the app.component should be set, and should be unset when the logout button is clicked.

Geraint Anderson
  • 3,234
  • 4
  • 28
  • 49
  • We have used a pattern where the service exposes an observable of the current state and some methods to update that state, which I wrote about here: http://blog.jonrshar.pe/2017/Apr/09/async-angular-data.html. I also wrote an answer showing the technique on a related question: https://stackoverflow.com/a/41554338/3001761 – jonrsharpe Aug 10 '17 at 16:35

2 Answers2

4

I'd just use BehaviorSubject instead of keeping the user as a property in LoginService. Then you can subscribe to it any time you need the current user. Logout is then just calling .next(null):

See you updated plnkr: https://plnkr.co/edit/soeqH4IiNhcanNqxdnLS?p=preview

LoginService

@Injectable()
export class LoginService {
  private apiUrl = 'https://api.github.com/users/geraintanderson'

  user$ = new BehaviorSubject(null);

  constructor(
    private http: Http
  ) {}

  logIn() {
    return this.http
      .get(this.apiUrl)
      .map(response => response.json())
      .do(user => this.user$.next(user))
  }

  logOut() {
    this.user$.next(null);
  }
}

Template:

<span *ngIf="loginService.user$ | async; else noUser; let user">
  Logged in as <b>{{ user.name }}</b>
</span>
<ng-template #noUser><span>Not logged in</span></ng-template>
martin
  • 93,354
  • 25
  • 191
  • 226
  • I'd recommend not exposing the subject directly, as that means any other consumer of the service could push data into the stream. – jonrsharpe Aug 10 '17 at 16:56
  • @jonrsharpe Sure, I was just too lazy to rewrite it. – martin Aug 10 '17 at 16:58
  • Why use a complex `BehaviorSubject` solution when a simpler solution is available? `BehaviorSubject` seems grossly overused unnecessarily. – DeborahK Aug 10 '17 at 17:35
1

A much simpler solution is to not expose an observable but instead just expose the data. I have updated your plunker here: https://plnkr.co/edit/ilfEtmxolC16vRKRiE0q?p=preview

The service:

import { Injectable } from '@angular/core'
import { Http } from '@angular/http'

import 'rxjs/add/operator/map'

@Injectable()
export class LoginService {
  user;

  private apiUrl = 'https://api.github.com/users/geraintanderson'

  constructor(
    private http: Http
  ) {}

  logIn() {
    return this.http
    .get(this.apiUrl)
    .map(response => response.json())
    .subscribe((user) => {
      console.log('Logged in')
      this.user = user
      console.log(JSON.stringify(this.user));
    )
  }

  logOut() {
    this.user = null;
    console.log('Logged out')
  }
}

The component:

import {Component, OnInit, VERSION} from '@angular/core'
import { LoginService } from './login.service'

@Component({
  selector: 'my-app',
  templateUrl: './src/app.component.html',
})
export class App implements OnInit {
  get user() {
    return this.loginService.user;
  }

  constructor(private loginService: LoginService) {
    this.angularVersion = `Angular version ${VERSION.full}`
  }
}

The html:

<div>
  <span *ngIf="user">Logged in as <b>{{user.name}}</b></span>
  <span *ngIf="!user">Not logged in</span>
  <h2>Shared services</h2><p>Created with {{angularVersion}}</p>
  <hr>
  <app-login></app-login>
</div>
DeborahK
  • 57,520
  • 12
  • 104
  • 129