Angular2 Notes II

  • Angular 2 Notes - Modules

  • API Service

  • First, configure the application to use server communication facilities.

The Angular Http client communicates with the server using a familiar HTTP request/response protocol. The Http client is one of a family of services in the Angular HTTP library.

E.g., main.ts

...
import { HttpModule} from '@angular/http';
//import { HttpModule, JsonpModule } from '@angular/http';
import { App, providers} from './app';
...
@NgModule({
declarations: [
App,
Main
],
providers,
imports: [BrowserModule, FormsModule, HttpModule],
bootstrap: [App]
})

When importing from the @angular/http module, SystemJS knows how to load services from the Angular HTTP library because the systemjs.config.js file maps to that module name.

JsonpModule isn’t necessary for plain HTTP.

  • Make a General Service

    E.g., api.ts
import { Injectable } from '@angular/core';
import { Headers, Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/Rx';
import 'rxjs/add/observable/throw';

@Injectable()
export class ApiService {
headers: Headers = new Headers({
'Content-Type': 'application/json',
Accept: 'application/json'
});

api_url: string = 'http://localhost:3500';

//Http client service is injected the constructor.
constructor(private http: Http) {}

//the response object doesn't hold the data in a form the app can use directly.
//we must parse the response data into a JSON object.
private getJson(response: Response) {
return response.json();
}

//always handle errors
private checkForError(response: Response): Response {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
var error = new Error(response.statusText)
error['response'] = response;
console.error(error);
throw error;
}
}

//it will return an Observable when the Http client fetched from the server.
//think of an Observable as a stream of events published by some source.
get(path: string): Observable<any> {
return this.http.get(`${this.api_url}${path}`, { headers: this.headers })
.map(this.checkForError)
.catch(err => Observable.throw(err))
.map(this.getJson)
}

post(path: string, body): Observable<any> {
return this.http.post(
`${this.api_url}${path}`,
JSON.stringify(body),
{ headers: this.headers }
)
.map(this.checkForError)
.catch(err => Observable.throw(err))
.map(this.getJson)
}

delete(path: string): Observable<any> {
return this.http.delete(
`${this.api_url}${path}`,
{ headers: this.headers }
)
.map(this.checkForError)
.catch(err => Observable.throw(err))
.map(this.getJson)
}
}

In fact, the http.get method returns an Observable of HTTP Responses (Observable) from the RxJS library and map is one of the RxJS operators.

  • Export providers
    E.g., ../index.ts
import * as services from './services'
export { App } from './app';

const mapValuesToArray = (obj) => Object.keys(obj).map(key => obj[key]);

export const providers = [
...mapValuesToArray(services)
];
  • Export services
    E.g., ../services/index.ts
export { ApiService } from './api';
export { NoteService } from './notes';
  • Implement a service

    E.g., note.ts
import { Injectable } from '@angular/core';
import { ApiService } from './api';
import 'rxjs/Rx';

@Injectable()
export class NoteService {
path: string = '/notes';
constructor(private apiService: ApiService) {}

createNote(note) {
return this.apiService.post(this.path, note)
}

getNotes() {
return this.apiService.get(this.path)
}

completeNote(note) {
return this.apiService.delete(`${this.path}/${note.id}`)
}
}
  • Apply a service

    E.g., notes.ts
import { NoteService } from '../services';
...
export class Notes {
notes = [];
//inject NoteService to constructor
constructor(private noteService: NoteService) {}

//Although at runtime the component requests heroes immediately after
//creation, you don't call the service's get method in the component's
//constructor. Instead, call it inside the ngOnInit()
ngOnInit() {
this.noteService.getNotes()
.subscribe(res => this.notes = res.data);
}

//golden rule: always delegate data access to a supporting service class.
onCreateNote(note) {
this.noteService.createNote(note)
.subscribe(note => this.notes.push(note));
}

onNoteChecked(note) {
this.noteService.completeNote(note)
.subscribe(note => {
const i = this.notes.findIndex(localNote => localNote.id === note.id);
this.notes.splice(i, 1);
});
}
}

This is a best practice. Components are easier to test and debug when their constructors are simple, and all real work (especially calling a remote server) is handled in a separate method.

To listen for events in this stream, subscribe to the Observable (Here, we have translated the obserable to json). These subscriptions specify the actions to take when the web request produces a success event (with the data in the event payload) or a fail event (with the error in the payload).

  • Promise Based

getHeroes (): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
addHero (name: string): Promise<Hero> {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this.heroesUrl, { name }, options)
.toPromise()
.then(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
return body.data || { };
}
private handleError (error: Response | any) {
// In a real world app, we might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Promise.reject(errMsg);
}

We can follow the promise then(this.extractData).catch(this.handleError) pattern as in this example.

While promises may be more familiar, observables have many advantages.

Alternatively, you can call toPromise(success, fail). The observable’s map callback moves to the first success parameter and its catch callback to the second fail parameter in this pattern: .toPromise(this.extractData, this.handleError).

Also, We have to adjust the calling component to expect a Promise instead of an observable:

getHeroes() {
this.heroService.getHeroes()
.then(
heroes => this.heroes = heroes,
error => this.errorMessage = <any>error);
}
addHero (name: string) {
if (!name) { return; }
this.heroService.addHero(name)
.then(
hero => this.heroes.push(hero),
error => this.errorMessage = <any>error);
}

The less obvious but critical difference is that these two methods return very different results.

The promise-based then returns another promise. You can keep chaining more then and catch calls, getting a new promise each time.
The subscribe method returns a Subscription. A Subscription is not another Observable. It’s the end of the line for observables. You can’t call map on it or call subscribe again. The Subscription object has a different purpose, signified by its primary method, unsubscribe.

  • Observalbe Based*

Concept
Observable in short addresses asynchronous processing and events. Comparing to promise, it can be described as observable = promise + events.

What is great with observables is that they are lazy. Observables can be canceled and you can apply some operators in them (like map, filter, …). This allows to handle asynchronous things in a very flexible way.

A great sample describing the strong power of observables is the way to connect a filter input to a corresponding filtered list. When the user enters characters, the list is refreshed. Observables handle corresponding AJAX requests and cancel previous in progress requests if another one is triggered by new value in the input. E.g.,

this.textValue.valueChanges 
.debounceTime(500)
.switchMap(data => this.httpService.getListValues(data))
.subscribe(data => console.log('new list values', data));

In action

  1. Manage the observable by your own. In this case, call the subscribe method on the observable and assign the result into an attribute of the component. Then use this attribute in the view to iterate over the collection:
@Component({
template: `
<h1>My Friends</h1>
<ul>
<li *ngFor="#frnd of result">
{{frnd.name}} is {{frnd.age}} years old.
</li>
</ul>
`,
directive:[CORE_DIRECTIVES]
})

export class FriendsList implement OnInit, OnDestroy {
result:Array<Object>;

constructor(http: Http) {
}

ngOnInit() {
this.friendsObservable = http.get('friends.json')
.map(response => response.json())
.subscribe(result => this.result = result);
}

ngOnDestroy() {
this.friendsObservable.dispose();
}
}

Returns from both get and map methods are the observable not the result (in the same way than with promises).

  1. Manage the observable by the Angular template. You can also use the a pipe to implicitly manage the observable. In this case, there is no need to explicitly call the subscribe method.
@Component({
template: `
<h1>My Friends</h1>
<ul>
<li *ngFor="#frnd of (result | async)">
{{frnd.name}} is {{frnd.age}} years old.
</li>
</ul>
`,
directive:[CORE_DIRECTIVES]
})

export class FriendsList implement OnInit {
result:Array<Object>;

constructor(http: Http) {
}

ngOnInit() {
this.result = http.get('friends.json')
.map(response => response.json());
}
}

Note that observables are lazy. So the corresponding HTTP request will be only called once when a listener attached on it is using the subscribe method.

The map method is used to extract the JSON content from the response and use it in the observable processing.

  • State Management

  • Make a Store

e.g., store.ts

import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Injectable } from '@angular/core';
import 'rxjs/Rx';

export interface Note {
color: string,
title: string,
value: string,
id?: string | number,
createdAt?: string,
updatedAt?: string,
userId?: string
}

export interface State {
notes: Array<Note>
}

const defaultState : State = {
notes: []
}

const _store = new BehaviorSubject<State>(defaultState);

@Injectable()
export class Store {
private _store = _store;
changes = this._store.asObservable().distinctUntilChanged()

setState(state: State) {
this._store.next(state);
}

getState(): State {
return this._store.value;
}

purge() {
this._store.next(defaultState);
}
}

  • Make a Store-Helper

e.g., store-helper.ts

import { Injectable } from '@angular/core';
import { Store } from '../store';

@Injectable()
export class StoreHelper {
constructor(private store: Store) {}

update(prop, state) {
const currentState = this.store.getState();
this.store.setState(Object.assign({}, currentState, { [prop]: state }));
}

add(prop, state) {
const currentState = this.store.getState();
const collection = currentState[prop];
this.store.setState(Object.assign({}, currentState, { [prop]: [state, ...collection] }));
}

findAndUpdate(prop, state) {
const currentState = this.store.getState();
const collection = currentState[prop];

this.store.setState(Object.assign({}, currentState, {[prop]: collection.map(item => {
if (item.id !== state.id) {
return item;
}
return Object.assign({}, item, state)
})}))
}

findAndDelete(prop, id) {
const currentState = this.store.getState();
const collection = currentState[prop];
this.store.setState(Object.assign({}, currentState, {[prop]: collection.filter(item => item.id !== id)}));
}
}
  • Implement a Store

In container note.ts,

import { Store } from '../store';
//...
export class Notes implements OnDestroy{
notes = [];
constructor(
private store: Store,
private noteService: NoteService
){
this.noteService.getNotes().subscribe();

// this.store.changes.pluck('notes')
// .subscribe((notes: any) => this.notes = notes);

this.store.changes
.map(data => data.notes)
.subscribe(notes => this.notes = notes);
}

onCreateNote(note) {
this.noteService.createNote(note)
.subscribe();
// .subscribe(note => this.notes.push(note));
}

onNoteChecked(note) {
this.noteService.completeNote(note)
.subscribe();
// .subscribe(note => {
// const i = this.notes.findIndex(localNote => localNote.id === note.id);
// this.notes.splice(i, 1);
// });
}
}

In service note.ts, keep the store up-to-date.

import { StoreHelper } from './store-helper';
//...
@Injectable()
export class NoteService {
path: string = '/notes';
constructor(
private apiService: ApiService,
private storeHelper: StoreHelper
) {}

createNote(note) {
return this.apiService.post(this.path, note)
.do(savedNote => this.storeHelper.add('notes', savedNote))
}

getNotes() {
return this.apiService.get(this.path)
.do(res => this.storeHelper.update('notes', res.data));
}

completeNote(note) {
return this.apiService.delete(`${this.path}/${note.id}`)
.do(res => this.storeHelper.findAndDelete('notes', res.id));
}
}

Ref:

  1. angular2 http client