Angular2 observe an attribute in a service for changes

1.2k views Asked by At

I am new to Angular2, and am trying to figure out the design pattern for parent and child components to perform actions when an attribute in a service changes, this service is available globally (declared in app.component.ts in the providers attribute) . I am using:

  • Angular 2.2.1
  • rxjs 5.0.0-beta.12
  • typescript 2.2.1

Basically the service connects to a remote API, and this API requires token authentication. The service has public login() and logout() functions. The former makes a call to the API and then sets a private attribute token to the token string (if valid login) or '' (if invalid login). All other public functions that make calls to the API use this private token.

I have assumed that there is no need for the components that use this service to see or use the token attribute, because they would should be 'dumb' to the business processes of the API, and only consume the data via this service.

Service (simplified)

import { Injectable } from '@angular/core';
import { Http, Response, Headers, URLSearchParams } from '@angular/http';
import {Observable, Subject} from 'rxjs/Rx';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';

@Injectable()
export class ApiService {
  private url:string = 'my.service.com/';
  private username: string;
  private password: string;
  private token: string;

  constructor (private http: Http) {}

  public getUsername(): string {
    return this.username;
  }

  public setUsername(username: string) {
    this.username = username;
  }

  public getPassword(): string {
    return this.password;
  }

  public setPassword(password: string) {
    this.password = password;
  }

  private setToken(token: string) {
    this.token = token;
  }

  public login() {
    let headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' });
    let urlSearchParams = new URLSearchParams();
    urlSearchParams.append('username', this.username);
    urlSearchParams.append('password', this.password);
    let body = urlSearchParams.toString();
    let url = this.url + 'user/login';
    this.http.post(url, body, {headers: headers})
      .map(res => {this.setToken(typeof res['token'] !== 'undefined' ?  res['token'] : '');});
  }

  public logout() {
    this.setToken('');
  }

  public getData() {
    let headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' });
    let urlSearchParams = new URLSearchParams();
    urlSearchParams.append('token', this.token);
    let body = urlSearchParams.toString();
    let url = this.url + 'data';
    return this.http.get(url, body, {headers: headers})
      .map(res => res.json());
  }

app.component.ts

import { Component, ViewEncapsulation } from '@angular/core';
import { ApiService } from './services/api';

@Component({
  selector: 'app',
  encapsulation: ViewEncapsulation.None,
  styleUrls: ['./app.component.global.scss'],
  templateUrl: './app.component.html',
  providers: [ApiService]
})

export class AppComponent {
  constructor(private apiService: ApiService) {}
}

child component

import { Component } from '@angular/core';
import { ApiService } from '../services/api';

@Component({
  selector: 'child-component',
  styleUrls: ['./child-component.component.scss'],
  templateUrl: './child-component.component.html'
})

export class ChildComponent {
  data: any[];
  constructor(private apiService: ApiService) {}
}

How do I get the components (like ChildComponent to observe the state of the private attribute token in ApiService, and when it is not '', to call functions in the service that require a token (e.g. getData())? Perhaps I am approaching this in the wrong way, but my reasons for approaching it in this way are that there are several child components reliant on a valid token to fetch/send and display data, and should only fetch/send data when successfully logged in. I have been experimenting with various ngOnChanges, Observerable, Subject.emit, etc, but not been able to make this work.

2

There are 2 answers

2
Johan Blomgren On

The child components should not have to know about the state of the service.

when a child-component uses getData() the service can check if it has a valid token, if not it can get one via login and then execute the GET.

if you want to stop the user from loading the component if the token is missing you can use route guards.

Maybe not what you need but good reading about guards here: http://blog.thoughtram.io/angular/2016/07/18/guards-in-angular-2.html

0
vim On

Thanks to Johan, however, I want the page to be available and have some child components to only populate with data when the user is logged in.

I found the solution on Delegation: EventEmitter or Observable in Angular2.

In this way, I can prevent the child components making a request until token is changed (i.e. login() was called successfully):

import { Injectable } from '@angular/core';
import { Http, Response, Headers, URLSearchParams } from '@angular/http';
import {Observable, Subject} from 'rxjs/Rx';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';

@Injectable()
export class ApiService {
  private url:string = 'my.service.com/';
  private username: string;
  private password: string;
  private token: string;

  constructor (private http: Http) {}

  private setToken(token: string) {
    this.tokenValue = token;
    this.token.next(token);
  }

  public login() {
    let headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' });
    let urlSearchParams = new URLSearchParams();
    urlSearchParams.append('username', this.username);
    urlSearchParams.append('password', this.password);
    let body = urlSearchParams.toString();
    let url = this.url + 'user/login';
    this.http.post(url, body, {headers: headers})
      .map(res => {this.setToken(typeof res['token'] !== 'undefined' ?  res['token'] : '');});
  }

  public logout() {
    this.setToken('');
  }

  public getData() {
    let headers = new Headers({ 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' });
    let urlSearchParams = new URLSearchParams();
    urlSearchParams.append('token', this.token);
    let body = urlSearchParams.toString();
    let url = this.url + 'data';
    return this.http.get(url, body, {headers: headers})
  .map(res => res.json());
  }

child component

import { Component } from '@angular/core';
import { ApiService } from '../services/api';
import { Subscription } from 'rxjs/Rx';

@Component({
  selector: 'child-component',
  styleUrls: ['./child-component.component.scss'],
  templateUrl: './child-component.component.html'
})

export class ChildComponent {
  tokenSubscription: Subscription;
  data: any[];
  constructor(private apiService: ApiService) {}

 ngOnInit() {
  this.tokenSubscription = this.apiService.changeToken$
  .subscribe(token => { this.apiService.getData().subscribe(res => this.data = res.json()));
 }

 ngOnDestroy() {
  this.tokenSubscription.unsubscribe();
  }
}