Angular es un framework JavaScript, gratuito y Open Source, creado por Google y destinado a facilitar la creación de aplicaciones web modernas de tipo SPA (Single Page Application).
Su primera versión, AngularJS, se convirtió en muy poco tiempo en el estándar de facto para el desarrollo de aplicaciones web avanzadas.
En septiembre de 2016 Google lanzó la versión definitiva de lo que llamó en su momento Angular 2, y que ahora es simplemente Angular. Este nuevo framework se ha construido sobre años de trabajo y feedback de los usuarios usando AngularJS (la versión 1.x del framework). Su desarrollo llevó más de 2 años. No se trata de una nueva versión o una evolución de AngularJS, sino que es un nuevo producto, con sus propios conceptos y técnicas. Además, Angular utiliza como lenguaje de programación principal TypeScript, un súper-conjunto de JavaScript/ECMAScript que facilita mucho el desarrollo.
Este es el tutorial oficial de la página de Angular. Lo que haré es ir haciendo el tutorial y después intentar hacer como un manual de como funciona Angular.
En este tutorial se creara una aplicación que ayuda a una agencia personal a administrar super héroes.
Esta aplicación básica tiene muchas de las características que esperaras encontrar en una aplicación basada en datos. Adquiere y muestra una lista de héroes, edita los detalles de un héroe seleccionado y navega entre diferentes vistas de datos heróicos.
Al final del tutorial, podrás hacer lo siguiente:
- Utilizar las directivas de Angular para ver y ocultar elementos y mostrar listas de los datos de los héroes.
- Crear componentes de Angular para mostrar los detalles de los héroes y mostrar un array de héroes.
- Usar datos de una sola dirección para leer sólo datos.
- Añadir campos editables a los eventos del usuario, como pulsaciones de teclas o clicks.
- Habilitar usuarios para seleccionar un héroe de una lista maestra y editar ese héroe en la vista de detalles.
- Formatear datos con "tuberías".
- Crea un servicio compartido para armar a los héroes.
- Usar el enrutamiento para navegar entre diferentes vistas y sus componentes.
Con esto aprenders lo suficiente de Angular para comenzar y tener confianza de que Angular puede hacer lo que necesitas.
Aquí hay una idea visual de dónde conduce este tutorial, comenzando con la vista "Tablero" y los héroes más heroicos.
Puedes hacer click en los dos enlaces sobre el tablero ("Tablero" y "Héroes") para navegar entre esta vista del Tablero y una vista de Héroes.
Si haces click en el héroe del tablero "Magneta", el enrutador abre una vista de "Detalles del héroe" donde puede cambiar el nombre del héroe.
Al hacer click en el botón "Atrás", volverá al panel. Los enlaces en la parte superior te llevan a cualquier a cualquiera de las vistas principales. Si haces click en "Héroes" la aplicación muestra "Héroes" de la vista principal.
Cuando haces click en un nombre de héroe diferente, el mini detalle de sólo lectura debajo de la lista refleja la nueva opción.
Puedes hacer click en el botón "Ver detalles" para explorar los detalles editables del héroe seleccionado.
El siguiente diagrama captura todas las opciones de navegación.
Y esta es la aplicación en acción:
Instalar el CLI de Angular es muy fácil, se hace de la siguiente manera:
npm install -g @angular/cliVamos a crear un nuevo proyecto llamado angular-tour-of-heroes con este comando CLI.
ng new angular-tour-of-heroesEl CLI de Angular ha generado un nuevo proyecto con una aplicación predeterminada y archivos de respaldo.
Puedes agragar funcionalidades preempaquetadas a un nuevo proyecto utilizando el comando
ng add. El comandong addtransforma un proyecto aplicando los esquemas en el paquete especificado. Para obtener más información, consulta la documentación del CLI de Angular.Angular Material proporciona esquemas para diseños de aplicaciones típicos. Consulta la documentación de Material Angular para más detalles.
Nos metemos dentro del directorio de nuestra aplicación y lanzamos el siguiente comando:
cd angular-tour-of-heroes
ng serve --open
ng serveeste comando crea la aplicación, inicia el servidor de desarrollo, mira los archivos fuente y reconstruye la aplicación a medida que se realizan cambios en estos archivos.La bandera
--openabre un navegador conhttp://localhost:4200.
La pagina que puedes ver es la application shell. La shell está controlada por un coponente de Angular llamado AppComponent.
Los Componentes son los bloques fundamentales con las que se construyen nuestras aplicaciones con Angular. Muestran datos en la pantalla, escuchan los input (entradas) de los usuarios y toman medidas en función a ese input.
Abrimos el proyecto con nuestro editor favorito o IDE y navegamos al directorio src/app.
Encontraremos la implementación del shell AppComponent distribuido en tres archivos:
app.component.ts- el código de clase del componente, escrito en TypeScript.app.component.html- la plantilla del componente, escrito en HTML.app.component.css- los estilos de CSS privados del componente.
Abrimos el archivo de clase de componente (app.component.ts) y cambiamos el valor de title por la propiedad Tour of Heroes.
Archivo: app.component.ts (propiedad del título de la clase)
title = 'Tour of Heroes';Debería quedar de la siguiente manera:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Tour of Heroes';
}Abrimos el archivo de plantilla del componente (app.component.html) y borramos la plantilla generada por defecto por el CLI de Angular. Reemplaza esto con la siguiente línea de HTML.
Archivo: app.component.html (template - plantilla)
<h1>{{title}}</h1>Las llaves dobles son la sintaxis de unión de interpolación de Angular. Este enlace de interpolación presenta el valor de propiedad del título del componente dentro de la etiqueta del encabezado HTML.
El navegador se actualiza y muestra el nuevo título de la aplicación.
La mayoría de las aplicaciones se esfuerzan por obtener un aspecto uniforme en toda la aplicación. El CLI generó un styles.css vacío para este propósito. Coloque allí sus estilos para toda la aplicación.
A continuación tendremos un extracto del styles.css para la aplicacion de muestra Tour of Heroes.
Archivo: src/styles.css (extracto)
/* Application-wide Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}- Hemos creado una estructura para una aplicación inicial usando el CLI de Angular.
- Hemos aprendido que los componentes de Angular muestran datos.
- Hemos usado las llaves dobles
{{}}de interpolación para mostrar el título de la aplicación.
La aplicación ahora tiene un título básico. A continuación, crearemos un nuevo componente para mostrar la información del héroe y colocar ese componente en el shell de la aplicación.
Con el CLI de Angular, generamos un nuevo componente llamado heroes.
ng generate component heroesEl CLI crea una nueva carpeta, src/app/heroes/, y genera tres archivos en el HeroesComponent.
El archivo clase de HeroesComponent es el siguiente:
Archivo: app/heroes/heroes.component.ts (versión inicial)
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}Siempre se importa el symbol de Component de la biblioteca central de Angular y anota la clase componente con @Component.
@Component es una función decorador que especifíca los metadatos de Angular al componente.
El Cli genera tres propiedades metadata:
selector- el selector de elemento CSS del componente.templateUrl- la ubicación del archivo de plantilla del componente.styleUrls- la ubicación de los estilos de CSS privados del componente.
El elemento selector CSS, app-heroes, coincide con el nombre del elemento HTML que identifica este componente dentro de la plantilla de un componente principal.
NgOnInites un hook (gancho) de ciclo de vida. Angular llama a ngOnInitpoco después de crear un componente. Es un buen lugar para poner la lógica de inicialización.
Siempre debemos exportar (export) la clase del componente para que pueda importarla (import) en otro lugar... como en el AppModule.
Agregar al hero una propiedad al HeroesComponent para un héroe llamado "Windstorm".
Archivo: heroes.component.ts (propiedad del heroe)
hero = 'Windstorm';Abre el archivo de plantilla heroes.component.html. Borra el texto por defecto generado por el CLI de Angular y reemplazalo con lo que viene a continuación.
Archivo: heroes.component.html
{{hero}}Para mostrar el HeroesComponent, debemos de agregarlo a la plantilla del shell AppComponent.
Recuerda que app-heroes es el selector de elementos para HeroesComponent. Así que agrega un elemento <app-heroes> al archivo de plantilla de AppComponent, justo debajo del título.
Archivo: src/app/app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>Suponiendo que el comando CLI ng serve aún se está ejecutando, el navegador debe actualizar y mostrar tanto el título de la aplicación como el nombre del héroe.
Un héroe real es más que un simple nombre.
Crea una clase Hero creando tu propio archivo hero.ts en la carpeta src/app. Dale las propiedades id y name.
Archivo: src/app/hero.ts
export class Hero {
id: number;
name: string;
}Vuelve a la clase HeroesComponent e importa la clase Hero.
Refactoriza la propiedad del componente hero para que sea del tipo Hero. Inizializarlo con una idde 1 y el nombre Windstorm.
El archivo de clase HeroesComponent revisado debera de verse así:
Archivo: src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
hero: Hero = {
id: 1,
name: 'Windstorm'
};
constructor() { }
ngOnInit() {
}
}La página ya no se muestra correctamente porque ha cambiado el héroe de una cadena (string) a un objeto (object).
Actualiza el enlace en la plantilla para anunciar el nombre del héroe y mostrar tanto la id como el name en un diseño como este:
Archivo: heroes.component.html (modelo HeroesComponent)
<h2>{{hero.name}} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div><span>name: </span>{{hero.name}}</div>El navegador se refrescará y mostrara la información del héroe.
Modifica el enlace hero.name como vemos aquí:
<h2>{{hero.name | uppercase}} Details</h2>El navegador se refrescará y ahora el nombre del héroe se verá en mayúsculas.
La palabra uppercase en el enlace de interpolación, justo después del operador de tubería (pipe operator) (|), activa el UperCasePipe incorporado.
Las 'tuberías' son una buena forma de formatear cadenas, cantidades de moneda, fechas y otros datos de visualización. Angular dispone de distintos tuberías integradas y puedes crear las tuyas propias.
Los usuarios deberían poder editar el nombre del héroe en un cuadro dre texto <input>.
El cuadro de texto debe mostrar la propiedad de name del héroe y actualizar esa propiedad a medida que el usuario lo escribe. Eso significa que el flujo de datos de la clase de componente salida a la pantalla y de la pantalla se lo devuelva a la clase.
Para automizar ese flujo de datos, configura un enlace de datos bidireccional entre el elemento de formulario <input> y la propiedad del elemento hero.name.
Refactoriza el área de detalles en la plantilla HeroesComponent para que se vea así:
Archivo: src/app/heroes/heroes.component.html (plantilla HeroesComponent)
<div>
<label>name:
<input [(ngModel)]="hero.name" placeholder="name">
</label>
</div>[(ngModel)] es la sintaxis de enlace de datos bidireccional de Angular.
Aquí se vincula la propiedad hero.name al cuadro de texto HTML para que los datos puedan fluir en ambas direcciones: de la propiedad hero.name al cuadro de texto y del cuadro de texto a hero.name.
Nos daremos cuenta que la aplicación dejó de funcionar cuando agregamos [(ngModel)].
Para ver estos errores, abrimos las herramientas de desarrollo del navegador y buscamos en la consola un mensaje como:
Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'.Aunque ngModel es una directiva de Angular válida, no está disponible por defecto.
Pertenece a FormsModuley es opcional, podemos optarlo si lo usamos.
Angular necesita saber cómo encajan las piezas de su aplicación y qué otros archivos y bibliotecas necesita la aplicación. Esta información se llama metadata.
Algunos de los metadatos están en los decoradores de @Component que ha agregado a sus clases de componentes. Otros metadatos críticos se encuentan en los decoradores de @NgModule.
El decorador @NgModule más importante anota la clase AppModule de nivel superior.
El CLI de Angular genera una clase de AppModule en src/app/app.module.ts cuando crea el proyecto. Aquí es donde se puede optar por el FormsModule.
Abre AppModule (app.module.ts) e importa el symbol FormsModule de la libreria @angular/forms.
Archivo: app.module.ts (Importar symbol FormsModule)
import { FormsModule } from '@angular/forms'; // <-- NgModel lives hereA continuación agrega FormsModule a la matriz de importaciones de los metadatos de @NgModule, que contiene una lista de módulos externos que la aplicación necesita.
Archivo: app.module.js (importa @NgModule)
imports: [
BrowserModule,
FormsModule
],Cuando el navegador se actualiza, la aplicación debería de funcionar nuvamente. Puedes editar el nombre del héroe y ver los cambios reflejados inmediatamente en el <h2> encima del cuadro de texto.
Todo componente debe ser declarado exactamente en NgModule.
¿No declaraste antes el componente HeroesComponent?. Entonces, ¿por qué funcionó la aplicación?.
Funcionó porque el CLI de Angular declaró HeroesComponenten el AppModule cuando generó ese componente.
Abre src/app/app.module.ts y encuentra HeroesComponent que está importado por arriba del todo.
import { HeroesComponent } from './heroes/heroes.component';HeroesComponent está declarado en el array @NgModule.declarations.
declarations: [
AppComponent,
HeroesComponent
],Ten en cuenta que AppModule declara ambos componentes en la aplicación AppComponent y HeroesComponent.
- Usamos el CLI para crear un segundo
HeroesComponent. - Mostramos
HeroesComponentagregándolo al shell deAppComponent. - Aplicamos
UppercasePipepara formatear el nombre. Usó el enlace de datos bidireccional con la directivangModel. - Hemos aprendido sobre
AppModule. - Hemos aprendido sobre
FormsModuleen elAppModulepara que Angular reconozca y aplique la directivangModel. - Aprendimos la importancia de declarar componentes en el
AppModuley agradecimos que el CLI lo declarara por nosotros.
En esta pgina, expandiremos la aplicación 'Tour of Heroes' para mostrar una lista de héroes y permitir a los usuarios seleccionar un héroe y mostrar los detalles de éste.
Necesitaremos unos cuantos héroes para mostrarlos.
Normalmente los obtendremos de un servidor, pero por ahora, crearemos algunos héroes falsos y nos creeremos que vienen del servidor.
Crea un archivo llamado mock-heroes.ts en la ubicación src/app/. Define HEROES como una constante y un array de diez hroes y expórtala. El archivo debe verse así:
Archivo: src/app/mock-heroes.ts
import { Hero } from './hero';
export const HEROES: Hero[] = [
{ 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' }
];Estás a punto de mostrar la lista de héroes en la parte superior de HeroesComponent.
Abre el archvio de clase HeroesComponent e importa la lista simulada de 'HEROES'.
Archivo: src/app/heroes/heroes.component.ts (importando HEROES)
import { HEROES } from '../mock-heroes';Agrega una propiedad de heroes a la clase que expone a estos héroes para vincularlos.
heroes = HEROES;Abre el archivo de la plantilla HeroesComponent y realiza los siguientes cambios:
- Agrega un
<h2>en la parte superior. - A continuación, agrega una lista desordenada HTML (
<ul>). - Inserta un
<li>dentro de<ul>que muestra las propiedades de unhero. - Espolvoreamos algunas clases de CSS para el estilo (agregaremos los estilos CSS en breve).
Haz que se vea así:
Archivo: heroes.component.html (plantilla heroes)
<h2>My Heroes</h2>
<ul class="heroes">
<li>
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>Ahora cambia el <li> así:
<li *ngFor="let hero of heroes">El *ngFor es la directiva de repetidor de Angular. Repite el elemento host para cada elemento en una lista.
En este ejemplo:
<li>es el elemento host.heroeses la listsa de la claseHeroesComponent.herotiene el objeto héroe actual para cada iteración a través de la lista.
No olvides el asterísco (*) delante de
ngFor. Es una parte crítica de la sintaxis.
Después de que el navegador se actualiza, aparece la lista de héroes.
La lista de héroes debe de ser atractiva y debe responder visualmente cuando los usuarios se pongan encima de la lista y seleccionen uno.
En la primera parte del tutorial, establecimos los estilos básicos para toda la aplicación en styles.css. Esa hoja de estilo no incluía estilos para esta lista de héroes.
Puedes añadir más estilos a styles.css y seguir creciendo esa hoja de estilos a medida que agregas componentes.
En su lugar, puedes preferir definir estilos privados para un componente en específico y mantener todo lo que necesita un componente - el código, el HTML y el CSS - en un sólo lugar.
Este enfoque hace que sea más fácil reutilizar el componente en otro lugar y entregar la apariencia prevista del componente, incluso si los estilos globales son diferentes.
Definimos los estilos privados ya sea en un array en @Component.styles o como un archivo(s) de hoja de estilos identificados en el array @Component.styleUrls.
Cuando el CLI generó HeroesComponent, creó una hoja de estilo heroes.component.css vaca para HeroesComponent y la apuntó en @Component.styleUrls de esta manera.
Archivo: src/app/heroes/heroes.component.ts (@Component)
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})Abre el archivo heroes.component.css y pega los estilos CSS privados para HeroesComponent que se muestran a continuación:
Los estilos y las hojas de estilo identificadas en el metadata de
@Componenttiene un alcance para ese componente en específico. Los estilos deheroes.component.cssse aplican sólo aHeroesComponenty no afecta al HTML externo o al HTML en ningún otro componente.
Archivo: src/app/heroes/heroes.component.css
/* HeroesComponent's private CSS styles */
.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.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.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;
}Cuando el usuario hace click en un héroe en la lista maestra, el componente debe mostrar los detalles del héroe seleccionado en la parte inferior de la página.
En esta sección, escuchará el evento de click del hemelento héroe y actualiza el detalle del héroe.
Vamos a agregar un evento click al enlace <li>, debe de quedar así:
Archivo: heroes.component.html (extracto de la plantilla)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)">Este es un ejemplo de la sintáxis de Angular del event binding.
Los paréntesis de alrededor de click dicen a Angular que escuche el evento click del elemento<li>. Cuando el usuario hace click en el elemento <li>, Angular ejecuta la expresión onSelect(hero).
onSelect() es un método de HeroesComponent que estamos a punto de escribir. Angular lo llama con el objeto heroe que se muestra en el <li> clickado, el mismo hero definido previamente en la expresión *ngFor.
Renombramos la propiedad del componente hero a selectedHero pero no se lo asignamos. No hay un héroe seleccionado cuando se inicia la aplicación.
Agregamos el siguiente método onSelect(), que asigna al héroe clickado de la plantilla del componente selectedHero.
Archivo: src/app/heroes/heroes.component.ts (onSelect)
selectedHero: Hero;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}La plantilla todavía se refiere a la antigua propiedad del hero del componente que ya no existe. Cambiar el nombre de hero a selectedHero.
Archivo: heroes.component.html (Detalles seleccionados del héroe)
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="selectedHero.name" placeholder="name">
</label>
</div>Después de que el navegador se actualiza, la aplicación está rota.
Abre las herramientas de desarrollador del navegador y busca en la consola un mensaje de error como este:
HeroesComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined
Ahora haz click en uno de los elementos de la lista. La aplicación parece estar funcionando de nuevo. Los héroes aparecen en una lista y los detalles sobre el héroe clickado aparecen en la parte inferior de la página.
Cuando se inicia la aplicación, selectedHero está con el valor undefined por diseño.
Las expresiones vinculantes en la plantilla que hacen referencia a las propiedades de selectedHero - expresiones como {{selectedHero.name}} - debe fallar porque no hay ningún héroe seleccionado.
El componente sólo debera mostrar los detalles del héroe seleccionado si el selectedHero existe.
Envuelva el HTML de detalles del héroe con un <div>. Agrega la directiva de Angular *ngIf al <div> y establécelo en selectedHero.
No olvides el asterisco (*) delante de
ngIf. Es una parte crítica de la sintáxis.
Archivo: src/app/heroes/heroes.component.html (*ngif)
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="selectedHero.name" placeholder="name">
</label>
</div>
</div>Después de que el navegador se actualiza, la lista de nombres vuelve a aparecer. El área de detalles está en blanco. Haz click en un héroe y aparecerán sus detalles.
Cuando selectedHero está como undefined, ngIf elimina los detalles del héroe del DOM. No hay enlaces de selectedHero de los que preocuparse.
Cuando el usuario eige un héroe, selectedHero tiene un valor y ngIf pone el detalle del héroe en el DOM.
Es difícil identificar al héroe selecccionado en la lista cuando todos los elementos <li> se aparecen.
Si el usuaruio hace click en "Magneta", ese héroe debe renderizarse con un color de fondo distinto pero sutil como este:
Ese color de héroe seleccionado es el trabajo de la clase CSS .selected en los estilos que agregamos antes. Sólo tenemos que aplicar la clase .selected al <li> cuando el usuario hace click.
El enlace de clase de Angular hace que sea fácil agregar y eliminar una clase de CSS condicionalmente. Simplemente agregamos [class.some-css-class}="some-condition" al elemento que deseamos diseñar.
Agregamos el siguiente enlace [class.selected] al elemento <li> en la plantilla HeroesComponent:
Archivo: heroes.component.html (alterna la clase CSS 'selected')
[class.selected]="hero === selectedHero"Cuando el héroe de fila actual es el mismo que el mismo que selectedHero, Angular agrega la clase de CSS selected. Cuando dos héroes son diferentes, Angular elimina la clase.e
El <li> terminado tiene este aspecto:
Archivo: heroes.component.html (elemento de lista de héroes)
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>Para que quede todo claro vamos a ver como han quedado todos los archivos que hemos hecho en esta sección.
Archivo: src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HEROES } from '../mock-heroes';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes = HEROES;
selectedHero: Hero;
constructor() { }
ngOnInit() {
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}Archivo: src/app/heroes/heroes.component.html
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="selectedHero.name" placeholder="name">
</label>
</div>
</div>Archivo: src/app/heroes/heroes.component.css
/* HeroesComponent's private CSS styles */
.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.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.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;
}Por el momento, HeroesComponent muestra tanto la lista de héroes como los detalles del héroe seleccionado.
Mantener todas las características en un componente a medida que crece la aplicación no será mantenible. Deberemos dividir los componentes grandes en subcomponentes más pequeños, cada uno centrado en una tarea o flujo de trabajo específico.
En esta página, damos el primer paso en esa dirección al mover los detalles del héroe a un lugar separado y reutilizable HeroDetailComponent.
El HeroesComponent sólo presentará la lista de héroes. El HeroDetailComponent presentará los detalles de un héroe seleccionado.
Usa el CLI de Angular para generar un nuevo componente nombrado hero-detail.
ng generate component hero-detail
El comando hace scaffolding de los archivos de HeroDetailComponent y declara el componente en AppModule.
Corta el HTML para el detalle del héroe desde la parte inferior de la plantilla HeroesComponent y pegalo sobre la plantilla repetitiva generada HeroDetailComponent.
El HTML pegado se refiere a selectedHero. El nuevo HeroDetailComponent puede presentar cualquier héroe, no sólo un héroe seleccionado. Reemplaza selectedHero por hero en todas partes en la plantilla.
Cuando termines, la plantilla HeroDetailComponent debería de verse así:
Archivo: src/app/hero-detail/hero-detail.component.html
<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>
</div>La plantilla HeroDetailComponentse una a la propiedad hero del componente que es de tipo Hero.
Abre el archvio de clase HeroDetailComponent e importa el symbol Hero.
Archivo: src/app/hero-detail/hero-detail.components.ts (importar Hero)
import { Hero } from '../hero';La propiedad hero debe ser una propiedad de entrada (Input), anotada con el decorador @Input(), porque el HeroesComponent externo se enlazará de esta manera.
<app-hero-detail [hero]="selectedHero"></app-hero-detail>Modificamos la declaración de importación @angular/core para incluir el symbol Input.
Archivo: src/app/hero-detail/hero-detail.component.ts (Importamos Input)
import { Component, OnInit, Input } from '@angular/core';Agregamos una propiedad a hero, procedidad por el decorador `@Input().
@Input() hero: Hero;Este es el único cambio que debemos hacer en la clase HeroDetailComponent. No hay más propiedades. No hay lógica de presentación. Este componente simplemente recibe un objeto héroe a través de su propiedad hero y lo muestra.
HeroesComponent sigue siendo la vista maestra / detalles.
Mostraba los detalles del héroe por sí mismo, antes de cortaremos una pequeña parte de la plantilla. Ahora delegaremos en el HeroDetailComponent.
Los dos componentes tendrán una relación padre/hijo. El componente padre heroesComponent controlará al componente hijo HeroDetailComponent enviándole un nuevo héroe para mostrar cuando el usuario seleccione un héroe de la lista.
No cambiaremos la clase HeroesComponent pero cambiaremos su plantilla.
El selector HeroDetailComponent es app-hero-detail. Aregamos un elemento <app-hero-detail> cerca de la parte inferior de la plantilla HeroesComponent, donde solía estar la vista de detalles del héroe.
Enlazamos el elemento HeroesComponent.selectedHero ala propiedad hero de esta manera.
Archivo: heroes.component.html (enlace de HeroDetail)
<app-hero-detail [hero]="selectedHero"></app-hero-detail>[hero]="selectedHero" es una propiedad binding de Angular.
Es un binding de datos unidireccional desde la propiedad selectedHero de HeroesComponent hasta la propiedad hero del elemento de destino, que se asigna a la propiedad hero de HeroDetailComponent.
Ahora cuando el usuario hace click en un héroe en la lista, se verán los cambios de selectedHero. Cuando se realizan los cambios de selectedHero, la propiedad hero se actualza y HeroDetailComponent muestra el nuevo héroe.
La plantilla HeroesComponent revisada debería de verse así:
Archivo: heroes.component.html
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>El navegador se refrescará y la aplicación vuelve a funcionar como antes.
Como antes, cada vez que un usuario hace click en un nombre de un héroe, el detalle del héroe aparece debajo de la lista de héroes. Ahora HeroDetailComponent está presentando esos detalles en lugar de HeroesComponent.
Refactorizando HeroesComponent original en dos componentes produce beneficios, tanto ahora como en un futuro:
- Simplificamos
HeroesComponentreduciendo sus responsabilidades. - Convertimos
HeroDetailComponenten un rico editor de héroes sin tocar al padreHeroesComponent. - Evolucionamos
HeroesComponentsin tocar la vista de detalle del héroe. - Reutilizamos
HeroDetailComponentpara algún componente en un fúturo.
Nuestros archivos deberían de verse así:
Archivo: src/app/hero-detail/hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
@Input() hero: Hero;
constructor() { }
ngOnInit() {
}
}Archivo: src/app/hero-detail/hero-detail.component.html
<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>
</div>Archivo: src/app/heroes/heroes.component.html
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>Archivo: src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
@NgModule({
declarations: [
AppComponent,
HeroesComponent,
HeroDetailComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }El Tour de Héroes HeroesComponent está recibiendo y mostrando datos falsos.
Después de la refactorización en este tutorial, HeroesComponent se apoyará y se centrara en dar soporte a la vista. También será más fácil realizar una prueba unitaria con un servicio simulado..
Los componentes no deben buscar ni guardar datos directamente y, desde luego, no deben presentar datos falsos a sabiendas. Deben enfocarse en presentar datos y delegar el acceso a datos a un servicio.
En esta lección, crearemos un HeroService donde toda las vistas de la aplicación podrán usar para obtener héroes. En lugar de crear un servicio con new, dependeremos de inyección de dependencia de Anglar para inyectarlo en el constructor HeroesComponent.
Los servicios son una excelente manera de compartir información entre cases que no se conocen entre sí. Crearemos MessageService que inyectará en dos sitios:
- En
HeroServiceque usa el servicio para enviar el mensaje. - En
MessageComponentque se muestra el mensaje.
Con el CLI de Angular, creamos un servicio llamado hero
ng generate service hero
El comando genera una estructura de la clase HeroService en src/app/hero/hero.service.ts. La clase HeroService debe verse como el siguiente ejemplo.
Archivo: src/app/hero.service.ts (Nuevo Servicio)
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}Observamos que el nuevo servicio importa el symbol de Angular Injectable y anota la clase con el decorador @Injectable(). Esto marca la clase como una que participa en el sistema de inyección de dependencia. La clase HeroServiceproporcionará un servicio inyectable y también puede tener sus propias dependencias inyectadas. Todavía no tenemos dependencias, pero pronto lo haremos.
El decorador @Injectable() acepta metadatos del objeto del servicio, de la misma manera que con el decorador @Component() que hicimos para la clase de componentes.
HeroService puede obtener los datos del héroe desde cualquier lugar - un servicio web, almacenamiento local, o una fuente de datos simulada.
Eliminar el acceso a los datos de los componentes significa que podemos cambiar de opinión sobre la implementación en cualquier momento, sin tocar ningún componente. No sabemos como funciona el servicio.
La implementación en esta lección continuará entregando héroes simulados.
Importamos Hero y HEROES.
import { Hero } from './hero';
import { HEROES } from './mock-heroes';Agrega un método getHeroes para devolver a los héroes falsos.
getHeroes(): Hero[] {
return HEROES;
}Debemos de poner HeroService a disposición del sistema de inyección de dependencia antes de que Angular pueda inyectarlo en HeroesComponent, como lo haremos a continuación. Haremos esto registrando un proveedor. Un proveedor es algo que puede crear o engregar un servicio; en este caso, una instalcia de la clase HeroService para proporcionar el servicio.
Ahora, debemos asegurarnos de que HeroService está registrado como proveedor de este servicio. Lo estamos registrando con un inyector, que es el objeto que es responsable de elegir e inyectar el proveedor donde se requiere.
De forma predeterminada, el comando CLI de Angular ng generate service registra un proveedor con el inyector raíz para el servicio al incluir los metadatos del proveedor al decorador @Injectable.
Si miramos la declaración @Injectable() justo antes de la definición de la clase HeroService, podemos ver que el valor de providedIn de los metadatos es 'root':
@Injectable({
providedIn: 'root',
})
Cuando proporcionamos el servicio en la raíz, Angular crea una única instancia compartida de HeroService e inyecta en cualquier clase que lo solicite. El registro del proveedor de @Injectable en los metadatos también permite que a Angular opimizar una aplicación eliminando el servicio si resulta que no se usa después de todo.
Para obtener más información sobre proveedores, consulta la sección proveedores. Para tener más información acerca de los inyectores consulta la guía de inyección de dependencias.
HeroService está ahora listo para conectar a HeroesComponent.
Esta es una muesta de código provisional que nos permitirá proporcionar y usar
HeroService. El código será diferente deHeroServiceen la revisión de código final.
Abre el archivo de la clase HeroesComponent.
Borra la importación HEROES, porque ya no la necesitaremos. Importaremos HeroService en su lugar.
Archivo: src/app/heroes/heroes.component.ts (importar HeroService)
import { HeroService } from '../hero.service';Reemplaza la definición de la propiedad heroes con una declaración simple.
heroes: Hero[];Agregamos un parámetro privado a heroServicede tipo HeroService al constructor.
constructor(private heroService: HeroService) { }El parámetro define simultáneamente una propiedad privada de heroService y la identifica como una instancia simple de HeroService.
Cuando Angular crea HeroesComponent, el sistema de Inyección de Dependencia establece el parámetro heroService en la única instancia de HeroService.
Crea una función para recuperar los héroes del servicio.
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}Si bien puedes llamar a getHeroes() en el constructor, esta no es la mejor práctica.
Reserva el constructor para la inicialización simple, como el cableado de los parámetros del constructor a las propiedades. El constructor no debería hacer nada. Ciertamente, no debería de llamar a una función que realiza solicitudes HTTP a un servidor remoto como lo haría un servicio de datos real.
En su lugar, llama a getHeroes() dentro del hook de ciclo de vida ngOnInit y deja que Angular llame a ngOnInit en el momento apropiado después de construir una instancia HeroesComponent.
ngOnInit() {
this.getHeroes();
}Después de que el navegador se refresque, la aplicación debería de ejecutarse como antes, mostrando una lista de héroes y una vista de detalles de héroe cuando haga clic en un nombre de héroe.
El método HeroService.getHeroes() tiene una firma síncrona, lo que implica que HeroService puede obtener héroes con sincronía. HeroesComponent consume el resultado de getHeroes() obteniendo los héroes encontrados de forma síncrona.
this.heroes = this.heroService.getHeroes();Esto no funcionará en una aplicación real. Te estás saliendo con la tuya ahora porque el servicio actualmente devuelve héroes falsos. Pero pronto la aplicación obtendrá héroes de un servidor remoto, que es una operación inherentemente asíncrona.
HeroService debe de esperar a que el servidor responda, getHeroes() no puede regresar de inmediato con datos de héroe, y el navegador no se bloqueará mientras el servicio espere.
HeroService.getHeroes() debe de tener una firma asíncrona de algún tipo.
Puede tomar una devolución de llamada. Podría regresar una Promise. Podría devolver un Observable.
En este tutorial, HeroService.getHeroes() se devolverá Observable en parte porque enventualmente usaremos el método de Angular HttpClient.get para obtener los héroes y HttpClient.get() devolverá un Observable.
Observable es una de las clases clave de la bliblioteca RxJS.
Más adelante aprenderemos que los métodos de Angular HttpClient devuelven RxJS Observable. En este tutorial, simularemos obtener datos del servidor con la función of() de RxJS.
Abrimo el archivo HeroService e importamos los symbols Observable y of de RxJS.
Archivo: src/app/hero.service.ts (Importacion Observable)
import { Observable, of } from 'rxjs';Remplaza el método getHeroes con este.
getHeroes(): Observable<Hero[]> {
return of(HEROES);
}of(Heroes) devuelve un Observable<Hero[]> que emite un único valor, la matriz de héroes simulados.
El método HeroService.getHeroes para devolver Hero[]. Ahora devuelve Observable<Hero[]>.
Vamos a tener que adaptar esta diferencia en HeroesComponent.
Encuentra el método getHeroes y reemplazalo con el siguiente código. (Mostramos la versión anterior para comparar)
Archivo: heroes.component (Observable)
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}Archivo: heroes.component (Original)
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}Observable.suscribe() es la única diferencia crítica.
La versión anterior asigna una matriz de héroes a la propiedad heroes del component. La asignación se produce de forma síncrona, como si el servidor pudiera devolver héroes al instante o el navegador pudiera congelar la interfaz de usuario mientras esperaba la respuesta del servidor.
Esto no funcionaría cuando en realidad HeroService se le realicen solicitudes de un servidor remoto.
La nueva version espera que Observable emita la serie de héroes, lo que podría suceder ahora o en varios minutos a partir de ahora. Luego subscribe pasa la matriz emitida a la devolución de la llamada, que establece la propiedad del componente heroes.
En esta sección lo haremos.
- Agrega
MessagesComponentque muestra los mensaje dela aplicación en la parte inferior de la pantalla. - Crea un inyectable, a nivel de aplicación
MessageServicepara enviar mensajes que se muestren. - Inyectar
MessageServiceenHeroService. - Muestra un mensaje cuando
HeroServiceobtiene con éxito héroes.
Usa el CLI para crear MessagesComponent.
ng generate component messages
El cCLI crea los archivos de los componentes en la carpeta y los declara en src/app/messagesMessagesComponentAppModule.
Modifica la plantilla AppComponent par amostrar el generado MessagesComponent.
Archivo: /src/app/app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>Deberíamos ver el párrafo predeterminado MessageComponent en la parte inferior de la página.
Utiliza el CLI para crear MessageService en src/app.
ng generate service message
Abre MessageService y reemplaza su contenido con lo siguiente.
Archivo: /src/app/message.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}El servicio expone su caché messages y dos métodos: uno add() un mensaje a la caché y otro a clear() la caché.
Vamos a volver a abrir HeroServicee importar el MessageService
Archivo: /src/app/hero.service.ts (importar MessageService)
import { MessageService } from './message.service';Modifica el constructor con un parámetro que declare una propiedad messageService privada. Angular inyectará el singleton MessageServiceen esa propiedad cuando cree HeroService.
constructor(private messageService: MessageService) { }Este es un escenario típico de "servicio en servicio": se inyecta
MessageServiceenHeroServiceque se inyecta enHeroesComponent.
Modifica el método getHeroes par aenvíar un mensaje cuando se vaya a buscar a los héroes.
getHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}MessageComponent debería mostrar todos los mensajes, incluido el mensaje enviado por HeroService cuando obtiene héroes.
Abre MessageComponente importa MessageService
Archivo: /src/app/messages/messages.component.ts (importar MessageService)
import { MessageService } from '../message.service';Modifica el constructor con un parámetro que declare una propiedad pública messageService. Angular inyectará el singleton MessageService en esa propiedad cuando cree el MessagesComponent
constructor(public messageService: MessageService) {}La propiedad messageService debe ser pública porque está a punto de unirse a ella en la plantilla.
Angular sólo se une a las propiedades de los componentes públicos
Remplaza la plantilla MessagesComponent generada por el CLI con lo siguiente.
Archivo: /src/app/messages/messages.component.html
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>Esta plantilla se une directamente a la del componente messageService.
ngIfsólo muestra el área de mensajes si hay mensajes para mostrar.ngForpresenta la lista de mensajes en elementos repetidos.- Un enlace de evento de Angular víncula el evento click del botoón a
MessageService.clear().
Los mensajes se verán mejor cuando agregemos los estios de CSS privados a los messages.component.css listados en una de las pestañas de revisión de código final a continuación.
El navegador se actualiza y la página muestra la lista de héroes. Desplázalo hasta la parte inferior para ver el mensaje HeroService el área de mensajes. Haz click en el botón "borrar" y el área de mensaje desaparecerá.
Archivo: /src/app/hero.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
}Archivo: /src/app/message.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}Archivo: /src/app/heroes/heroes.component.ts
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 {
selectedHero: Hero;
heroes: Hero[];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}Archivo: /src/app/messages/messages.component.ts
import { Component, OnInit } from '@angular/core';
import { MessageService } from '../message.service';
@Component({
selector: 'app-messages',
templateUrl: './messages.component.html',
styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {
constructor(public messageService: MessageService) {}
ngOnInit() {
}
}Archivo: /src/app/messages/messages.component.html
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button class="clear"
(click)="messageService.clear()">clear</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>Archivo: /src/app/messages/messages.component.css
/* MessagesComponent's private CSS styles */
h2 {
color: red;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: crimson;
font-family: Cambria, Georgia;
}
button.clear {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
button.clear {
color: #888;
margin-bottom: 12px;
}Archivo: /src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessagesComponent } from './messages/messages.component';
@NgModule({
declarations: [
AppComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent
],
imports: [
BrowserModule,
FormsModule
],
providers: [
// no need to place any providers due to the `providedIn` flag...
],
bootstrap: [ AppComponent ]
})
export class AppModule { }Archivo: /src/app/app.component.html
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>- Refactorizamos el acceso a los datos de la clase
HeroService. - Registramos el
HeroServicecomo el proveedor de su servicio en el nivel de raíz para que pueda ser inyectado en cualquier lugar de la aplicación. - Utilizamos la inyección de dependencia de Angular a inyectarlo en un componente.
- Proporcionamos al método
HeroServicerecoger datos de una firma asincrónica. - Descubrimos
Observabley la biblioteca RxJS Observable . Usamos RxJSof()para devolver un observable de héroes falsos(Observable<Hero[]>). - El componente
ngOnInitdel hook del ciclo de vida llamando al métodoHeroService, no al constructor. - Creamos un
MessageServicepara la comunicación débilmente acoplada entre clases. HeroServiceinyectado en un componente y creando otro servicio inyectado deMessageService.
Hay nuevos requisitos para la aplicación Tour of Heroes:
- Agregar una vista de Tablero.
- Agregar la capacidad de navegar entre las vistas Héroes y Tablero.
- Cuando los usuarios hacen click en un nombre de héroe en cualquiera de las vistas, navega hasta una vista de detalle del héroe seleccionado.
- Cuando los usuarios hacen click en un enlace profundo en un correo electrónico, abre la vista detalles de un héroe en particular.
Cuando hayamos terminado, los usuarios podrán navegar por la aplicación de esta manera:
Una mejor páctica de Angular es cargar y configurar el enrutador en un módulo separado de nivel superior dedicado al enrutamiento e importado por la raíz AppModule.
Por convención, el nombre de la clase del módulo es AppRoutingModule y pertenece a app-routing.module.ts en la carpeta src/app.
Usamos el CLI para generarlo:
ng generate module app-routing --flat --module=app
--flatcoloca el archivo ensrc/appen lugar de su propia carpeta.--module=apple dice a el CLI que lo registre enimportsde la matrizAppModule.
El archivo generado se verá así:
Archivo: src/app/app-routing.module.ts (generado)
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
@NgModule({
imports: [
CommonModule
],
declarations: []
})
export class AppRoutingModule { }Por lo general, no declara los componentes en un módulo de enrutamiento para que pueda eliminar el array @NgModule.declarations y eliminar las referencias de CommonModule.
Configuraremos el enrutador con Routes, en el RouterModule así que importamos dos symbols de la librería @angular/router.
Agregamos un array @NgModule.exports con RouterModule en él. Exportamos RouterModule hace que las directivas de enrutadores estén disponible para su uso en el componenteAppModule que necesitarémos más adelante.
AppRoutingModule se ve así ahora:
Archivo: src/app/app-routing.module.ts (v1)
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
@NgModule({
exports: [ RouterModule ]
})
export class AppRoutingModule {}Las rutas le dicen al enrutador qué vista mostrar cuando un usuario hace clic en un enlace o pega una URL en la barra de direcciones del navegador.
Una Route típica de Angular tiene dos propiedades:
path: una cadena que coincide con la URL en la barra de direcciones del navegador.component: el componente que debe crear el enrutador cuando navega por esta ruta.
Vamos a intentar navegar hacia el componente HeroesComponent cuando la URL es esta localhost:4200/heroes.
Importa el archivo HeroesComponent par aque pueda referenciarlo en Route. Luego, define un array de rutas con un único componente route.
import { HeroesComponent } from './heroes/heroes.component';
const routes: Routes = [
{ path: 'heroes', component: HeroesComponent }
];Una vez hayamos terminado de configurarlo, el enrutador coincidirá con esa URL path: 'heroes' y mostrará el componente HeroesComponent.
Primero debemos de inicializar el enrutador y comenzar a escuchar los cambios de ubicación del navegador.
Agregar RouterModule al array @NgModule.imports y configurarla con routes en un solo paso llamando dentro del array RouterModule.forRoot(), de esta manera el array imports, queda así:
imports: [ RouterModule.forRoot(routes) ],Se llama al método
forRoot()porque configura el enrutador en el nivel raíz de la aplicación. El métodoforRoot()suministra los proveedores de servicios y las directivas necesarias para el enrutamiento y realiza la navegación inicial en función de la URL del navegador actual.
Abre la plantilla AppComponenty reemplaza el elemento <app-heroes> con un elemento <router-outlet>.
Archivo: src/app/app.component.html (router-outlet)
<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>Hemos eliminado <app-heroes> porque sólo mostraremos HeroesComponent cuando el usuario navega hacia él.
<router-outlet> le dice al enrutador dónde mostrar las vistas enrutadas.
RouterOutletes una de las directivas de enrutador que están disponibles para lasAppComponentporqueAppModuleimportaAppRoutingModuleque exportaRouterModule.
Debería ejecutarse con este comando CLI.
ng serve
El navegador debe de actualizarse y mostrar el título de la aplicación, pero no la lista de héroes.
Mira la barra de dirección del navegador. La URL termina en /. La ruta de acceso a HeroesComponet es /heroes.
Agrega /heroes a la URL de la barra de direcciones del navegador. Deberíamos ver el conocido maestro/detalle de la vista de héroes.
Los usuarios no deberían tener que pegar una URL de ruta en la barra de direcciones. Deben poder hacer clic en un enlace para navegar.
Agregamos un elemento <nav> y, dentro de eso, un elemento de anclaje que, al hacer click, desencadena la navegación hacia HeroesComponent. La plantilla AppComponent revisada se ve así:
Archivo: src/app/app.component.html (héroes RouterLink)
<h1>{{title}}</h1>
<nav>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>El atributo routerLink se estable en /heroes, la cadena con la que el enrutador coincide con la ruta HeroesComponent. El routerLink es el selector de la directiva RouterLink que hace que el usuario haga click en las navegaciones del enrutador. Es otra de las directivas públicas en RouterModule.
El Navegador se actualiza y muestra el título de la aplicación y el enlace de héroes, pero no la lista de héroes.
Hacemos click en el enlace. La barra de direcciones se actualiza a /heroes y aparece en la lista de héroes.
Haz esto y el futuro navegador de enlaces se verá mejor añadiendo unos estilos privados de CSS a
app.component.css. Tendrás esto en la revisión final del código.
El enrutamiento tiene más sentido cuando hay múltiples vistas. Hasta ahora solo hay una vista de héroes.
Agregamos una DashboardComponent usando el CLI:
ng generate component dashboard
El Cli genera los archivos para DashboardComponent y los declara en AppModule.
Reemplaza el contenido del archivo predeterminado en estos tres archivos de la siguiente manera y luego regrese para una pequeña discusión:
Archivo: src/app/dashboard/dashboard.component.html
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" class="col-1-4">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>Archivo: src/app/dashboard/dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}Archivo: src/app/dashboard/dashboard.component.css
/* DashboardComponent's private CSS styles */
[class*='col-'] {
float: left;
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
h4 {
position: relative;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607d8b;
border-radius: 2px;
}
.module:hover {
background-color: #eee;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}La plantilla presenta un gir de enlaces de héroes.
- El
*ngForcrea tantos enlaces como están en el array del componenteheroes. - Los enlaces se diseñan como bloques de coloers por
dashboard.component.css. - Los enlaces no van a ningún lado todavía, pero lo harán en breve.
La clase es similar a la clase de HeroesComponent.
- Define una propiedad
heroesal array. - El constructor espera que Angular lo inyecte
HeroServiceen una propiedadheroServiceprivada. - El hook
ngOnInit()del ciclo de vida llama agetHeroes.
getHeroes devuelve la lista de héroes divididos en las posiciones 1 y 5, devolviendo sólo cuatro de los Héroes principales (2º, 3º, 4º, y 5º).
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}Para navegar al tablero, el enrutador necesita una ruta apropiada.
Importar el DashboardComponent en el AppRoutingModule.
Archivo: src/app/app-routing.module.ts (importa DashBoardComponent)
import { DashboardComponent } from './dashboard/dashboard.component';Agrega una ruta al array AppRoutingModule.routes que coincida con una ruta a DashboardComponent.
{ path: 'dashboard', component: DashboardComponent },Cuando se inicia la aplicación, la barra de direcciones de los navegadores apunta a la raíz del sitio web. Eso no coincide con ninguna ruta existente por lo que el enrutador no navega a ninguna parte. El espacio debajo de el <router-outlet> está en blanco.
Para hacer que la aplicación vaya automáticamente al tablero, agregua la siguiente ruta al array AppRoutingModule.Routes.
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },Esta ruta redirige una URL que coincide completamente con la ruta vacía a la ruta cuya ruta es /dashboard.
Después de que el navegador se actualiza, el enrutador carga el DashboardComponent y la barra de direcciones del navegador muestra la URL /dashboard
El usuario debe poder navegar hacia adelante y hacia atrás entre el DashboardComponent y el HeroesComponent haciendo clic en los enlaces en el área de navegación cerca de la parte superior de la página.
Agregua un enlace de navegación del tablero a la plantilla AppComponent de shell, justo encima del enlace Héroes.
Archivo: src/app/app.component.html
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>Después de que el navegador se actualice, puedes navegar libremente entre las dos vistas haciendo click en los enlaces.















