Construye Minioland: Tu primera aplicación con Angular | Parte #3

NOTA

Esta es la parte #3 de la serie Construye Minioland. Si acabas de aterrizar aquí, puedes ver la parte #1 antes.

Tuneando los parámetros del MinionComponent para mostrar una página individual por cada minion

Desde la página Minions ya podemos acceder a cada uno de nuestros minions pinchando en "Ver más info". Al menos, eso es lo que nos indica nuestra URL. Ya que al pinchar en Lobeznion, por ejemplo, nuestra URL debería ser:

http://localhost:4200/minion/6

Pero a parte de eso, sólo vemos la página que dice minion works!

Vamos a ponernos a trabajar sobre esa página ?‍? 

1. Vamos a nuestro archivo minion.component.ts y eliminamos el ngOnInit() { } porque no lo vamos a utilizar.

2. Desde ese archivo vamos a extraer el parámetro que nos muestra nuestra URL (en el ejemplo anterior sería el 6). Para eso, debemos importar el paquete ActivatedRoute. Se usa de la misma manera con la que usamos un service, es decir, lo inyectamos en el constructor.

Después de eso vamos a utilizar la propiedad params y vamos a "suscribirnos" a esos ids que hemos creado. El concepto de "suscripción" es un poco abstracto, al menos, en este momento, pero vamos a entenderlo como la sintaxis que necesitamos para poder manejar esos ids correspondientes a cada uno de nuestros minions.

Si haces un console.log de params desde la URL del minion con id 6, verás que en la consola se imprime un objeto con una propiedad llamada id: ''6''.

Si queremos que solamente imprima el id, se lo pasamos a nuestro parámetro como una propiedad:

this.activatedRoute.params.subscribe(params => {

  console.log(params.id);

}

? Estamos utilizando el término id porque ese el nombre que le hemos puesto a nuestro parámetro en las routes del archivo app.routes.ts, ¿te acuerdas?:

{ path: 'minion/:id', component: MinionComponent},

Nuestro archivo minion.component.ts nos quedaría de la siguiente manera:

import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-minion', templateUrl: './minion.component.html', styleUrls: ['./minion.component.css'] }) export class MinionComponent { constructor(private activatedRoute: ActivatedRoute) { this.activatedRoute.params.subscribe( params => { console.log(params.id); }) } }

3. Para obtener el minion en particular vinculado a un id, debemos modificar nuestro service:

Añadimos un nuevo método llamado getMinion() , que va a hacer un return del índice de cada minion, basándonos en el método getMinions().

Nuestro archivo minions.service.ts nos quedaría así:

import { Injectable } from '@angular/core'; @Injectable() export class MinionsService { private minions: Minion[] = [ { name: "Kevin", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/kevin.jpg", birth: "1951", side:"de los buenos" }, { name: "Josua", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/Josua.jpg", birth: "1672", side:"malvado" }, { name: "Dave", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/dave.jpg", birth: "1723", side: "de los buenos" }, { name: "Mudito", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/mudito.jpeg", birth: "1379", side:"de los buenos" }, { name: "Llongueras", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/llongueras.jpg", birth: "1687", side: "malvado" }, { name: "Minioncé", bio: "Le va el cante, dar la nota, ama los karaokes, es el rey y reina de la fiesta. Invítalo a tu fiesta o te arrepentirás.", img: "assets/img/minionce.jpg", birth: "1976", side: "de los buenos" }, { name: "Lobeznion", bio: "No lo enfades, este bichillo tiene muy malas pulgas... aunque sólo mide medio metro y ¡no puede ser más gracioso!", img: "assets/img/lobeznion.jpg", birth: "2017", side: "malvado" }, { name: "Minion Presley", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/minion-presley.jpg", birth: "2017", side: "malvado" } ]; constructor() { console.log('minions service listo para usar, oiga!'); } getMinions() { return this.minions; } getMinion(idx) { return this.minions[idx]; } } export interface Minion { name: string; bio: string; img: string; birth: string; side: string; }

4. Volvemos al archivo minion.component.ts, donde nos creamos una propiedad llamada minion que va a ser un objeto vacío. Esa será nuestra variable local que utilizaremos en el template de este componente. 

5. Debemos importar nuestro service si queremos utilizar el método que acabamos de crear ahí. ¡No te olvides de inyectarlo en el constructor!

Una vez importado, ya podemos utilizarlo dentro de nuestro activateRoute. 

En este momento nuestro código del archivo minion.component.ts tiene esta pinta:

import { Component } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { MinionsService } from '../../services/minions.service'; @Component({ selector: 'app-minion', templateUrl: './minion.component.html', styleUrls: ['./minion.component.css'] }) export class MinionComponent { minion: any = {}; constructor(private activatedRoute: ActivatedRoute, private minionsService: MinionsService) { this.activatedRoute.params.subscribe( params => { // console.log(params.id]); this.minion = this.minionsService.getMinion(params.id); }) } }

Dando forma al template del MinionComponent

En el archivo minion.component.ts podemos hacernos un console.log (en el constructor) para ver en nuestras dev tools las propiedades del minion que tenemos disponibles. 

Así puede que te resulte más fácil y rápido trabajar.

1. Como hemos adelantado, vamos a utilizar la variable minion de nuestro archivo minion.component.ts como referencia para acceder a las propiedades de cada minion.

2. Para determinar qué tipo de minion es (puede ser "de los buenos" o "malvado"), vamos a utilizar un *ngIfdándole así a Angular dos posibilidades: 

  • si es "de los buenos", en la pantalla veremos un emoji de un angelito
  • si es "malvado", en la pantalla veremos un emoji de un demonio

Nuestro archivo queda ahora así:

<div class="container mt-4 animated fadeIn fast">

<h1>{{ minion.name }} <small> | Nacido en {{ minion.birth }} </small> </h1>

<hr>

<div class="row">

<div class="col-md-4">

<img [src]="minion.img" [alt]="minion.name" class="img-fluid">

<a [routerLink]="['/minions']" class="btn btn-outline-danger btn-block mt-3">Volver</a>

</div>

<div class="col-md-8">

<h3>{{ minion.name }}</h3>

<hr>

<p>{{ minion.bio }}</p>

<h5> Lado: {{ minion.side }} </h5>

<div>

<img *ngIf="minion.side === 'de los buenos'" src="../../../assets/img/de los buenos.PNG" alt="" class="img-fluid side">

<img *ngIf="minion.side === 'malvado'" src="../../../assets/img/de los malos.PNG" alt="minion.name" class="img-fluid side">

</div>

</div>

</div>

</div>

3. Por último, añadimos un pequeño retoque en el CSS (archivo minion.component.css), para que nuestra image (emojis de angelito/demonio) tenga un tamaño adecuado.

.side { width: 50px; }

¡Y ya está! ???

Nuestra app tiene ahora este aspecto cada vez que consultamos la página de cualquier minion:

pagina kevin

Creando un buscador de minions

En esta sección vamos a hacer que funcione nuestro buscador de minions, que podemos encontrarlo en la navbar.

1. En el archivo navbar.component.html, debemos tener acceso al valor del input de búsqueda:

<input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">

Para ello, le añadimos un alias a ese input, al que llamamos #searchTerm.

Nuestro objetivo es que se active una función cada vez que alguien escriba algo en nuestro input y le de a enterPara hacer eso, incluimos el evento (keyup.enter) y nos creamos una función llamada searchMinion()A esa función tenemos que mandarle el valor de nuestro input, y para eso usamos el alias que le hemos asignado.

También queremos que el botón de búsqueda funcione. Para ello, añadimos un evento (click) al botón y lo vinculamos a la misma función.

2. Cambiamos el type del botón por button, y el form que envuelve a nuestro input por un div, para que evitar que se haga un submit de nuestra búsqueda a...ninguna parte.

Nuestro archivo queda de la siguiente manera:

<nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <a class="navbar-brand" href="#">Minioland</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav mr-auto"> <li class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/home']" >Home<span class="sr-only">(current)</span></a> </li> <li class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/minions']" >Minions</a> </li> <li class="nav-item" routerLinkActive="active"> <a class="nav-link" [routerLink]="['/about']" >About</a> </li> </ul> <div class="form-inline my-2 my-lg-0"> <input class="form-control mr-sm-2" type="search" placeholder="Search minion" aria-label="Search" #searchTerm (keyup.enter)="searchMinion(searchTerm.value)"> <button class="btn btn-outline-success my-2 my-sm-0" type="button" (click)="searchMinion(searchTerm.value)">Search</button> </div> </div> </nav>

3. Nos creamos esa función en el archivo navbar.component.ts.

Si hacemos un console.log en la función, verás que en la consola sale cualquier palabra que busques en el buscador al darle a enter.

Nuestro archivo queda de la siguiente manera:

import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', styleUrls: ['./navbar.component.css'] }) export class NavbarComponent implements OnInit { constructor() { } ngOnInit() { } searchMinion(term: string) { console.log(term); } }

4. Modificamos nuestro service para que sea posible realizar una búsqueda en nuestro array de minions. 

Para ello, nos creamos una función en nuestro archivo minions.service.ts llamada searchMinions()Vamos a aplicar algo de lógica de programación dentro:

  • usaremos un método muy útil de los arrays, el filter()Este método coge el array al que lo apliquemos y devuelve otro array con únicamente los elementos que cumplan la condición que nosotros le hayamos dado. Es, como su nombre indica, un filtro, aunque también podemos verlo como un embudo. 
  • a nuestra función le pasamos como parámetro un term.
  • mediante el método filter(), le pasamos un parámetro (minion) y una callback function, que comprobará si el término introducido por el usuario coincide con el nombre de alguno de nuestros minions (esta es nuestra condición). 
  • hacemos la comparación en lowercase para homogeneizar la búsqueda.

Si el método filter() no te ha quedado claro, puedes consultar uno de mis guías sobre métodos de los arraysy/o este artículo de CSS Tricks, la chica se lo curra tanto que hasta compone una canción para ayudarte a recordar este método y otros. ??‍?  ?

Nuestro archivo minions.service.ts quedaría así:

import { Injectable } from '@angular/core'; @Injectable() export class MinionsService { private minions: Minion[] = [ { name: "Kevin", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/kevin.jpg", birth: "1951", side:"de los buenos" }, { name: "Josua", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/Josua.jpg", birth: "1672", side:"malvado" }, { name: "Dave", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/dave.jpg", birth: "1723", side: "de los buenos" }, { name: "Mudito", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/mudito.jpeg", birth: "1379", side:"de los buenos" }, { name: "Llongueras", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/llongueras.jpg", birth: "1687", side: "malvado" }, { name: "Minioncé", bio: "Le va el cante, dar la nota, ama los karaokes, es el rey y reina de la fiesta. Invítalo a tu fiesta o te arrepentirás.", img: "assets/img/minionce.jpg", birth: "1976", side: "de los buenos" }, { name: "Lobeznion", bio: "No lo enfades, este bichillo tiene muy malas pulgas...aunque sólo mide medio metro y ¡no puede ser más gracioso!", img: "assets/img/lobeznion.jpg", birth: "2017", side: "malvado" }, { name: "Minion Presley", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/minion-presley.jpg", birth: "2017", side: "malvado" } ]; constructor() { console.log('minions service listo para usar, oiga!'); } getMinions() { return this.minions; } getMinion(idx: string) { return this.minions[idx]; } searchMinions = (term: string): Minion[] => { return this.minions.filter(minion => minion.name.toLowerCase().includes(term.toLowerCase())); } } export interface Minion { name: string; bio: string; img: string; birth: string; side: string; }

Creando la pantalla de nuestros resultados de búsqueda

1. Nos creamos un componente (Browser) que será la pantalla que se mostrará cuando hagamos una búsqueda de un minion, mostrando los resultados. 

ng g c components/browser --skipTests

2. Añadimos una nueva route con este componente a nuestro archivo app.routes.ts, para así avisar a Angular de su existencia. 

Nuestro archivo nos quedaría así:

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HomeComponent } from './components/home/home.component'; import { AboutComponent } from './components/about/about.component'; import { MinionsComponent } from './components/minions/minions.component'; import { MinionComponent } from './components/minion/minion.component'; import { BrowserComponent } from './components/browser/browser.component'; const routes: Routes = [ { path: 'home', component: HomeComponent}, { path: 'about', component: AboutComponent}, { path: 'minions', component: MinionsComponent}, { path: 'minion/:id', component: MinionComponent}, { path: 'search/:term', component: BrowserComponent}, { path: '**', pathMatch: 'full', redirectTo: 'home' }, ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule {}

3. En el archivo browser.component.ts, vamos a seleccionar el parámetro que le pasaremos por URL (el /:term, definido en nuestras routes).

Para ello, importamos el módulo de Angular ActivatedRoute y lo declaramos en el constructor. Esa es la manera de trabajar con los parámetros, como ya habíamos hecho anteriormente cuando nos dedicamos a seleccionar el parámetro /:id.

La sintaxis ya la hemos visto: debemos añadir nuestro código en el ngOnInit() donde deberemos "suscribirnos" a los params. 

Como el parámetro que queremos seleccionar es el /:term (ese nombre viene de nuestro archivo de routes), vamos a utilizarlo como una propiedad de params.

Si haces un console.log() verás que, al escribir y enviar cualquier palabra en el buscador, ésta se imprime en la consola.

En estos momentos, el código del archivo browser.component.ts quedaría así:

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-browser', templateUrl: './browser.component.html', styleUrls: ['./browser.component.css'] }) export class BrowserComponent implements OnInit { minions: any[] = []; constructor(private activatedRoute: ActivatedRoute) { } ngOnInit() { this.activatedRoute.params.subscribe(params => { console.log(params.term); }) } }

4. El siguiente paso es hacer funcionar la redirección desde nuestro navbar (navbar.component.ts), de manera que cuando escribamos algún nombre en el buscador, nos redireccione a la página de dicho minion (si existe, claro...).

Para eso, importamos el módulo Router y lo definimos en el constructor. 

En nuestra función searchMinion() utilizamos el método navigate proporcionado por el módulo Router y le pasamos un array con dos elementos: nuestra URL y el term. 

Nuestro código nos quedaría así: 

import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-navbar', templateUrl: './navbar.component.html', styleUrls: ['./navbar.component.css'] }) export class NavbarComponent implements OnInit { constructor(private router: Router) { } ngOnInit() { } searchMinion(term: string) { // console.log(term); this.router.navigate(['/search', term]); } }

5. Lo siguiente es invocar al service desde nuestro componente BrowserComponent. No olvides inyectarlo en el constructor.

Ahora, nos creamos un array vacío de minionsEs un array porque cuando el usuario busque un término, éste puede coincidir con el nombre de varios minions. Por ejemplo, si alguien busca ''ion'', deberían aparecerle Lobeznion, Minion Presley y Minioncé.

A ese array le asignamos el valor del term haciendo uso de la función searchMinions de nuestro service. 

Hacemos un console.log para ver los resultados en la consola:

búsqueda ion

Nuestro archivo browser.component.ts nos quedaría así:

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { MinionsService } from '../../services/minions.service'; @Component({ selector: 'app-browser', templateUrl: './browser.component.html', styleUrls: ['./browser.component.css'] }) export class BrowserComponent implements OnInit { minions: any[] = []; constructor(private activatedRoute: ActivatedRoute, private minionsService: MinionsService) { } ngOnInit() { this.activatedRoute.params.subscribe(params => { // console.log(params.term); this.minions = this.minionsService.searchMinions(params.term); console.log(this.minions); }); } }

Configurando la parte visual del BrowserComponent

1. Vamos a utilizar prácticamente el mismo código del template de nuestro MinionsComponent, cambiando únicamente el H1 y mostrando la palabra que el usuario ha buscado. 

Para ello, declaramos una variable local (term)y le asignamos el valor del término dinámico de params. 

Para que se muestre en nuestro template, lo vinculamos mediante string interpolation. 

2. También vamos a añadir un mensajito útil si el usuario hace alguna búsqueda que no coincide con el nombre de algún minion. Para eso, añadimos un bloque de código con un *ngIf que sólo se verá si la length de nuestro minions array es igual a 0. Es decir, si no existe ninguna coincidencia con el nombre de alguno de nuestros minions. 

Para hacer referencia al término buscado, volvemos a hacerlo mediante string interpolation. 

Le añadimos también un pequeño efecto para que el mensaje entre en escena con suavidad. 

Nuestro código del archivo browser.component.html nos queda así:

<h2 class="mt-4">Resultados para: <small>{{ term }}</small></h2>
<hr>

<div class="row justify-content-center" *ngIf="minions.length === 0">
<div class="col-md-6">
<p class="alert alert-info animated fadeIn fast">
Ooops...! Ningún resultado para: <b>{{ term }}</b>
</p>
</div>
</div>

<div class="container">
<div class="card-columns mb-5">
<div class="card" *ngFor="let minion of minions; let i = index;">
<img [src]="minion.img" class="card-img-top" [alt]="minion.name">
<div class="card-body">
<h5 class="card-title"> {{ minion.name }} </h5>
<p class="card-text"> {{ minion.bio }} </p>
<p class="card-text"><small class="text-muted"> {{ minion.birth }} </small></p>
<button type="button" class="btn btn-outline-warning btn-block" (click)="seeMinion(i)">Ver más info</button>
</div>
</div>
</div>
</div>

Aplicando el DRY principle usando la lógica del componente padre y el componente hijo

La esencia de los componentes en Angular es su naturaleza reutilizable. Así, te habrás dado cuenta de que hemos utilizado varias veces el mismo código en varios sitios, yendo en contra del principio DRY (Don't Repeat Yourself). 

Vamos a encargarnos de arreglar eso.

Como verás, existe un elemento que hemos repetido varias veces a lo largo de nuestro código: las cards que resumen las características de un minion.

minionCard

Así que vamos a transformar esas tarjetas en un componente independiente

1. Creamos un nuevo componente dentro de la carpeta components llamado MinionCard.

ng g c minion-card --skipTests

Fíjate que Angular automáticamente convierte las palabras con guiones en camelCase:

CAMELcaSE

2. Ahora hagamos un par de apaños en el MinionsComponent.

El código que abarca la clase card es que el vamos a convertir en un componente. 

Para eso, quitamos el *ngFor (yo lo subo un nivel y lo dejo ahí comentado) y cortamos todo el código dentro de la clase card. 

Ahora nuestro archivo minions.component.html nos queda así: 

<h2 class="mt-4">Minions <small>a mansalva</small></h2> <hr> <div class="container"> <div class="card-columns mb-5"> <!-- *ngFor="let minion of minions; let i = index;" --> </div> </div>

3. Vamos al archivo minion-card.component.html y pegamos ahí nuestro código.

Ahora en nuestra URL http://localhost:4200/minions no deberíamos ver nada más que el navbar y y el título.

Nuestro archivo está así en estos momentos:

<div class="card" >
<img [src]="minion.img" class="card-img-top" [alt]="minion.name">
<div class="card-body">
<h5 class="card-title"> {{ minion.name }} </h5>
<p class="card-text"> {{ minion.bio }} </p>
<p class="card-text"><small class="text-muted"> {{ minion.birth }} </small></p>
<button type="button" class="btn btn-outline-warning btn-block" (click)="seeMinion(i)">Ver más info</button>
</div>
</div>

4. Vamos a editar el archivo minion-card.component.ts.

Nos creamos una propiedad llamada minion  de tipo any y la inicializamos como un objeto vacío, para que todo lo que tenemos escrito en la template no nos de errores. 

minion: any = {};

5. Volvemos al archivo minions.component.html e incluimos nuestro componente MinionCardComponent, ahora como un custom HTML element.

Si echamos un vistazo al navegador (en la página Minions), verás que la tarjeta se ve, pero sin imagen y sin información. 

Para arreglarlo, recuperamos nuestro *ngFor y lo aplicamos a nuestro elemento <app-minion-card>. Y con esto verás en el navegador que hemos multiplicado el error. ? ? 

Así nos queda nuestro código:

<h2 class="mt-4">Minions <small>a mansalva</small></h2> <hr> <div class="container"> <div class="card-columns mb-5"> <app-minion-card *ngFor="let minion of minions; let i = index;"></app-minion-card> </div> </div>

6. Para solucionarlo, vamos a utilizar la lógica de comunicación del parent component y el child component

Esto es una herramienta que nos proporciona Angular, la cual nos permite transmitir información de un componente padre a otro hijo y la inversa. ¿¿Pero qué diantres es un componente padre y un componente hijo?? ?

Muy sencillo: un componente adquiere la característica de "hijo" cuando lo usamos dentro de otro componente, que pasa a ser el padre. En el caso que nos ocupa, hemos usado el componente MinionCardComponent dentro del componente MinionsComponent (mira el código de arriba).

Pero no basta con poner los componentes uno dentro de otro, sino que hay que usar dos decoradores: el @Input() y el @Output().

7. Para poner esto en práctica, vamos al archivo minion-card.component.ts.

Desde ahí, importamos el decorador Input, que sirve para indicar a Angular que alguna propiedad que definamos en este archivo va a ser recibida desde fuera de este archivo. Únicamente añadimos la palabra @Input() delante de nuestra propiedad minion. 

El archivo nos quedaría así:

import { Component, OnInit, Input } from '@angular/core'; @Component({ selector: 'app-minion-card', templateUrl: './minion-card.component.html', styleUrls: ['./minion-card.component.css'] }) export class MinionCardComponent implements OnInit { @Input() minion: any = {}; constructor() { } ngOnInit() { } }

8. Ahora podemos utilizar esa propiedad en el child component (en el archivo minions.component.html):

<h2 class="mt-4">Minions <small>a mansalva</small></h2> <hr> <div class="container"> <div class="card-columns mb-5"> <app-minion-card [minion]="minion" *ngFor="let minion of minions; let i = index;"></app-minion-card> </div> </div>

? ¡No te confundas! Que la propiedad se la estamos pasando como un atributo ( [minion] ), al que le damos el valor de la variable local minion. El nombre del atributo y de la variable local no tienen por qué coincidir. 

Y ahora en nuestro navegador deberías poder ver la pantalla con todas las tarjetas de cada uno de nuestros minions. ?

Eso sí, el botón de "Ver más info" no funciona, porque al clicarlo, busca la función seeMinion() del archivo minion-card.component.html

9. Para arreglarlo, vamos a ese archivo y eliminamos el parámetro (i) (ahora verás por qué).

Después, vamos al archivo minion-card.component.ts y nos creamos una función con el mismo nombre. 

No le vamos a pasar un índice por parámetro, sino que el índice lo vamos a declarar como una propiedad cuya información vendrá del exterior. 

Por el momento, sólo le añadimos un console.log a la función, pasándole esa propiedad que nos acabamos de crear, llamada index.

El código nos queda así:

import { Component, OnInit, Input } from '@angular/core'; @Component({ selector: 'app-minion-card', templateUrl: './minion-card.component.html', styleUrls: ['./minion-card.component.css'] }) export class MinionCardComponent implements OnInit { @Input() minion: any = {}; @Input() index: number; constructor() { } ngOnInit() { } seeMinion() { console.log(this.index); } }

10. Volvemos al minions.component.html y hacemos una operación parecida a la de antes: le pasamos nuestra propiedad index como atributo al child component (el <app-minion-card>, remember). Como parámetro, le pasamos nuestra variable local, i. 

Ahora, al hacer clic en el botón "Ver más info", en la consola debería aparecer el número (índice) con el que se identifica dicho minion. Por ejemplo, si clicas en Kevin, te debería salir el 0. 

Tu código debería tener esta pinta:

<h2 class="mt-4">Minions <small>a mansalva</small></h2> <hr> <div class="container"> <div class="card-columns mb-5"> <app-minion-card [minion]="minion" [index]="i" *ngFor="let minion of minions; let i = index;"></app-minion-card> </div> </div>

11. Ahora, vamos a hacer que la redirección funcione, porque cuando clicamos en el susodicho botón, sólo nos imprime su número identificador. 

Vamos a utilizar la misma técnica de redirección que usamos en el MinionsComponent, usando el Router module

Para ello, nos copiamos el código de dentro de la función seeMinion:

this.router.navigate(['/minion', idx]);

 y lo pegamos en el archivo minion-card.component.ts, dentro de la función que tiene el mismo nombre (seeMinion, you know 😉 ? ).  Importamos también el módulo Router para poder usarlo, y lo inyectamos en el constructor. 

Fíjate que el segundo elemento del array tenemos que sustuirlo por nuestra propiedad index.

Y finalmente nuestro código quedaría así:

import { Component, OnInit, Input } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-minion-card', templateUrl: './minion-card.component.html', styleUrls: ['./minion-card.component.css'] }) export class MinionCardComponent implements OnInit { @Input() minion: any = {}; @Input() index: number; constructor(private router: Router) { } ngOnInit() { } seeMinion() { // console.log(this.index); this.router.navigate(['/minion', this.index]); } }

Aplicación de nuestro nuevo componente MinionCardComponent a otros componentes

1. Vamos a insertar el componente MinionCardComponent en el componente BrowserComponent siguiendo el ejemplo de la sección anterior. Así, en el archivo browser.component.html: 

  • borramos el código que abarca la clase card.
  • insertamos la etiqueta customizada de HTML <app-card-minion> con la misma lógica de programación dentro que hemos utilizado en el minions.component.html.

El código nos queda tal que así:

<h2 class="mt-4">Resultados para: <small>{{ term }}</small></h2> <hr> <div class="row justify-content-center" *ngIf="minions.length === 0"> <div class="col-md-6"> <p class="alert alert-info animated fadeIn fast"> Ooops...! Ningún resultado para: <b>{{ term }}</b> </p> </div> </div> <div class="container"> <div class="card-columns mb-5"> <app-minion-card [minion]="minion" [index]="i" *ngFor="let minion of minions; let i = index;"></app-minion-card> </div> </div>

Ahora, si haces una búsqueda un minion, te saldrá la búsqueda correcta, pero al ciclar en ''Ver más info'' te redireccionará a un minion incorrecto. Vamos a arreglar eso en la siguiente sección. ?‍?

Utilizando @Output para comunicarnos entre el child component y el parent component

Hasta ahora, sólo hemos enviado información de un parent component (de dos, para ser más exactos: el MinionsComponent y el BrowserComponent) a un child component (el MinionCardComponent). Pero ahora necesitamos enviar también información a la inversa: del hijo al padre

Esto funciona un pelín distinto a pasar información de padre a hijo. Lo que hacemos pasa pasar información de hijo a padre es emitir un evento. Para esto vamos a utilizar:

  • un @Output() decorator
  • un EventEmitter
  • la propiedad (evento) que queremos que el parent component reciba
  • el método .emit()

Verás que en el archivo minions.component.ts tenemos nuestra función seeMinion. Recuerda que esa función también la tenemos en el archivo minion-card.component.ts.

1. Vamos al archivo minion-card.component.ts  y comentamos el contenido de dicha función. Con esto, si clicamos el botón ''Ver más info'' no debería pasar nada, pues le acabamos de eliminar su funcionalidad.  Lo que queremos conseguir es acceder a la función seeMinion que está en el parent component, es decir, en el MinionsComponent. 

Para eso, importamos los módulos Output EventEmitter desde '@angular/core'.

Debajo de los Inputs declaramos un decorador Output() y nos creamos un evento que queremos que el componente padre reciba. Lo llamamos selectedMinion y determinamos que sea de tipo EventEmitter. Angular nos pide que indiquemos qué data type va a emitir nuestro evento (un string, un boolean, etc..), así que le indicamos que emitirá un number. 

Necesitamos inicializar el EventEmitter en el constructor, y eso se hace asignando a nuestra propiedad selectedMinion el valor de new EventEmitter(). 

Ahora, en nuestra función seeMinion, le decimos que emita nuestro evento y le pasamos por parámetro lo que queramos emitir. En este caso, el index. 

Nuestro código está así en estos momentos:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-minion-card', templateUrl: './minion-card.component.html', styleUrls: ['./minion-card.component.css'] }) export class MinionCardComponent implements OnInit { @Input() minion: any = {}; @Input() index: number; @Output() selectedMinion: EventEmitter<number>; constructor(private router: Router) { this.selectedMinion = new EventEmitter(); } ngOnInit() { } seeMinion() { // console.log(this.index); // this.router.navigate(['/minion', this.index]); this.selectedMinion.emit(this.index); } }

Con estos pasos, el parent component aún no tiene ni idea de que el componente hijo quiere hablar con él. ¡Vamos a solucionar eso!

pay attention

2. Vamos al archivo minions.component.html, donde vamos a incluir nuestro evento dentro de la etiqueta <app-minion-card>. 

Esto funciona igual que si añadiésemos un click event: 

  • utilizamos la sintaxis del event binding ()
  • le pasamos la función que queremos que ejecute cuando nuestro evento se dispare
  • a la función le pasamos un parámetro.

Pero en lugar de un evento definido por defecto en Angular (como el click event) le pasamos el nuestro propio (selectedMinion). Y en el caso del parámetro, le pasamos la keyword $event, porque esa es la manera de comunicarnos desde el componente hijo al padre:

<app-minion-card [minion]="minion" [index]="i" *ngFor="let minion of minions; let i = index;"

                 (selectedMinion)="seeMinion($event)">

</app-minion-card>

Puliendo el componente de búsqueda de minions

Si intentas buscar un minion y hacer click en ''Ver más info'', verás que no te redirige al minion correcto. Esto es porque nuestros minions no tienen un número identificativo propio, como se puede comprobar en nuestro service. Cada minion tiene un nombre propio, una bio... pero no un número propio que los identifique.

Vamos a arreglar eso.

Lo que queremos conseguir es generar un índice para cada minion que corresponda a su posición en el array original, o sea, el que se encuentra en el service. 

1. Para eso, vamos a modificar nuestro archivo minion-card.component.ts, haciendo la redirección como la hacíamos antes: desde el mismo componente, haciendo uso del módulo router. 

Así, nuestro event emitter queda solo con fines ilustrativos, porque no lo vamos a usar. Nuestra función, por tanto, se quedaría así: 

seeMinion() { // console.log(this.index); this.router.navigate(['/minion', this.index]); // this.selectedMinion.emit(this.index); }

2. Ahora vamos a hacer unos pequeños cambios en nuestro archivo minions.service.ts.

Primero, añadimos una propiedad opcional a nuestra interface llamada idx.

Segundo, ajustamos nuestra función para buscar minions (searchMinions) : 

  • recibirá por parámetro la búsqueda del usuario, al que llamamos term, que es de tipo string.
  • creamos un array vacío de tipo Minion, al que llamamos minionsArr.
  • como el usuario puede escribir en minúsculas y mayúsculas, pasamos su búsqueda a minúsculas. 
  • barremos nuestro array de minions para comprobar si alguno coincide con la búsqueda del usuario. Lo hacemos con un for loop para poder tener acceso al index (i).
    • creamos una variable local llamada minion, que será igual al array en la posición i.
    • creamos una variable local llamada name, que será el nombre de cada minion (el minion de nuestra variable local), y lo pasamos también a lowercase para poder compararlo rigurosamente con la búsqueda que haga el usuario (es decir, con el term). 
    • si se encuentra una coincidencia, añadimos ese minion a nuestro array y a la propiedad idx le damos el valor de la posición i.
  • fuera del loop, hacemos un return de nuestro array. 

Esta es la manera (una de tantas) con la indicamos que la posición siempre coincidirá con la posición de cada minion como viene definida en nuestro array de minions. Por ejemplo, como Kevin es el primer elemento del array, su propiedad idx siempre será 0.

Nuestro código nos quedaría así:

import { Injectable } from '@angular/core'; @Injectable() export class MinionsService { private minions: Minion[] = [ { name: "Kevin", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/kevin.jpg", birth: "1951", side:"de los buenos" }, { name: "Josua", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/Josua.jpg", birth: "1672", side:"malvado" }, { name: "Dave", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/dave.jpg", birth: "1723", side: "de los buenos" }, { name: "Mudito", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/mudito.jpeg", birth: "1379", side:"de los buenos" }, { name: "Llongueras", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/llongueras.jpg", birth: "1687", side: "malvado" }, { name: "Minioncé", bio: "Le va el cante, dar la nota, ama los karaokes, es el rey y reina de la fiesta. Invítalo a tu fiesta o te arrepentirás.", img: "assets/img/minionce.jpg", birth: "1976", side: "de los buenos" }, { name: "Lobeznion", bio: "No lo enfades, este bichillo tiene muy malas pulgas...aunque sólo mide medio metro y ¡no puede ser más gracioso!", img: "assets/img/lobeznion.jpg", birth: "2017", side: "malvado" }, { name: "Minion Presley", bio: "Aquí debería haber una biografía de la vida de este minion, pero son gente muy misteriosa, así que tendrás que imaginártela...", img: "assets/img/minion-presley.jpg", birth: "2017", side: "malvado" } ]; constructor() { console.log('minions service listo para usar, oiga!'); } getMinions() { return this.minions; } getMinion(idx: string) { return this.minions[idx]; } // searchMinions = (term: string): Minion[] => { // return this.minions.filter(minion => // minion.name.toLowerCase().includes(term.toLowerCase())); // } searchMinions(term: string): Minion[] { let minionsArr: Minion[] = []; term = term.toLowerCase(); for(let i = 0; i < this.minions.length; i++) { let minion = this.minions[i]; let name = minion.name.toLowerCase(); if(name.indexOf(term) >= 0) { minion.idx = i; minionsArr.push(minion); } } return minionsArr; } } export interface Minion { name: string; bio: string; img: string; birth: string; side: string; idx?: number; }

Ahora, si buscas cualquier minion, verás que en la consola te sale su nueva propiedad: idx. 

3. Vamos a pasarle esa idx al <app-minion-card> que tenemos dentro del BrowserComponent.

Para ello, borramos la segunda parte del *ngFor porque ya no la necesitamos, ya que ahora podemos acceder a la propiedad idx desde la variable local minion, así que se la pasamos al atributo [index] 

Nuestro código nos quedaría, finalmente, así: 

<h2 class="mt-4">Resultados para: <small>{{ term }}</small></h2> <hr> <div class="row justify-content-center" *ngIf="minions.length === 0"> <div class="col-md-6"> <p class="alert alert-info animated fadeIn fast"> Ooops...! Ningún resultado para: <b>{{ term }}</b> </p> </div> </div> <div class="container"> <div class="card-columns mb-5"> <app-minion-card [minion]="minion" [index]="minion.idx" *ngFor="let minion of minions;"> </app-minion-card> </div> </div>

¡¡Y YA ESTÁ!! ?‍♀️ ? Nuestra app está, por fin, completa. ? ?‍?

Espero que hayas disfrutado este proyecto tanto como yo y que hayas aprendido algo nuevo. 

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

libro 1 angular
libro 2 angular

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos ?.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win ?.

THE END!

¡Y con esto terminamos Minioland, una sencilla app hecha con Angular​​! Espero que hayas aprendido algo nuevo ?.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Y si crees que este post puede serle útil a alguien, ¡compártelo!

Otros artículos que pueden interesarte

Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Días del 160 al 203 – ¡Primer objetivo conseguido!
“A veces podemos pasarnos años sin vivir en absoluto, y de pronto toda nuestra vida se concentra en un solo[...]
Claves para entender Angular. Qué es y cómo se utiliza
Angular es un framework creado por Google que nos permite construir Single Page Applications (SPA, por sus siglas en inglés).Frameworks¿Pero qué es[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

2 comentarios en «Construye Minioland: Tu primera aplicación con Angular | Parte #3»

  1. Al final, un excelente mini curso, he aprendido mucho, la parte final, a mi parecer, fue subir demasiado el nivel. Pero todo muy bien, gracias:))

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Esta web utiliza cookies para asegurar que se da la mejor experiencia al usuario. Si continúas utilizando este sitio se asume que estás de acuerdo. más información

Los ajustes de cookies en esta web están configurados para «permitir las cookies» y ofrecerte la mejor experiencia de navegación posible. Si sigues usando esta web sin cambiar tus ajustes de cookies o haces clic en «Aceptar», estarás dando tu consentimiento a esto.

Cerrar