Estás viendo la documentación de Angular
Maestro / detalle
Nuestra historia necesita más héroes. Vamos a expandir nuestra app del Tour de los héroes para que muestra una lista de héroes, nos permita seleccionar un héroe, y muestre los detalles del mismo.
Puedes ejecutar el ejemplo en vivo para esta parte.
¿Qué necesitamos para mostrar una lista de héroes?. Primero, vamos a necesitar una lista de héroes. Queremos mostrar los héroes en el template de la vista, así que vamos a necesitar también una forma de hacer eso.
A donde habíamos quedado
Antes de continuar con la Parte 2 del tour de los héroes, verifiquemos que tenemos la siguiente estructura después de la Parte 1(todo: add link). Si no, necesitamos volver a la Parte 1 y fijarnos que nos faltó.
angular-tour-of-héroes
|-> src
|-> app
|-> app.component.ts
|-> app.module.ts
|-> main.ts
|-> index.html
|-> styles.css
|-> systemjs.config.js
|-> tsconfig.json
|-> node_modules ...
|-> package.json
Manten la app transpilando y ejecutando
Vamos a comenzar con el compilador TypeScript, hacer que observe los cambios y arrancar nuestro servidor. Lo hacemos tipeando
npm start
Este comando ejecuta el compilador en modo observador, inicia el servidor, lanza la aplicación en un navegador, y mantiene la app ejecutandose mientras continuamos la construcción del Tour de los Héroes.
Mostrando nuestros héroes
Creando los héroes
Vamos a crear un array de 10 héroes.
app.component.ts (array de héroes)
const héroeS: 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' }
];
El array héroeS es de tipo Hero, la clase definida en la parte uno. Queremos obtener esta lista desde un servicio web, pero vamos a ir de a poco y primero vamos a mostrar datos hardcodeados.
Exponiendo los héroes
Creemos una propiedad en AppComponent que expone los héroes para enlazar:
app.component.ts (propiedad del array de héroes)
héroes = héroeS;
No necesitamos definir de que tipo es héroes, TypeScript lo infiere desde el array héroeS.
Podríamos haber definido la lista de héroes en esta clase de componente.
Pero sabemos que más adelante vamos a obtener los héroes desde un servicio de datos. Dado que sabemos que vamos a cambiar esto más adelante, nos parece lógico separar los datos de héroes de su clase de implementación desde un principio.
Mostrando los héroes en un template
Nuestro componente tiene héroes. Creemos una lista sin ordenar en nuestro template para mostrarlos. Vamos a insertar el siguiente trozo de HTML debajo del titulo y arriba de los detalles del héroe.
app.component.ts (template de héroes)
<h2>My héroes</h2>
<ul class="héroes">
<li>
<!-- each hero goes here -->
</li>
</ul>
Ahora tenemos un template que podemos llenar con nuestros héroes.
Listando héroes con ngFor
Queremos enlazar nuestro array de héroes en nuestro componente a nuestro template, iterar sobre ellos y mostrarlos de forma individual. Necesitamos algo de ayuda de Angular para hacer esto.
Hagamoslo paso a paso.
Primero modifica el tag <li>
agregando la directiva de Angular ngFor
app.component.ts (ngFor)
<li *ngFor="let hero of héroes">
El asterisco al principio (*) en frente de ngFor es una parte importante de esta sintaxis.
El (*) al principio de ngFor indica que el elemento
<li>
y sus hijos constituyen un template maestro.La directiva ngFor itera sobre el array héroes retornado por la propiedad AppComponent.héroes y haciendo instancias (copias) de ese pedacito del template.
El texto entre comillas asignado a ngFor significa “toma cada héroe en el array héroes, guardalo en la variable local hero y hazlo disponible a cada instancia del template.”
La palabra clave let antes de “hero” hace que esté disponible el item actual en la variable hero. Podemos referenciar esta variable en nuestro template para acceder a las propiedades del héroe.
Ahora insertamos algo de contenido entre los tags <li>
que usan la variable del template hero
para mostrar las propiedades del héroe
app.component.ts (template ngFor)
<li *ngFor="let hero of héroes">
<span class="badge"></span>
</li>
Dandole estilo a nuestros héroes
Nuestra lista de héroes luce un poco aburrida. Queremos que sea obvio para el usuario que héroe está pasando por encima con el mouse y cual está seleccionado. Agreguemos algunos estilos a nuestro componente seteando la propiedad styles en el decorador @Component usando los siguientes estilos CSS:
app.component.ts (template ngFor)
styles: [`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.héroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.héroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.héroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.héroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.héroes .text {
position: relative;
top: -3px;
}
.héroes .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;
}
`]
Nota como nuevamente hemos usado la notación de comilla invertida para cadenas de texto de muchas líneas.
¡Esos son muchos estilos! Podemos ponerlos en linea como mostramos acá, o podemos moverlos a su propio archivo lo que haría más facil nuestro desarrollo. Haremos esto en un capítulo posterior. Por ahora sigamos.
Cuando asignamos estilos a un componente, estos solo aplican para ese componente específico. Nuestros estilos solo van a aplicar a nuestro AppComponent y no se van a “derramar” sobre otro HTML.
Nuestro template para mostrar los héroes debería verse así:
app.component.ts (héroes con estilo)
<h2>My héroes</h2>
<ul class="héroes">
<li *ngFor="let hero of héroes">
<span class="badge"></span>
</li>
</ul>
Seleccionando un héroe
Tenemos una lista de héroes y tenemos un héroe único mostrado en nuestra aplicación. La lista y el héroe único no están conectados en ninguna forma. Queremos que el usuario seleccione un héroe de nuestra lista y este héroe seleccionado aparezca en la vista de detalles. Este patrón de UI se conoce como “maestro-detalle”. En nuestro caso, el maestro es la lista de héroes, y el detalle es el héroe seleccionado.
Conectemos el maestro al detalle a través de una propiedad del componente llamada selectedHero que cambiaramos con un evento de click.
El evento click
Modifiquemos el <li>
insertando un evento de Angular para que reaccione al evento click.
app.component.ts (pedacito del template)
<li *ngFor="let hero of héroes" (click)="onSelect(hero)">
<span class="badge"></span>
</li>
Enfoquemonos en el enlace del evento
(click)="onSelect(hero)"
Los paretentesis indican que estamos agregando un evento al <li>
. La expresión
a la derecha del signo igual llama al método onSelect() de la clase AppComponent,
pasando la variable hero como argumento. Es el mismo hero que definimos antes
usando el ngFor
Agregando el manejador del click
Nuestro enlace de eventos se refiere a un método onSelect que todavía no existe. Agregaremos ese método a nuestro componente ahora.
¿Qué debería hacer este método? Debería cambiar el héroe seleccionado del componente por el héroe en el que el usuario hizo click.
Nuestro componente tampoco tiene un “héroe seleccionado”. Empecemos por ahí.
Exponer el héroe seleccionado
No necesitamos más la propiedad estática hero en el AppComponent. Remplazala con esta simple propiedad selectedHero:
app.component.ts (selectedHero)
selectedHero: Hero;
Hemos decidido que ninguno de los héroes debería estar seleccionado antes de que el usuario haga click en uno asi que no vamos a inicializar selectedHero como lo estuvimos haciendo con hero.
Ahora agrega un método onSelect que cambia selectedHero por el héroe que el usuario hizo click:
app.component.ts (onSelect)
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
Queremos mostrar los detalles del héroe seleccionado en nuestro template. En este momento el template aún se está refiriendo a la vieja propiedad hero. Arreglemos el template para enlazar a la nueva propiedad selectedHero
app.component.ts (pedazo de template)
<h2> details!</h2>
<div><label>id: </label></div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
Ocultar el detalle vacío con ngIf
Cuando nuestra app carga vamos a ver una lista de héroes, pero no hay ningun héroe seleccionado. La propiedad selectedHero es undefined, Por eso es que vemos el siguiente error en la consola del navegador:
EXCEPTION: TypeError: Cannot read property 'name' of undefined in [null]
Recuerda que estamos mostrando selectedHero.name en el template. Esta propiedad name no existe porque selectedHero es undefined.
Vamos a encargarnos de este problema manteniendo el detalle del heroe fuera del DOM hasta que haya un heroe seleccionado.
Envolvemos el HTML del detale del héroe con un <div>
. Luego agregamos la directiva
de Angular ngIf y la configuramos con la propiedad selectedHero de nuestro componente.
app.component.ts (ngIf)
<div *ngIf="selectedHero">
<h2> details!</h2>
<div><label>id: </label></div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
</div>
Recuerda que el asterisco (*) adelante de ngIf es una parte muy importante de la sintaxis.
Cuando no hay nada en selectedHero, la directiva ngIf elimina el HTML del detalle del héroe del DOM. No hay elementos del detalles ni enlaces de datos que puedan causar error.
Cuando el usuario elige un héroe, selectedHero* se vuelve “truthy” (equivalente a true en JavaScript/TypeScript) y **ngIf coloca el contenido del detalle del héroe en el DOM y evalúa los enlaces de datos.
ngIf y ngFor son llamadas “directivas estructurales” porque pueden cambiar la estructura de porciones del DOM. En otras palabras, ellas dan estructura a la forma en que Angular muestra contenido en el DOM.
El navegador se actualiza y vemos la lista de héroes pero no el detalle del héroe seleccionado. La directiva ngIf lo mantiene fuera del DOM mientras selectedHero sea undefined.
Cuando hacemos click en un héroe de la lista, el heroe seleccionado muestra los detalles del héroe. Todo funciona como lo esperamos.
Dandole estilo a la selección
Vemos el héroe seleccionado pero no podemos localizar de forma rapida ese héroe en la lista.
Podemos arreglarlo agregando una clase de CSS selected al elemento <li>
apropiado
en la lista maestra. Por ejemplo, cuando seleccionamos Magneta de la lista de héroes,
podemos hacer que resalte de forma visual dandole un sutil color de fondo como mostramos aqui:
Vamos a agregar un enlace a una propiedad en el atributo class para la clase selected en el template. Configuramos esto con una expresión que compare el valor actual de selectedHero con la variable hero.
La clave es el nombre de la clase CSS (selected). Este valor es true si ambos heroes (el seleccionado y el actual son el mismo o falso, de lo contrario). Estamos diciendo “aplicar la clase selected si ambos heroes son el mismo, quitarla si no lo son”.
app.component.ts (configurando la clase CSS)
[class.selected]="hero === selectedHero"
Notemos que en el template class.selected esta rodeado de corchetes ([]). Esta es la sintaxis para un enlace de propiedad, es un enlace de datos en el que los datos fluyen en un sentido desde el origen de datos (la expresión hero === selectedHero) a una propiedad class.
app.component.ts (dandole estilo a cada héroe)
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge"></span>
</li>
El navegador recarga nuestra app. Seleccionamos el heroe Magneta y la selección está identificada de forma clara por el color del fondo del elemento.
Si seleccionamos un héroe diferente, el elemento coloreado pasa a ser el de ese héroe.
Acá está el archivo app.component.ts como se encuentra en este momento:
app.component.ts
import { Component } from '@angular/core';
export class Hero {
id: number;
name: string;
}
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' }
];
@Component({
selector: 'my-app',
template: `
<h1></h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge"></span>
</li>
</ul>
<div *ngIf="selectedHero">
<h2> details!</h2>
<div><label>id: </label></div>
<div>
<label>name: </label>
<input [(ngModel)]="selectedHero.name" placeholder="name"/>
</div>
</div>
`,
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;
}
`]
})
export class AppComponent {
title = 'Tour of Heroes';
heroes = HEROES;
selectedHero: Hero;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}
El camino que hemos recorrido
Esto es lo que hemos logrado en este capítulo:
- Nuestro tour de los héroes ahora muestra una lista de heroes seleccionados.
- Hemos agregado la habilidad para seleccionar un héroe y mostrar su detalle.
- Hemos aprendido como usar directivas de Angular como ngIf y ngFor en un template de un componente.
Puedes ejecutar el ejemplo en vivo para esta parte.
El camino por delante
El tour de los heroes ha crecido pero está lejos de estar completo. No podemos meter la aplicación entera en un solo componente. Necesitamos separarlo en sub componentes y decirles como trabajar juntos como aprenderemos en el próximo capítulo.
Siguiente cápitulo: Múltiples componentes