Geraint Anderson
Geraint Anderson

Reputation: 3383

Share observables with other components or services

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.

Upvotes: 2

Views: 1136

Answers (2)

martin
martin

Reputation: 96891

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>

Upvotes: 4

DeborahK
DeborahK

Reputation: 60518

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>

Upvotes: 1

Related Questions