HTTP
En este tutorial añadiremos las siguientes características sobre persistencia de datos, con la ayuda de HttpClient
de Angular.
HeroService
obtendrá datos mediante peticiones HTTP.- Los usuarios podrán añadir, editar y borrar héroes y guardar estos cambios por HTTP.
- Los usuarios podrán buscar héroes por nombre.
Cuando hayas terminado con esta página, la aplicación debería parecerse a este ejemplo / descarga ejemplo
Habilitar servicios HHTP
HttpClient
es el mecanismo de Angular para comunicarse con un servidor remoto a través de HTTP.
Para hacer que HttpClient
esté disponible en cualquier punto de la aplicación:
- abre el
AppModule
raíz. - importa el símbolo
HttpClientModule
de@angular/common/http
- añádelo al array
@NgModule.imports
.
Simular un servidor de datos
Este tutorial imita la comunicación con un servidor de datos remoto usando el módulo In-memory web API.
Después de instalar el módulo, la aplicación hará peticiones y recibirá respuestas de HttpClient
sin saber que In-memory web API intercepta estas peticiones, aplicándolas a un almacén de datos en memoria (In-memory) y devolviendo respuestas simuladas.
Esta funcionalidad es muy práctica para este tutorial. No tendremos que configurar un servidor para aprender acerca de HttpClient
.
También puede ser de utilidad en las fases iniciales de desarrollo de tu propia aplicación, cuando la API del servidor no está aún correctamente definida o no implementada
Importate: el módulo In-memory Web API no guarda ninguna relación con HTTP de Angular.
Ai sólo estás leyendo este tutorial para aprender acerca de
HttpClient
, puedes saltarte este paso. Si estás escribiendo el código, sigue las instrucciones a continuación para añadir In-memory Web API.
Desde npm, instala In-memory Web API.
1 2 3 |
npm install angular-in-memory-web-api --save |
Importa InMemoryWebApiModule
y InMemoryWebApiModule
, que crearemos a continuación.
1 2 |
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; |
Añade InMemoryWebApiModule
al array @NgModule.imports
–después de importar HttpClient, – mientras lo configuras con InMemoryDataService
,
1 2 3 4 5 6 7 8 |
HttpClientModule, // The HttpClientInMemoryWebApiModule module intercepts HTTP requests // and returns simulated server responses. // Remove it when a real server is ready to receive requests. HttpClientInMemoryWebApiModule.forRoot( InMemoryDataService, { dataEncapsulation: false } ) |
El método de configuración forRoot()
usa una clase InMemoryDataService
que hace que la base de datos en memoria este disponible.
El ejemplo de Tour de Héroes crea esta clase src/app/in-memory-data.service.ts
con el siguiente contenido:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { const 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}; } } |
Este fichero reemplaza mock-heroes.ts
, que ya podemos borrar.
Cuando el servidor esté listo, podemos desactivar In-memory Web API y las peticiones de la aplicación irán contra el servidor.
Ahora volvemos con HttpClient
.
Héroes y HTTP
Importamos algunos símbolos HTTP que vamos a necesitar.
1 |
import { HttpClient, HttpHeaders } from '@angular/common/http'; |
Inyecta HttpClient
en el constructor usando una propiedad privada llamada http
.
1 2 3 |
constructor( private http: HttpClient, private messageService: MessageService) { } |
Sigue inyectado MessageService
. Lo llamaremos tan a menudo que es conveniente tenerlo en un método privado llamado log
.
1 2 3 4 |
/** Log a HeroService message with the MessageService */ private log(message: string) { this.messageService.add('HeroService: ' + message); } |
Define HeroesUrl
con la dirección del servidor que apunta a los héroes.
1 |
private heroesUrl = 'api/heroes'; // URL to web api |
Obtener los héroes con HttpCLient
Actualmente HeroService.getHeroes()
usa la función of()
de RxJS para devolver un array de héroes simulados como un Observable<Hero[]>
.
1 2 3 |
getHeroes(): Observable<Hero[]> { return of(HEROES); } |
Modifica este método para usar HttpClient
.
1 2 3 4 |
/** GET heroes from the server */ getHeroes (): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) } |
Actualiza el navegador. Los datos del héroe debería cargarse correctamente del servidor simulado.
Hemos intercambiado of
por http.get
y la aplicación sigue funcionando sin ningún otro cambio porque ambos métodos devuelven un Observable<Hero[]>
.
Los métodos Http devuelven un valor
Todos los métodos HttpClient
devuelven un Observable
de RxJS de algo.
HTTP es un protocolo de petición/respuesta. Hacemos una petición y devuelve una única respuesta.
En general, un Observable
puede devolver múltiples valores. Un Observable
de HttpClient
siempre emite un único valor y finaliza, no vuelve a emitir.
Esta llamada en particular devuelve un Observable<Hero[]>
, literalmente un observable de array de héroes. En la práctica, sólo devolverá un único array de héroes.
HttpClient.get devuelve los datos de respuesta
HttpClient.get
devuelve por defecto el cuerpo de la respuesta como un objeto JSON sin tipo. Aplicando el indicador de tipo <Hero>
, nos devuelve un objeto tipado.
La forma del JSON viene definida por el API del servidor de datos. El API de Tour de Héroes devuelve los datos como un array.
Otras APIs pueden ‘enterrar’ los datos necesarios dentro de un objeto. Es posible que tengamos que ‘cavar’ en esos datos procesando el
Observable
de respuesta con el operadormap
de RxJS.
Manejo de errores
Las cosas pueden fallar, especialmente cuando se obtienen datos de un servidor remoto. El método HeroService.getHeroes()
capturar posibles errores y gestionarlos adecuadamente.
Para capturar el error, haremos «pipe» sobre el observable resultante de http.get()
, mediante el operador de RxJS catchError()
.
Importa el símbolo catchError()
de rxjs/operators
, junto con otros operadores que necesitaremos más adelante.
1 |
import { catchError, map, tap } from 'rxjs/operators'; |
Ahora, extiende el resultado del observable con el método .pipe()
y dale el operador catchError()
.
1 2 3 4 5 6 |
getHeroes (): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( catchError(this.handleError('getHeroes', [])) ); } |
El operador catchError()
intercepta un Observable que haya fallado. Le pasa el error a un manejador de errores
que puede hacer lo que considere con el error.
El siguiente método handleError()
informa del error y devuelve un resultado inocuo, de forma que la aplicación sigue funcionando.
handleError
El siguiente errorHandler
estará compartido por varios métodos de HeroService
así que debería cubrir diferentes necesidades.
En lugar de manejar el error directamente, devuelve una función manejadora de errores a catchError
que está configurada con el nombre de la operación fallida y un valor de retorno seguro.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * Handle Http operation that failed. * Let the app continue. * @param operation - name of the operation that failed * @param result - optional value to return as the observable result */ private handleError<T> (operation = 'operation', result?: T) { return (error: any): Observable<T> => { // TODO: send the error to remote logging infrastructure console.error(error); // log to console instead // TODO: better job of transforming error for user consumption this.log(`${operation} failed: ${error.message}`); // Let the app keep running by returning an empty result. return of(result as T); }; } |
Después de reportar el error a la consola, el manejador construye un mensaje coherente para el usuario final y devuelve un valor seguro a la aplicación para que siga funcionando,
Como cada método del servicio devuelve un tipo distinto de Observable
, errorHandler()
toma un parámetro ‘tipo’, de modo que pueda devolver un valor del tipo que la aplicación espera.
Interceptar el Observable
Los métodos de HeroService
interceptarán el flujo de los valores del observable y enviarán un mensaje (vía log) al área de mensajes el final de la página.
Esto se hará con el operador de RxJS tap
, el cual mira el valor de los observables, hace algo con estos valores y los pasa. La llamada de vuelta de tap
no modifica propiamente los valores.
Aquí está la versión final de getHeroes
con el tap
que registra (log) la operación.
1 2 3 4 5 6 7 8 |
/** GET heroes from the server */ getHeroes (): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( tap(heroes => this.log(`fetched heroes`)), catchError(this.handleError('getHeroes', [])) ); } |
Obtener el héroe por id
La mayoría de APIs soportan una petición tipo obtener por id con el formato api/hero/:id
(como api/hero/11
). Añade un método HeroService.getHero()
para hacer esa petición:
1 2 3 4 5 6 7 8 |
/** GET hero by id. Will 404 if id not found */ getHero(id: number): Observable<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get<Hero>(url).pipe( tap(_ => this.log(`fetched hero id=${id}`)), catchError(this.handleError<Hero>(`getHero id=${id}`)) ); } |
Hay tres diferencias significativas con getHeroes()
.
- construye una URL de petición con el id del héroe solicitado.
- el servidor debería responder con un único héroe en lugar de un array de héroes.
- por tanto,
getHero
devuelve unObservable<Hero<
(un observable de objetos Héroe) en lugar de un observable de arrays de héroes.
Actualizar héroes
Editando el nombre de un héroe en la vista del detalle del héroe. A medida que tecleas, el nombre del héroe se actualiza en la parte de arriba de la página. Pero cuando haces clic en el botón ‘volver’, los cambios se pierden.
Si quieres persistir los cambios, debes escribirlos en el servidor.
Al final de la plantilla de detalle de héroes, añade un botón de guardado con un evento click
vinculado que invoca un nuevo método del componente llamado save()
.
1 |
<button (click)="save()">save</button> |
Añade el siguiente método save()
, el cual persiste los cambios en el nombre del héroe usando el método updateHero()
del servicio del héroe y navega de vuelta a la vista anterior.
1 2 3 4 |
save(): void { this.heroService.updateHero(this.hero) .subscribe(() => this.goBack()); } |
Añadir HeroService.updateHero()
La estructura general del método updateHero()
es similar a la de getHeroes()
, pero usa http.put
para persistir en el servidor el héroe modificado.
1 2 3 4 5 6 7 |
/** PUT: update the hero on the server */ updateHero (hero: Hero): Observable<any> { return this.http.put(this.heroesUrl, hero, httpOptions).pipe( tap(_ => this.log(`updated hero id=${hero.id}`)), catchError(this.handleError<any>('updateHero')) ); } |
El método HttpClient.put()
toma tres parámetros
- la URL.
- los datos a actualizar (el héroe modificado en este caso).
- opciones
La URL no ha cambiado. La API web de héroes conoce cual es el héroe a actualizar observando el id
del héroe.
La API web de héroes espera una cabecera HTTP concreta en las peticiones de guardado. Esa cabecera estña en la constante httpOptions
definida en HeroService
.
1 2 3 |
const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; |
Actualiza el navegador, cambia un nombre de héroe, guarda el cambio y haz clic en el botón ‘volver’. El héroe aparece ahora en la lista con el nombre modificado.
Añadir un nuevo héroe
Para añadir un héroe, esta aplicación sólo necesita el nombre del héroe. Podemos usar un elemento <input>
con un botón de ‘añadir’.
Inserta lo siguiente en la plantilla HeroesComponent
, justo después de la cabecera:
1 2 3 4 5 6 7 8 9 |
<div> <label>Hero name: <input #heroName /> </label> <!-- (click) passes input value to add() and then clears the input --> <button (click)="add(heroName.value); heroName.value=''"> add </button> </div> |
En respuesta al evento clic, llamamos al manejador del clic del componente y después vaciamos el campo de entrada, de modo que esté listo para otro nombre.
1 2 3 4 5 6 7 8 |
add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } |
Cuando el nombre no está vacío, el manejador crea un objeto Hero
a partir del nombre (tan sólo le falta el id
) y se lo pasa al método addHero()
del servicio.
Cuando addHero
guarda con éxito, la retrollamada subscribe
recibe el nuevo héroe y lo inserta en la lista de heroes
a mostrar.
Escribiremos HeroService.addHero()
en la siguiente sección.
Añadir HeroService.addHero()
Añade el siguiente método addHero()
a la clase HeroService
1 2 3 4 5 6 7 |
/** POST: add a new hero to the server */ addHero (hero: Hero): Observable<Hero> { return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe( tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)), catchError(this.handleError<Hero>('addHero')) ); } |
HeroService.addHero()
difiere de updateHero
de dos maneras:
- llama a
HttpClient.post()
en lugar deput()
- espera a que el servidor genere un id para el nuevo héroe, el cual devuelve en
Observable<Hero>
al invocador.
Actualiza el navegador y añade algunos héroes.
Borrar un héroe
Cada héroe en el listado de héroes debería tener un botón de borrado.
Añade el siguiente botón a la plantilla HeroesComponent
, después del nombre del héroe en el elemento <li>
.
1 2 |
<button class="delete" title="delete hero" (click)="delete(hero)">x</button> |
El HTML para el listado de héroes debería quedar así:
1 2 3 4 5 6 7 8 9 |
<ul class="heroes"> <li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"> <span class="badge">{{hero.id}}</span> {{hero.name}} </a> <button class="delete" title="delete hero" (click)="delete(hero)">x</button> </li> </ul> |
Para posicionar el botón de borrado a la derecha de la caja el héroe, añade CSS a heroes.component.css
. Encontrarás este CSS en la revisión final de código.
Añade el manejador delete()
al componente.
1 2 3 4 |
delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } |
Aunque el componente delega el borrado del héroe en HeroService
, sigue siendo responsable de actualizar su propio listado de héroes. El método delete()
del componente borra inmediatamente el héroe a borrar de la lista, anticipando que HeroService
.
En realidad, no hay nada que el componente deba hacer con el Observable
devuelto por heroService.delete()
. Debe suscribirse de todas maneras.
Si dejáramos de usar
susbscribe()
, el servicio no enviaría la petición de borrado al servidor. Como norma general, unObservable
no hace nada hasta que alguien se suscribe. Comprueba esto por ti mismo eliminando temporalmentesubscribe()
, haciendo clic en «Dashboard» (Cuadro de Mandos) y después haciendo clic en «Heroes». Verás la lista de héroes completa de nuevo.
Añadir HeroService.deleteHero()
Añade un método deleteHero()
a HeroService
así:
1 2 3 4 5 6 7 8 9 10 |
/** DELETE: delete the hero from the server */ deleteHero (hero: Hero | number): Observable<Hero> { const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; return this.http.delete<Hero>(url, httpOptions).pipe( tap(_ => this.log(`deleted hero id=${id}`)), catchError(this.handleError<Hero>('deleteHero')) ); } |
Fíjate que
- llama a
HttpClient.delete
. - la URL es la URL del recurso de héroes más el
id
del héroe a eliminar. - no se envían datos como hicimos con
put
ypost
. - seguimos enviando
httpOptions
.
Actualiza el navegador y prueba la nueva funcionalidad de borrado.
Búsqueda por nombre
En el ejercicio anterior, hemos aprendido a encadenar operadores Observable
juntos de forma que minimizamos el número de peticiones HTTP similares y consumimos menos ancho de banda.
Vamos a añadir una opción de búsqueda de héroes al Cuadro de Mandos. Mientras el usuario teclea un nombre en el campo de búsqueda, haremos peticiones HTTP repetidamente por héroe filtrando por ese nombre. El objetivo es hacer el mínimo de peticiones necesarias.
HeroService.searchHeroes
Empezamos añadiendo un método searchHeroes
a HeroService
.
1 2 3 4 5 6 7 8 9 10 11 |
/* GET heroes whose name contains search term */ searchHeroes(term: string): Observable<Hero[]> { if (!term.trim()) { // if not search term, return empty hero array. return of([]); } return this.http.get<Hero[]>(`api/heroes/?name=${term}`).pipe( tap(_ => this.log(`found heroes matching "${term}"`)), catchError(this.handleError<Hero[]>('searchHeroes', [])) ); } |
El método devuelve inmediatamente un array vacío si no hay un término a buscar. El resto se parece bastante a getHeroes()
. LA única diferencia significativa es la URL, que incluye una cadena de consulta con el termino a buscar.
Añadir búsqueda al Cuadro de Mandos
Abre la plantilla de DashboardComponent
y añade el elemento de búsqueda <app-hero-search>
al final de dicha plantilla.
1 2 3 4 5 6 7 8 9 10 11 |
<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <app-hero-search></app-hero-search> |
Esta plantilla se parece mucho al iterador *ngFor
en la plantilla HeroesComponent
.
Desafortunadamente, añadir este elemento rompe la aplicación. Angular no puede encontrar un selector que coincida con <app-hero-search>
.
El componente HeroSearchComponent
aún no existe. Arreglaremos eso.
Crear HeroSearchComponent
Creamos un HeroSearchComponent
con el CLI.
1 2 3 |
ng generate component hero-search |
El CLI genera los tres ficheros de HeroSearchComponent
y añade el componente en las declaraciones de AppModule
.
Reemplaza la plantilla de HeroSearchComponent
generada por un cuadro de texto y un listado de coincidencias así.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <ul class="search-result"> <li *ngFor="let hero of heroes$ | async" > <a routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </li> </ul> </div> |
Añade unos estilos CSS privados a hero-search.component.css
como verás en la revisión final de código.
A medida que el usuario teclea en el cuadro de texto, un víncuño al evento keyup (soltar tecla) llama al método search()
del componente con el nuevo valor del cuadro de texto.
AsyncPipe
Como esperabamos, *ngFor
repite el objeto héroe.
Mira detenidamente y verás que *ngFor
itera sobre una lista llamada heroes$
, no heroes
.
1 |
<li *ngFor="let hero of heroes$ | async" > |
El $
es una convención que indica que heroes$
es un Observable
, no un array.
*ngFor
no puede hacer nada con un Observable
. Pero hay también un carácter ‘tubería’ (pipe |), seguido por async
, que identifica a AsyncPipe
de Angular.
AsyncPipe
suscribe automáticamente a un Observable
de modo que ya no tenemos que hacerlo en la clase del componente.
Arregla la clase HeroSearchComponent
Reemplaza la clase generada HeroSearchComponent
y sus metadatos de la siguiente manera.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { of } from 'rxjs/observable/of'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements OnInit { heroes$: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), ); } } |
Fíjate en que la declaración de heroes$
es un Observable
.
1 |
heroes$: Observable<Hero[]>; |
Lo ubicaremos en ngOnInit()
. Pero antes de hacerlo, nos fijaremos en la definición de searchTerms
El sujeto de RxJS searchTerms
La propiedad searchTerms
se declara como un Subject
(sujeto) de RxJS.
1 2 3 4 5 6 |
private searchTerms = new Subject<string>(); // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } |
Un Subject
es a la vez un origen de valores observables y un Observable
. Podemos suscribirnos a un Subject
como lo haríamos con cualquier Observable
.
Podemos también insertar valores en el Observable
llamando a su método next(valor)
del mismo modo que search()
.
El método search()
se llama a través de una vinculación de eventos al evento keystroke
(pulsar tecla) de la caja de texto.
1 |
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> |
Cada vez que el usuario teclea en la caja de texto, la vinculación llama a search()
con el valor de la caja de texto, un «término de búsqueda» (search term). El searchTerms
se convierte en un Observable
emitiendo una ‘corriente’ (stream) de términos de búsqueda.
Encadenando operadores RxJS
Pasar un nuevo término de búsqueda directamente a searchHeroes()
después de cada tecleo crearía una cantidad excesiva de peticiones HTTP, saturando los recursos del servidor y consumiendo los datos de nuestra tarifa móvil.
En su lugar, el método ngOnInit()
canaliza el observable searchTerms
a través de una secuencia de operadores RxJS que reduce el número de llamadas al searchHeroes()
, devolviendo finalmente un observable de los resultados de la búsqueda de héroes (cada Hero[]
).
Aquí está el código.
1 2 3 4 5 6 7 8 9 10 |
this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), ); |
debounceTime(300)
realiza una pausa de 300 milisegundos antes de pasar la última cadena. Nunca se harán peticiones con una frecuencia mayor a 300ms.distinctUntilChanged
se asegura de que la petición se envíe sólo si el texto de filtrado ha cambiadoswitchMap()
llama al servicio de búsqueda por cada término de búsqueda que llega hastadebounce
ydistinctUntilChanged
. Cancela y descarta los observables de búsqueda anteriores, devolviendo sólo el último.
Con el operador switchMap, cada evento de teclado definido puede lanzar una llamada al método
HttpClient.get()
. Incluso con una pausa de 300ms entre peticiones, puedes tener múltiples peticiones HTTP activas y pueden no devolverse en el orden enviado.
switchMap()
preserva el orden original de las peticiones y sólo devolverá el observable de la llamada a la petición HTTP más reciente. Los resultados de las llamadas anteriores son cancelados y descartados.Ten en cuenta que cancelar un Observable
searchHeroes()
previo en realidad no aborta una petición HTTP pendiente. Los resultados no deseados son simplemente descartados antes de llegar al código de la aplicación.
Recuerda que la clase del componente no se suscribe al observable heroe$
. Ese es el trabajo de AsyncPipe en la plantilla.
Pruébalo
Ejecuta la aplicación de nuevo. En el Cuadro de Mandos, introduce algún texto en la caja de texto. Si introduces caracteres
Revisión final de código
Tu aplicación debería parecerse a este ejemplo en vivo / descarga ejemplo
Aquí están los ficheros tratados en esta página (todos ellos en la carpeta src/app
).
HeroService, InMemoryDataService y AppModule
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Hero } from './hero'; import { MessageService } from './message.service'; const httpOptions = { headers: new HttpHeaders({ 'Content-Type': 'application/json' }) }; @Injectable() export class HeroService { private heroesUrl = 'api/heroes'; // URL to web api constructor( private http: HttpClient, private messageService: MessageService) { } /** GET heroes from the server */ getHeroes (): Observable<Hero[]> { return this.http.get<Hero[]>(this.heroesUrl) .pipe( tap(heroes => this.log(`fetched heroes`)), catchError(this.handleError('getHeroes', [])) ); } /** GET hero by id. Return `undefined` when id not found */ getHeroNo404<Data>(id: number): Observable<Hero> { const url = `${this.heroesUrl}/?id=${id}`; return this.http.get<Hero[]>(url) .pipe( map(heroes => heroes[0]), // returns a {0|1} element array tap(h => { const outcome = h ? `fetched` : `did not find`; this.log(`${outcome} hero id=${id}`); }), catchError(this.handleError<Hero>(`getHero id=${id}`)) ); } /** GET hero by id. Will 404 if id not found */ getHero(id: number): Observable<Hero> { const url = `${this.heroesUrl}/${id}`; return this.http.get<Hero>(url).pipe( tap(_ => this.log(`fetched hero id=${id}`)), catchError(this.handleError<Hero>(`getHero id=${id}`)) ); } /* GET heroes whose name contains search term */ searchHeroes(term: string): Observable<Hero[]> { if (!term.trim()) { // if not search term, return empty hero array. return of([]); } return this.http.get<Hero[]>(`api/heroes/?name=${term}`).pipe( tap(_ => this.log(`found heroes matching "${term}"`)), catchError(this.handleError<Hero[]>('searchHeroes', [])) ); } //////// Save methods ////////// /** POST: add a new hero to the server */ addHero (hero: Hero): Observable<Hero> { return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe( tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)), catchError(this.handleError<Hero>('addHero')) ); } /** DELETE: delete the hero from the server */ deleteHero (hero: Hero | number): Observable<Hero> { const id = typeof hero === 'number' ? hero : hero.id; const url = `${this.heroesUrl}/${id}`; return this.http.delete<Hero>(url, httpOptions).pipe( tap(_ => this.log(`deleted hero id=${id}`)), catchError(this.handleError<Hero>('deleteHero')) ); } /** PUT: update the hero on the server */ updateHero (hero: Hero): Observable<any> { return this.http.put(this.heroesUrl, hero, httpOptions).pipe( tap(_ => this.log(`updated hero id=${hero.id}`)), catchError(this.handleError<any>('updateHero')) ); } /** * Handle Http operation that failed. * Let the app continue. * @param operation - name of the operation that failed * @param result - optional value to return as the observable result */ private handleError<T> (operation = 'operation', result?: T) { return (error: any): Observable<T> => { // TODO: send the error to remote logging infrastructure console.error(error); // log to console instead // TODO: better job of transforming error for user consumption this.log(`${operation} failed: ${error.message}`); // Let the app keep running by returning an empty result. return of(result as T); }; } /** Log a HeroService message with the MessageService */ private log(message: string) { this.messageService.add('HeroService: ' + message); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { InMemoryDbService } from 'angular-in-memory-web-api'; export class InMemoryDataService implements InMemoryDbService { createDb() { const 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}; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; import { InMemoryDataService } from './in-memory-data.service'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroesComponent } from './heroes/heroes.component'; import { HeroSearchComponent } from './hero-search/hero-search.component'; import { HeroService } from './hero.service'; import { MessageService } from './message.service'; import { MessagesComponent } from './messages/messages.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule, HttpClientModule, // The HttpClientInMemoryWebApiModule module intercepts HTTP requests // and returns simulated server responses. // Remove it when a real server is ready to receive requests. HttpClientInMemoryWebApiModule.forRoot( InMemoryDataService, { dataEncapsulation: false } ) ], declarations: [ AppComponent, DashboardComponent, HeroesComponent, HeroDetailComponent, MessagesComponent, HeroSearchComponent ], providers: [ HeroService, MessageService ], bootstrap: [ AppComponent ] }) export class AppModule { } |
HeroesComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<h2>My Heroes</h2> <div> <label>Hero name: <input #heroName /> </label> <!-- (click) passes input value to add() and then clears the input --> <button (click)="add(heroName.value); heroName.value=''"> add </button> </div> <ul class="heroes"> <li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"> <span class="badge">{{hero.id}}</span> {{hero.name}} </a> <button class="delete" title="delete hero" (click)="delete(hero)">x</button> </li> </ul> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } add(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } delete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
/* HeroesComponent's private CSS styles */ .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { position: relative; cursor: pointer; 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 a { color: #888; text-decoration: none; position: relative; display: block; width: 250px; } .heroes a:hover { color:#607D8B; } .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; min-width: 16px; text-align: right; margin-right: .8em; border-radius: 4px 0 0 4px; } button { background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; font-family: Arial; } button:hover { background-color: #cfd8dc; } button.delete { position: relative; left: 194px; top: -32px; background-color: gray !important; color: white; } |
HeroDetailComponent
1 2 3 4 5 6 7 8 9 10 11 |
<div *ngIf="hero"> <h2>{{ hero.name | uppercase }} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"/> </label> </div> <button (click)="goBack()">go back</button> <button (click)="save()">save</button> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
import { Component, OnInit, Input } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { @Input() hero: Hero; constructor( private route: ActivatedRoute, private heroService: HeroService, private location: Location ) {} ngOnInit(): void { this.getHero(); } getHero(): void { const id = +this.route.snapshot.paramMap.get('id'); this.heroService.getHero(id) .subscribe(hero => this.hero = hero); } goBack(): void { this.location.back(); } save(): void { this.heroService.updateHero(this.hero) .subscribe(() => this.goBack()); } } |
HeroSearchComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<div id="search-component"> <h4>Hero Search</h4> <input #searchBox id="search-box" (keyup)="search(searchBox.value)" /> <ul class="search-result"> <li *ngFor="let hero of heroes$ | async" > <a routerLink="/detail/{{hero.id}}"> {{hero.name}} </a> </li> </ul> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Subject } from 'rxjs/Subject'; import { of } from 'rxjs/observable/of'; import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-hero-search', templateUrl: './hero-search.component.html', styleUrls: [ './hero-search.component.css' ] }) export class HeroSearchComponent implements OnInit { heroes$: Observable<Hero[]>; private searchTerms = new Subject<string>(); constructor(private heroService: HeroService) {} // Push a search term into the observable stream. search(term: string): void { this.searchTerms.next(term); } ngOnInit(): void { this.heroes$ = this.searchTerms.pipe( // wait 300ms after each keystroke before considering the term debounceTime(300), // ignore new term if same as previous term distinctUntilChanged(), // switch to new search observable each time the term changes switchMap((term: string) => this.heroService.searchHeroes(term)), ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
/* HeroSearch private styles */ .search-result li { border-bottom: 1px solid gray; border-left: 1px solid gray; border-right: 1px solid gray; width:195px; height: 16px; padding: 5px; background-color: white; cursor: pointer; list-style-type: none; } .search-result li:hover { background-color: #607D8B; } .search-result li a { color: #888; display: block; text-decoration: none; } .search-result li a:hover { color: white; } .search-result li a:active { color: white; } #search-box { width: 200px; height: 20px; } ul.search-result { margin-top: 0; padding-left: 0; } |
Resumen
Estamos al final de este viaje y hemos conseguido muchas cosas.
- Hemos añadido las dependencias necesarias para usar HTTP en la aplicación.
- Hemos refactorizado
HeroService
para cargar héroes desde una API Web. - Hemos extendido
HeroService
para que permita los métodospost()
,put()
ydelete()
. - Hemos actualizado los componentes para permitir que editen, añadan y borren héroes.
- Hemos configurado una API Web en memoria.
- Hemos aprendido como usar Observables.
Esto finaliza el tutorial ‘Tour de Héroes’. Ahora estamos listos para aprender más acerca de desarrollo en Angular en la sección de Fundamentos, empezando por la guía de Arquitectura.
Nota: puedes encontrar el documento original de esta entrada en https://angular.io/tutorial/toh-pt6
Muchas gracias por la traducción de este tutorial. Me ha servido mucho.
Un saludo.
Esta muy bueno, aunque tiene un pequeno error …. en la vista de heroes, cuando eliminas todos los heroes, y despues quieres adicionar uno, lo adiciona sin id por lo cual da error….
me estuve fijando en el codigo original de la pagina oficial y encontre este codigo que se encarga de eso….
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(…heroes.map(hero => hero.id)) + 1 : 11;
}
Hay que ponerlo en in-memory-data.service.ts …..
Bueno Muchas gracias por todo. ahora mismo estoy en el desarrollo del buscador. Cuando finalize les hago otro comentario.