One of the most obvious and useful cases of the Observer Pattern is the one in which a single entity in your application unidirectionally communicates information to a field of listeners on the outside. These listeners would like to be able to attach and detach freely from the single broadcasting entity. A good initial example of this is the login/logout component.
The code, links, and a live example of this are available at .
Suppose you have the following skeleton application:
[app/login.component.ts] import {Component} from '@angular/core'; @Component({ selector: 'login', template: ` <button *ngIf="!loggedIn" (click)="loggedIn=true"> Login </button> <button *ngIf="loggedIn" (click)="loggedIn=false"> Logout </button> ` }) export class LoginComponent { loggedIn:boolean = false; }
As it presently exists, this component will allow you to toggle between the login/logout button, but there is no concept of shared application state, and other components cannot utilize the login state that this component would track.
You would like to introduce this state to a shared service that is operated using the Observer Pattern.
Begin by creating an empty service and injecting it into this component:
[app/authentication.service.ts] import {Injectable} from '@angular/core'; @Injectable() export class AuthService { private authState_: AuthState; } export const enum AuthState { LoggedIn, LoggedOut }
Notice that you are using a TypeScript const enum
to keep track of the user's authentication state.
If you're new to ES6 and TypeScript, these keywords may feel a bit bizarre to you. The const
keyword is from the ES6 specification, signifying that this value is read only once declared. In vanilla ES6, this will throw an error, usually SyntaxError
, at runtime. With TypeScript compilation though, const
will be caught at compile time.
The enum
keyword is an offering of TypeScript. It is not dissimilar to a regular object literal, but note that the enum
members do not have values.
Throughout the application, you will reference these via AuthState.LoggedIn
and AuthState.LoggedOut
. If you reference the compiled JavaScript that TypeScript generates, you will see that these are actually assigned integer values. But for the purposes of building large applications, this allows us to develop a centralized repository of possible AuthState
values without worrying about their actual values.
As the skeleton service currently exists, you are going to instantiate a Subject
that will emit AuthState
, but there is no way available currently to interact with it. You will set this up in a bit. First, you must inject this service into your component:
[app/app.module.ts] import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {LoginComponent} from './login.component'; import {AuthService} from './authentication.service'; @NgModule({ imports: [ BrowserModule ], declarations: [ LoginComponent ], providers: [ AuthService ], bootstrap: [ LoginComponent ] }) export class AppModule {}
This is all well and good, but the service is still unusable as is.
Note that the path you import your AuthService
from may vary depending on where it lies in your file tree.
The core of this service is to maintain a global application state. It should expose itself to the rest of the application by letting other parts say to the service, "Let me know whenever the state changes. Also, I'd like to know what the state is right now." The perfect tool for this task is BehaviorSubject
.
RxJS Subjects
also have several subclasses, and BehaviorSubject
is one of them. Fundamentally, it follows all the rhythms of Subjects
, but the main difference is that it will emit its current state to any observer that begins to listen to it, as if that event is entirely new. In cases like this, where you want to keep track of the state, this is extremely useful.
Add a private BehaviorSubject
(initialized to the LoggedOut
state) and a public Observable
to AuthService
:
[app/authentication.service.ts] import {Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; @Injectable() export class AuthService { private authManager_:BehaviorSubject<AuthState> = new BehaviorSubject(AuthState.LoggedOut); private authState_:AuthState; authChange:Observable<AuthState>; constructor() { this.authChange = this.authManager_.asObservable(); } } export const enum AuthState { LoggedIn, LoggedOut }
Recall that you do not want to expose the BehaviorSubject
instance to outside actors. Instead, you would like to offer only its Observable
component, which you can openly subscribe to. Furthermore, you would like to allow outside actors to set the authentication state, but only indirectly. This can be accomplished with the following methods:
[app/authentication.service.ts] import {Injectable} from '@angular/core'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; import {Observable} from 'rxjs/Observable'; @Injectable() export class AuthService { private authManager_:BehaviorSubject<AuthState> = new BehaviorSubject(AuthState.LoggedOut); private authState_:AuthState; authChange:Observable<AuthState>; constructor() { this.authChange = this.authManager_.asObservable(); } login():void { this.setAuthState_(AuthState.LoggedIn); } logout():void { this.setAuthState_(AuthState.LoggedOut); } emitAuthState():void { this.authManager_.next(this.authState_); } private setAuthState_(newAuthState:AuthState):void { this.authState_ = newAuthState; this.emitAuthState(); } } export const enum AuthState { LoggedIn, LoggedOut }
Outstanding! With all of this, outside actors will be able to subscribe to authChange Observable
and will indirectly control the state via login()
and logout()
.
Note that the Observable
component of BehaviorSubject
is named authChange
. Naming the different components of the elements in the Observer Pattern can be tricky. This naming convention was selected to represent what an event emitted from the Observable
actually meant. Quite literally, authChange
is the answer to the question, "What event am I observing?". Therefore, it makes good semantic sense that your component subscribes to authChanges
when the authentication state changes.
LoginComponent
does not yet utilize the service, so add in its newly created methods:
[app/login.component.ts] import {Component} from '@angular/core'; import {AuthService, AuthState} from './authentication.service'; @Component({ selector: 'login', template: ` <button *ngIf="!loggedIn" (click)="login()"> Login </button> <button *ngIf="loggedIn" (click)="logout()"> Logout </button> ` }) export class LoginComponent { loggedIn:boolean; constructor(private authService_:AuthService) { authService_.authChange.subscribe( newAuthState => this.loggedIn = (newAuthState === AuthState.LoggedIn)); } login():void { this.authService_.login(); } logout():void { this.authService_.logout(); } }
With all of this in place, you should be able to see your login/logout buttons function well. This means you have correctly incorporated Observable
into your component.
This recipe is a good example of conventions you're required to maintain when using public/private. Note that the injected service is declared as a private member and wrapped with public component member methods. Anything that another part of the application calls or anything that is used inside the template should be a public member.
Central to this implementation is that each component that is listening to Observable
has an idempotent handling of events that are emitted. Each time a new component is connected to Observable
, it instructs the service to emit whatever the current state is, using emitAuthState()
. Necessarily, all components don't behave any differently if they see the same state emitted multiple times in a row; they will only alter their behavior if they see a change in the state.
Notice how you have totally encapsulated the authentication state inside the authentication service, and at the same time, have exposed and utilized a reactive API for the entire application to build upon.
Two critical components of hooking into services such as these are the setup and teardown processes. A fastidious developer will have noticed that even if an instance of LoginComponent
is destroyed, the subscription to Observable
will still persist. This, of course, is extremely undesirable!
Fortunately, the subscribe()
method of Observables
returns an instance of Subscription, which exposes an unsubscribe()
method. You can therefore capture this instance upon the invocation of subscribe()
and then invoke it when the component is being torn down.
Similar to listener teardown in Angular 1, you must invoke the unsubscribe method when the component instance is being destroyed. Happily, the Angular 2 life cycle provides you with such a method, ngOnDestroy
, in which you can invoke unsubscribe()
:
[app/login.component.ts] import {Component, ngOnDestroy} from '@angular/core'; import {AuthService, AuthState} from './authentication.service'; import {Subscription} from 'rxjs/Subscription'; @Component({ selector: 'login', template: ` <button *ngIf="!loggedIn" (click)="login()"> Login </button> <button *ngIf="loggedIn" (click)="logout()"> Logout </button> ` }) export class LoginComponent implements OnDestroy { loggedIn:boolean; private authChangeSubscription_: Subscription; constructor(private authService_:AuthService) { this.authChangeSubscription_ = authService_.authChange.subscribe( newAuthState => this.loggedIn = (newAuthState === AuthState.LoggedIn)); } login():void { this.authService_.login(); } logout():void { this.authService_.logout(); } ngOnDestroy() { this.authChangeSubscription_.unsubscribe(); } }
Now your application is safe from memory leaks should any instance of this component ever be destroyed in the lifetime of your application.