construye minioland

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

脷ltima actualizaci贸n:

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

Aprender Angular a trav茅s de tutoriales web y cursos online est谩 genial, pero si necesitas un apoyo en forma de libro, puede que 茅stos te sirvan de ayuda:

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 馃槍.

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

D铆as del 353 al 386
Objetivos versus realidad Y nuevamente, lleg贸 otro d铆a clave. Lleg贸鈥 pas贸. El pasado 4 de marzo este Reto Computer Geek[...]
Dates en JavaScript: Gu铆a completa
Introducci贸nLos dates son un tipo de objetos de JavaScript (JS), muy 煤tiles para trabajar con fechas, como podr铆a ser la fecha de[...]
D铆as del 2 al 4
"Si buscas resultados distintos no hagas siempre lo mismo" - Albert Einstein Estos d铆as estoy aprendiendo a hacer loops en[...]

Deja un comentario

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

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si contin煤as utilizando este sitio asumiremos que est谩s de acuerdo. m谩s informaci贸n

Los ajustes de cookies de esta web est谩n configurados para "permitir cookies" y as铆 ofrecerte la mejor experiencia de navegaci贸n posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estar谩s dando tu consentimiento a esto.

Cerrar