HTTP

Отримання та збереження даних

Наші замовники оцінили наш прогрес. Тепер вони хочуть отримувати дані із сервера, дозволити користувачам додавати, редагувати, та видаляти героїв, і зберігати ці зміни знову на сервері.

В цьому уроці, ми навчимо наш застосунок робити відповідні HTTP-виклики API віддаленого вебсервера.

Запустіть живий приклад для цієї частини.

На чому ми зупинились

У попередньому уроці, ми навчились переходити між панеллю керування та фіксованим списком героїв, і, разом з тим, редагувати вибраних героїв. Це зараз для нас є відправною точкою.

Забезпечте запуск та транспіляцію застосунка

Відкрийте вікно терміналу/консолі. Ми хочемо запустити компілятор TypeScript, запустити веб-сервер та мати нагляд за зміною файлів. Все це забезпечить наступна команда:

npm start

Таким чином застосунок працюватиме, коли ми продовжимо будувати Тур Героїв.

Надання HTTP Сервісів

HttpModule не є базовим Angular-модулем. Angular надає можливість опціонально вибирати його для веб-доступу до даних, він знаходиться в @angular/http, що йде окремим файлом як частина npm-пекета Angular.

На щастя, ми можемо імпортувати його з @angular/http, оскільки у systemjs.config сказано, що SystemJS повинен завантажувати цю бібліотеку при потребі.

Реєстрація HTTP сервісів

Наш застосунок буде залежати від Angular-сервіса http, який сам залежить від інших сервісів. HttpModule з бібліотеки @angular/http має провайдерів для усього набору HTTP-сервісів.

Потрібно щоб ми мали доступ до цих сервісів у будь-якому місці нашого застосунка. Отже ми реєструємо їх усі додаючи HttpModule до переліку imports у AppModule, де відбуватиметься початкове завантаження застосунка та його кореневого AppComponent.

app/app.module.ts (v1)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Зауважте, що ми застосовуємо HttpModule як частину масиву imports у кореневому NgModule AppModule.

Симуляція веб API

Ми рекомендуємо реєструвати сервіси для усього застосунку у кореневому масиві providers у AppModule. Here we're registering in main for a special reason.

Наш застосунок є на ранній стадії розробки і нам ще далеко до продуктової версії. Ми навіть не маємо вебсервера, який може обробляти запити щоб отримувати героїв. Поки ми цього не маємо, ми зробимо симуляцію цієї функціональності.

Ми збираємось обдурити HTTP-клієнта, коли він буде отримувати та зберігати дані у макеті сервісу, точніше — у пам'яті веб API. The application itself doesn't need to know and shouldn't know about this. So we'll slip the in-memory web API into the configuration above the AppComponent.

Ось ця версія app/app.module.ts, яка буде виконувати зазначену симуляцію:

app/app.module.ts (v2)

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService), AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { }

Замість того, щоб вимагати реальний API-сервер, в цьому прикладі симулюється комунікація з віддаленим сервером через додавання InMemoryWebApiModule до переліку imports модуля, тим самим підміняючи Http-сервіс на боці сервера.

InMemoryWebApiModule.forRoot(InMemoryDataService),

Метод конфігурації forRoot приймає клас InMemoryDataService, де зберігається база даних:

app/in-memory-data.service.ts

import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { let heroes = [ {id: 11, name: 'Mr. Nice'}, {id: 12, name: 'Narco'}, {id: 13, name: 'Bombasto'}, {id: 14, name: 'Celeritas'}, {id: 15, name: 'Magneta'}, {id: 16, name: 'RubberMan'}, {id: 17, name: 'Dynama'}, {id: 18, name: 'Dr IQ'}, {id: 19, name: 'Magma'}, {id: 20, name: 'Tornado'} ]; return {heroes}; } }

Цей файл замінює mock-heroes.ts, який вже можна видалити.

Цей урок знайомить з бібліотекою Angular HTTP. Прохання не відволікатись на деталі описаної підміни бекенду. Просто дотримуйтесь цього прикладу.

Ми вчитимемо "веб API з пам'яті" трохи згодом, в уроці HTTP-клієнт. Пам'ятайте лише, що "веб API з пам'яті" корисні тільки на ранній стадії розробки та демонстрації такий прикладів як у Турі Героїв. Пропустіть це, якщо ви маєте реальний веб API-сервер.

Герої та HTTP

Гляньте на нашу поточну реалізацію HeroService

getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); }

Ми повертаємо Promise, який у свою чергу поверає макет героїв. На даний момент це може здатись зайвим, але ми знали що настане той день, коли отримуватимемо дані через HTTP-клієнт, причому це буде відбуватись асинхронно.

Цей день настав! Давайте конвертуємо getHeroes() щоб використовувати HTTP.

app/hero.service.ts (оновлений getHeroes з новими членами класу)

private heroesUrl = 'api/heroes'; // URL to web api constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json().data as Hero[]) .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error('An error occurred', error); // for demo purposes only return Promise.reject(error.message || error); }

Наші оновлені вирази імпорту зараз такі:

app/hero.service.ts (оновлені вирази імпорту)

import { Injectable } from '@angular/core'; import { Headers, Http } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Hero } from './hero';

Оновіть браузер, та дані героя повинні успішно завантажити із макету сервера.

HTTP Promise

Ми все ще повертаємо Promise, але робимо це по-іншому.

Angular http.get повертає RxJS Observable. Observables є потужним способом обробляти асинхронний потік даних. Ми дізнаємось про Observables трохи згодом у цьому уроці.

Зараз же, ми повернемось до знайомого способу через конвертування Observable у Promise використовуючи оператор toPromise.

.toPromise()

На жаль, Angular Observable не має оператора toPromise ... точніше — не як початкове налаштування. В Angular із Observable реалізовано лише сама необхідна база.

Існує безліч операторів, таких як toPromise, які розширюють Observable з корисними можливостями. Якщо нам потрібні ці можливості, ми повинні додавати ці оператори самостійно. Просто імпортуйте їх з бібліотеки RxJS наступним чином:

import 'rxjs/add/operator/toPromise';

You'll add more operators, and learn why you must do so, later in this tutorial.

Витягування даних з колбеку then

У функції зворотнього виклику then викликаємо метод json в об'єкті з типом даних Response щоб витягнути дані зсередини HTTP-відповіді від сервера.

.then(response => response.json().data as Hero[])

Відповідь приходить у форматі JSON та має єдину властивість data, де утримується масив героїв, що насправді нам і треба. Отже, ми беремо цей масив та повертаємо його як значення вирішення (англ. resolved) Promise.

Зверніть особливу увагу на форму даних, що повертаються із сервера. У цьому конкретному прикладі у веб API повертається об'єкт з властивістю data. Ваш API може повертати щось інше. Підлаштуйте код відповідно до того, що повертає ваш веб API.

Викликаючий метод нічого не знає про нашу симуляцію відповіді. Він отримує героїв у Promise точно так само як це він робив перед цим. Він не знає, що отримує дані героїв із макету сервера. Він також нічого не знає про конвертацію HTTP-відповіді у масив героїв. Це те що нам і треба, і це є метою делегування доступу даних для таких сервісів як HeroService.

Обробка помилок

В кінці getHeroes() ми ловимо (англ. catch) збої та передаємо їх до обробника помилок:

.catch(this.handleError);

Це крок є критичним! Ми повинні передбачати HTTP-збої, оскільки вони часто трапляються поза межами нашого контролю.

private handleError(error: any): Promise<any> { console.error('An error occurred', error); // for demo purposes only return Promise.reject(error.message || error); }

У цьому демо сервіса ми записуємо помилки в консоль; в реальному житті придумаємо щось краще.

Ми також вирішили повертати помилки у дружній формі для викликаючого методу у відкинутому promise, щоб він міг відображати користувачу правильні повідомлення про помилки.

Отримаємо героя по id

HeroDetailComponent запитує HeroService щоб отримати одного героя для редагування.

На даний момент HeroService отримує усіх героїв, а потім знаходить бажаного героя відфільтровуючи його по id. Це згодиться для симуляції, але коли ми отримуємо усіх героїв заради одного потрібного — це марнотратно. Більшість веб API підтримує запити по-id у формі api/hero/:id (наприклад, api/hero/11).

Оновимо метод HeroService.getHero щоб робити запити по-id, застосовуючи те, що ми щойно вивчили при написанні getHeroes:

getHero(id: number): Promise<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) .toPromise() .then(response => response.json().data as Hero) .catch(this.handleError); }

Код майже такий самий як у getHeroes. URL ідентифікує котрого із героїв ми хочемо отримати, цей URL формується на основі патерну api/hero/:id.

Ми також врахували той факт, що data у відповіді сервера являє собою об'єкт героя, а не масив.

Незмінні API getHeroes

Хоча ми зробили значні внутрішні зміни для getHeroes() та getHero(), публічна сигнатура не змінилась. Ми як і раніше повертаємо Promise в обох методах. Нам не потрібно оновлювати будь-які компоненти, які викликають їх.

Покищо наші замовники у захваті від інтеграції веб API. Тепер вони хочуть мати можливість створювати та видаляти героїв.

Давайте подивимось спочатку що станеться, коли ми спробуємо оновити деталі про героя.

Оновлення деталей про героя

Ми вже можемо редагувати ім'я героя у візуальному поданні для деталей про героя. Будемо далі випробовувати це. Коли ми вписумємо ім'я героя, воно оновлюється у заголовку візуального подання. Але коли ми повертаємось натискаючи кнопку Back, зміни втрачаються!

Перед цим, оновлення не втрачались. Що змінилось? Коли застосунок використовує список макету героїв, зміни впроваджуються безпосередньо до об'єкту героя в межах єдиного на весь застосунок спільного списку. Тепер, коли ми отримуємо дані із сервера, якщо ми хочемо затверджувати зміни, нам потрібно записувати їх знову на сервер.

Збереження деталей про героя

Давайте пересвідчимось, що редагування імені героя не втрачається. Почнемо з додавання, в кінець шаблону деталей про героя, кнопки збереження з подією click та з прив'язкою до нового метода компонента, що має назву save:

app/hero-detail.component.html (save)

<button (click)="save()">Save</button>

Метод save затверджує зміну імені героя використовуючи метод update сервісу героя після чого перекидає назад до попереднього візуального подання:

app/hero-detail.component.ts (save)

save(): void { this.heroService.update(this.hero) .then(() => this.goBack()); }

Метод update сервісу героїв

Загальна структура методу update схожа на getHeroes, хоча ми будемо використовувати HTTP-метод put для затвердження змін на боці сервера:

app/hero.service.ts (update)

private headers = new Headers({'Content-Type': 'application/json'}); update(hero: Hero): Promise<Hero> { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put(url, JSON.stringify(hero), {headers: this.headers}) .toPromise() .then(() => hero) .catch(this.handleError); }

Ми ідентифікуємо котрого із героїв сервер повинен оновлювати підставляючи id героя в URL. Тіло методу put — це рядок, де представлено героя у форматі JSON, отрманого в результаті виклику JSON.stringify. Ми ідентифікуємо тип даних вмісту тіла (application/json) у заголовку запита.

Оновіть браузер та спробуйте результат. Зміна імені героя тепер повинна затверджуватись.

Додавання героя

Щоб додати нового героя, нам потрібно знати його ім'я. Давайте використаємо елемент input для цієї мети, разом з кнопкою add.

Вставимо наступне у HTML-шаблон компонента, зразу після заголовка:

app/heroes.component.html (add)

<div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div>

У відповідь на подію клацання, викликаємо обробник компонента, а потім очищуємо елемент input, щоб туди можна було записувати інше ім'я.

app/heroes.component.ts (add)

add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.create(name) .then(hero => { this.heroes.push(hero); this.selectedHero = null; }); }

Коли передається непусте ім'я, обробник делегує створення названого героя до сервісу героя, а потім додає нового героя до нашого масиву.

Нарешті, ми реалізовуємо метод create у класі HeroService.

app/hero.service.ts (create)

create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) .toPromise() .then(res => res.json().data) .catch(this.handleError); }

Оновіть браузер та створіть деяких нових героїв!

Видалення героя

Дуже багато героїв? Давайте додамо кнопку delete для кожного героя у візуальному поданні героїв.

Додамо цей елемент кнопки до HTML-шаблону компоненту, прямо після імені героя у повторюваному тегу <li>:

<button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button>

Елемент <li> повинен мати приблизно такий вигляд:

app/heroes.component.html (li-element)

<li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span> <span>{{hero.name}}</span> <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button> </li>

На додаток до виклику методу delete компонента, код обробника клацання кнопки delete зупиняє поширення події клацання — ми не хочемо щоб обробник клацання елемента <li> спрацьовував, оскільки це спричинить вибір героя, якого ми зібрались видаляти!

Логіка обробника delete трохи складніша:

app/heroes.component.ts (delete)

delete(hero: Hero): void { this.heroService .delete(hero.id) .then(() => { this.heroes = this.heroes.filter(h => h !== hero); if (this.selectedHero === hero) { this.selectedHero = null; } }); }

Of course, we delegate hero deletion to the hero service, but the component is still responsible for updating the display: it removes the deleted hero from the масив and resets the selected hero if necessary.

We want our delete button to be placed at the far right of the hero entry. This extra CSS accomplishes that:

app/heroes.component.css (additions)

button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; }

Hero service delete method

The hero service's delete method uses the delete HTTP method to remove the hero from the server:

app/hero.service.ts (delete)

delete(id: number): Promise<void> { const url = `${this.heroesUrl}/${id}`; return this.http.delete(url, {headers: this.headers}) .toPromise() .then(() => null) .catch(this.handleError); }

Refresh the browser and try the new delete functionality.

Observables

Each Http service method returns an Observable of HTTP Response objects.

Our HeroService converts that Observable into a Promise and returns the promise to the caller. In this section we learn to return the Observable directly and discuss when and why that might be a good thing to do.

Background

An observable is a stream of events that we can process with array-like operators.

Angular core has basic support for observables. We developers augment that support with operators and extensions from the RxJS library. We'll see how shortly.

Recall that our HeroService quickly chained the toPromise operator to the Observable result of http.get. That operator converted the Observable into a Promise and we passed that promise back to the caller.

Converting to a promise is often a good choice. We typically ask http.get to fetch a single chunk of data. When we receive the data, we're done. A single result in the form of a promise is easy for the calling component to consume and it helps that promises are widely understood by JavaScript programmers.

But requests aren't always "one and done". We may start one request, then cancel it, and make a different request before the server has responded to the first request. Such a request-cancel-new-request sequence is difficult to implement with Promises. It's easy with Observables as we'll see.

Search-by-name

We're going to add a hero search feature to the Tour of Heroes. As the user types a name into a search box, we'll make repeated HTTP requests for heroes filtered by that name.

We start by creating HeroSearchService that sends search queries to our server's web api.

app/hero-search.service.ts

import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { Hero } from './hero'; @Injectable() export class HeroSearchService { constructor(private http: Http) {} search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map(response => response.json().data as Hero[]); } }

The http.get() call in HeroSearchService is similar to the one in the HeroService, although the URL now has a query string.

A more important difference: we no longer call toPromise. Instead we return the observable from the the htttp.get, after chaining it to another RxJS operator, map, to extract heroes from the response data.

RxJS operator chaining makes response processing easy and readable. See the discuss below about operators.

HeroSearchComponent

Let's create a new HeroSearchComponent that calls this new HeroSearchService.

The component template is simple — just a text box and a list of matching search results.

app/hero-search.component.html

<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div>

We'll also want to add styles for the new component.

app/hero-search.component.css

.search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box{ width: 200px; height: 20px; }

As the user types in the search box, a keyup event binding calls the component's search method with the new search box value.

The *ngFor repeats hero objects from the component's heroes property. No surprise there.

But, as we'll soon see, the heroes property is now an Observable of hero масивs, rather than just a hero масив. The *ngFor can't do anything with an Observable until we flow it through the async pipe (AsyncPipe). The async pipe subscribes to the Observable and produces the масив of heroes to *ngFor.

Time to create the HeroSearchComponent class and metadata.

app/hero-search.component.ts

import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; // Observable class extensions import 'rxjs/add/observable/of'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import { HeroSearchService } from './hero-search.service'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ], providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroSearchService: HeroSearchService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if there was no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: add real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } }

Search terms

Let's focus on the searchTerms:

private searchTerms = new Subject<string>(); // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); }

A Subject is a producer of an observable event stream; searchTerms produces an Observable of strings, the filter criteria for the name search.

Each call to search puts a new string into this subject's observable stream by calling next.

Initialize the heroes property (ngOnInit)

A Subject is also an Observable. We're going to turn the stream of search terms into a stream of Hero масивs and assign the result to the heroes property.

heroes: Observable<Hero[]>; ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if there was no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: add real error handling console.log(error); return Observable.of<Hero[]>([]); }); }

If we passed every user keystroke directly to the HeroSearchService, we'd unleash a storm of HTTP requests. Bad idea. We don't want to tax our server resources and burn through our cellular network data plan.

Fortunately, we can chain Observable operators to the string Observable that reduce the request flow. We'll make fewer calls to the HeroSearchService and still get timely results. Here's how:

The switchMap operator (formerly known as "flatMapLatest") is very clever.

Every qualifying key event can trigger an http method call. Even with a 300ms pause between requests, we could have multiple HTTP requests in flight and they may not return in the order sent.

switchMap preserves the original request order while returning only the observable from the most recent http method call. Results from prior calls are canceled and discarded.

We also short-circuit the http method call and return an observable containing an empty array if the search text is empty.

Note that canceling the HeroSearchService observable won't actually abort a pending HTTP request until the service supports that feature, a topic for another day. We are content for now to discard unwanted results.

Import RxJS operators

Most RxJS operators are not included in Angular's base Observable implementation. The base implementation includes only what Angular itself requires.

If we want more RxJS features, we have to extend Observable by importing the libraries in which they are defined. Here are all the RxJS imports this component needs:

app/hero-search.component.ts (rxjs imports)

import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; // Observable class extensions import 'rxjs/add/observable/of'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged';

The import 'rxjs/add/...' syntax may be unfamiliar. It's missing the usual list of symbols between the braces: {...}.

We don't need the operator symbols themselves. In each case, the mere act of importing the library loads and executes the library's script file which, in turn, adds the operator to the Observable class.

Add the search component to the dashboard

We add the hero search HTML element to the bottom of the DashboardComponent template.

app/dashboard.component.html

<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <hero-search></hero-search>

Finally, we import HeroSearchComponent from hero-search.component.ts and add it to the declarations масив:

app/app.module.ts (search)

declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, HeroSearchComponent ],

Run the app again, go to the Dashboard, and enter some text in the search box. At some point it might look like this.

Hero Search Component

Application structure and code

Review the sample source code in the for this chapter. Verify that we have the following structure:

angular-tour-of-heroes
app
app.component.ts
app.component.css
app.module.ts
app-routing.module.ts
dashboard.component.css
dashboard.component.html
dashboard.component.ts
hero.ts
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
hero-search.component.html (new)
hero-search.component.css (new)
hero-search.component.ts (new)
hero-search.service.ts (new)
hero.service.ts
heroes.component.css
heroes.component.html
heroes.component.ts
main.ts
in-memory-data.service.ts (new)
node_modules ...
index.html
package.json
styles.css
systemjs.config.js
tsconfig.json

Home Stretch

We are at the end of our journey for now, but we have accomplished a lot.

Here are the files we added or changed in this chapter.

import { Component } from '@angular/core'; @Component({ moduleId: module.id, selector: 'my-app', template: ` <h1>{{title}}</h1> <nav> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `, styleUrls: ['./app.component.css'] }) export class AppComponent { title = 'Tour of Heroes'; } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { AppRoutingModule } from './app-routing.module'; // Imports for loading & configuring the in-memory web api import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroService } from './hero.service'; import { HeroSearchComponent } from './hero-search.component'; @NgModule({ imports: [ BrowserModule, FormsModule, HttpModule, InMemoryWebApiModule.forRoot(InMemoryDataService), AppRoutingModule ], declarations: [ AppComponent, DashboardComponent, HeroDetailComponent, HeroesComponent, HeroSearchComponent ], providers: [ HeroService ], bootstrap: [ AppComponent ] }) export class AppModule { } import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ moduleId: module.id, selector: 'my-heroes', templateUrl: './heroes.component.html', styleUrls: [ './heroes.component.css' ] }) export class HeroesComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor( private heroService: HeroService, private router: Router) { } getHeroes(): void { this.heroService .getHeroes() .then(heroes => this.heroes = heroes); } add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.create(name) .then(hero => { this.heroes.push(hero); this.selectedHero = null; }); } delete(hero: Hero): void { this.heroService .delete(hero.id) .then(() => { this.heroes = this.heroes.filter(h => h !== hero); if (this.selectedHero === hero) { this.selectedHero = null; } }); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } gotoDetail(): void { this.router.navigate(['/detail', this.selectedHero.id]); } } <h2>My Heroes</h2> <div> <label>Hero name:</label> <input #heroName /> <button (click)="add(heroName.value); heroName.value=''"> Add </button> </div> <ul class="heroes"> <li *ngFor="let hero of heroes" (click)="onSelect(hero)" [class.selected]="hero === selectedHero"> <span class="badge">{{hero.id}}</span> <span>{{hero.name}}</span> <button class="delete" (click)="delete(hero); $event.stopPropagation()">x</button> </li> </ul> <div *ngIf="selectedHero"> <h2> {{selectedHero.name | uppercase}} is my hero </h2> <button (click)="gotoDetail()">View Details</button> </div> .selected { background-color: #CFD8DC !important; color: white; } .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .heroes li.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes .text { position: relative; top: -3px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } button { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button.delete { float:right; margin-top: 2px; margin-right: .8em; background-color: gray !important; color:white; } import 'rxjs/add/operator/switchMap'; import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ moduleId: module.id, selector: 'my-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { hero: Hero; constructor( private heroService: HeroService, private route: ActivatedRoute, private location: Location ) {} ngOnInit(): void { this.route.params .switchMap((params: Params) => this.heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); } save(): void { this.heroService.update(this.hero) .then(() => this.goBack()); } goBack(): void { this.location.back(); } } <div *ngIf="hero"> <h2>{{hero.name}} details!</h2> <div> <label>id: </label>{{hero.id}}</div> <div> <label>name: </label> <input [(ngModel)]="hero.name" placeholder="name" /> </div> <button (click)="goBack()">Back</button> <button (click)="save()">Save</button> </div> import { Injectable } from '@angular/core'; import { Headers, Http } from '@angular/http'; import 'rxjs/add/operator/toPromise'; import { Hero } from './hero'; @Injectable() export class HeroService { private headers = new Headers({'Content-Type': 'application/json'}); private heroesUrl = 'api/heroes'; // URL to web api constructor(private http: Http) { } getHeroes(): Promise<Hero[]> { return this.http.get(this.heroesUrl) .toPromise() .then(response => response.json().data as Hero[]) .catch(this.handleError); } getHero(id: number): Promise<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get(url) .toPromise() .then(response => response.json().data as Hero) .catch(this.handleError); } delete(id: number): Promise<void> { const url = `${this.heroesUrl}/${id}`; return this.http.delete(url, {headers: this.headers}) .toPromise() .then(() => null) .catch(this.handleError); } create(name: string): Promise<Hero> { return this.http .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) .toPromise() .then(res => res.json().data) .catch(this.handleError); } update(hero: Hero): Promise<Hero> { const url = `${this.heroesUrl}/${hero.id}`; return this.http .put(url, JSON.stringify(hero), {headers: this.headers}) .toPromise() .then(() => hero) .catch(this.handleError); } private handleError(error: any): Promise<any> { console.error('An error occurred', error); // for demo purposes only return Promise.reject(error.message || error); } } import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { let heroes = [ {id: 11, name: 'Mr. Nice'}, {id: 12, name: 'Narco'}, {id: 13, name: 'Bombasto'}, {id: 14, name: 'Celeritas'}, {id: 15, name: 'Magneta'}, {id: 16, name: 'RubberMan'}, {id: 17, name: 'Dynama'}, {id: 18, name: 'Dr IQ'}, {id: 19, name: 'Magma'}, {id: 20, name: 'Tornado'} ]; return {heroes}; } } import { Injectable } from '@angular/core'; import { Http } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/map'; import { Hero } from './hero'; @Injectable() export class HeroSearchService { constructor(private http: Http) {} search(term: string): Observable<Hero[]> { return this.http .get(`app/heroes/?name=${term}`) .map(response => response.json().data as Hero[]); } } import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; // Observable class extensions import 'rxjs/add/observable/of'; // Observable operators import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged'; import { HeroSearchService } from './hero-search.service'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ], providers: [HeroSearchService] }) export class HeroSearchComponent implements OnInit { heroes: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor( private heroSearchService: HeroSearchService, private router: Router) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes = this.searchTerms .debounceTime(300) // wait 300ms after each keystroke before considering the term .distinctUntilChanged() // ignore if next search term is same as previous .switchMap(term => term // switch to new observable each time the term changes // return the http search observable ? this.heroSearchService.search(term) // or the observable of empty heroes if there was no search term : Observable.of<Hero[]>([])) .catch(error => { // TODO: add real error handling console.log(error); return Observable.of<Hero[]>([]); }); } gotoDetail(hero: Hero): void { let link = ['/detail', hero.id]; this.router.navigate(link); } } <div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <div> <div *ngFor="let hero of heroes | async" (click)="gotoDetail(hero)" class="search-result" > {{hero.name}} </div> </div> </div> .search-result{ border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 20px; padding: 5px; background-color: white; cursor: pointer; } #search-box{ width: 200px; height: 20px; }

Next Step

Return to the learning path where you can read about the concepts and practices you discovered in this tutorial.