Directivas, servicios e inyección de dependencias
En este capítulo vamos a explorar más directivas Angular. Empezaremos por aquellas que se llaman de estructura. Que nos permiten pintar listas o contenidos condicionales. Luego exploraremos el concepto de inyección de dependencia en Angular. Veremos como gestionar la lista de nuestros candidatos desde un servicio.
Outline
- Setup del proyecto
- Directivas de estructura
- Directivas de atributo
- Inyección de dependencias
- Referencias
Setup del proyecto
Para esta jornada de curso empezaremos con el siguiente repositorio. Clonarlo y hacer un install
:
git clone git@github.com:alexseik/angular-101-day-2-init.git
mv angular-101-day-2-init angular-101-day-2
cd angular-101-day-2
npm install
Directivas de estructura
Las directivas estructurales son responsables del diseño HTML. Dan forma o remodelan la estructura del DOM, típicamente añadiendo, eliminando y manipulando los elementos anfitriones a los que están adjuntas.
Estas directivas se pueden usar tanto en elementos del DOM como en componentes creados en Angular. Una regla básica es que sólo se puede utilizar una directiva de estructura por elemento.
Renderizado condicional - *ngIf
Angular utiliza la directiva *ngIf
para añadir o quitar un elemento y sus descendientes en función del valor que tenga la expresión que se le pase.
Como ejemplo, vamos a suponer que queremos que nuestro componente CandidateComponent
muestre una etiqueta indicando el tipo de desarrollador qué es: Junior, Intermedio o Senior en función de los años de experiencia.
Modificamos primero el fichero de estilos candidate.component.scss
:
.candidate-card {
... .label {
border: 1px solid black;
padding: 5px 10px;
border-radius: 20px;
background-color: white;
&.junior {
color: #8cc0f7;
}
&.midlevel {
color: black;
}
&.senior {
color: #f74131;
}
}
}
y luego el template candidate.component.html
:
<div [ngClass]="cssClasses" [style.color]="colorStyle">
<h2>{{ name }}</h2>
<h3>{{ surname }}</h3>
<p><strong>Age:</strong>{{ candidate.age }}</p>
<p><strong>Position:</strong>{{ candidate.position }}</p>
<p><strong>Experience:</strong>{{ candidate.experience }}</p>
<div *ngIf="candidate.experience < 3">
<span class="label junior">JUNIOR</span>
</div>
<div *ngIf="candidate.experience <= 5 && candidate.experience > 3">
<span class="label intermediate">INTERMEDIO</span>
</div>
<div *ngIf="candidate.experience > 5">
<span class="label senior">SENIOR</span>
</div>
<p><strong>Skills:</strong>{{ candidate.skills.join(", ") }}</p>
<p><button data-testid="candidate-edit" (click)="doEdit()">Edit</button></p>
</div>
Como vemos, la directiva ngIf
acepta una expresión: *ngIf="candidate.experience < 3
. En este caso mostrará el elemento cuando la expresión sea thruthy. Si el valor de la expresión fuera null
o undefined
tampoco se pintaría.
Otra directiva builtin proporcionada por Angular es NgSwitch
.
NgTemplate y NgContainer
En Angular, cuando una directiva es de estructura, se prefija con un asterisco. Esto lo interpreta Angular como un atajo para escribir la directiva de estructura dentro de un <ng-template>
. Con <ng-template>
se puede definir un template que no sea renderizado automáticamente por angular, si no que haya que indicar explícitamente que se quiere pintar el template. <ng-template>
acepta ser referenciado por variables. Un uso típico de <ng-template>
es una cláusula else
al usar ngIf
. Como ejemplo, vamos a mostrar la lista de skills si son menos de 4. En caso de que sean 4 o más skills vamos a mostrar un mensaje que es clickable. Cuando se haga click en él, se desplegará otra línea con todas las skills.
<ng-template>
Primero modificamos los estilos de CandidateComponent
modificando el fichero candidate.component.scss
:
.candidate-card {
// añadir la clase skills
.skills {
display: flex;
flex-direction: column;
&__name {
display: flex;
flex-direction: row;
}
&__more {
cursor: pointer;
}
}
}
Después modificamos su template, cambiando el archivo candidate.component.html
:
<div [ngClass]="cssClasses" [style.color]="colorStyle">
<h2>{{ name }}</h2>
<h3>{{ surname }}</h3>
<p><strong>Age:</strong>{{ candidate.age }}</p>
<p><strong>Position:</strong>{{ candidate.position }}</p>
<p><strong>Experience:</strong>{{ candidate.experience }}</p>
<!-- <div *ngIf="candidate.experience < 3">
<span class="label junior">JUNIOR</span>
</div>
<div *ngIf="candidate.experience <= 5 && candidate.experience > 3">
<span class="label intermediate">INTERMEDIO</span>
</div>
<div *ngIf="candidate.experience > 5">
<span class="label senior">SENIOR</span>
</div> -->
<div [ngSwitch]="experienceLabel">
<div *ngSwitchCase="'junior'"><span class="label junior">JUNIOR</span></div>
<div *ngSwitchCase="'midlevel'">
<span class="label intermediate">INTERMEDIO</span>
</div>
<div *ngSwitchCase="'senior'"><span class="label senior">SENIOR</span></div>
</div>
<p><strong>Skills:</strong> {{ candidate.skills.join(", ") }}</p>
<div class="skills">
<h3>Skills</h3>
<div
*ngIf="candidate.skills.length < 4; else moreSkills"
class="skills__name"
>
{{ candidate.skills.join(", ") }}
</div>
<ng-template #moreSkills>
<div
class="skills__more"
*ngIf="!showMoreSkills; else allSkills"
(click)="toggleMoreSkills()"
>
más de 3 skills.
</div>
<ng-template #allSkills>
<div>
{{ candidate.skills.join(", ") }}
<button (click)="toggleMoreSkills()">X</button>
</div>
</ng-template>
</ng-template>
</div>
<p><button data-testid="candidate-edit" (click)="doEdit()">Edit</button></p>
</div>
Y por último actualizamos el controlador del componente, el fichero candidate.component.ts
:
import {
AfterContentChecked,
AfterContentInit,
AfterViewChecked,
Component,
DoCheck,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
ViewEncapsulation,
} from "@angular/core";
import { Candidate } from "../../models/candidate.model";
@Component({
selector: "app-candidate",
templateUrl: "./candidate.component.html",
styleUrls: ["./candidate.component.scss"],
encapsulation: ViewEncapsulation.None,
})
export class CandidateComponent
implements
OnChanges,
OnInit,
DoCheck,
AfterContentInit,
AfterContentChecked,
AfterViewChecked,
OnDestroy
{
@Input()
set candidate(candidate: Candidate) {
this._candidate = candidate;
const index = candidate.name.indexOf(" ");
if (index < 0) {
throw new Error("The name does not contain a space");
}
this.name = candidate.name.slice(0, index + 1);
this.surname = candidate.name.slice(index) + 1;
this.cssClasses = {
animate: false,
"candidate-card": true,
senior: candidate.experience < 3,
junior: candidate.experience > 5,
};
this.colorStyle = candidate.experience <= 5 ? "black" : "white";
}
get candidate() {
return this._candidate;
}
@Output() select = new EventEmitter<Candidate>();
public name: string = "";
public surname: string = "";
public cssClasses: any = {};
public colorStyle: string = "";
showMoreSkills = false;
private _candidate!: Candidate;
ngOnChanges(changes: SimpleChanges) {
let log: string = "";
for (const propName in changes) {
const changedProp = changes[propName];
console.log({ changedProp });
const to = JSON.stringify(changedProp.currentValue);
if (changedProp.isFirstChange()) {
log = `Initial value of ${propName} set to ${to}`;
} else {
const from = JSON.stringify(changedProp.previousValue);
log = `${propName} changed from ${from} to ${to}`;
}
}
console.log("OnChanges", log);
}
ngOnInit(): void {
console.log("OnInit", { candidate: this.candidate });
}
ngDoCheck(): void {
console.log("DoCheck");
}
ngAfterContentInit(): void {
console.log("AfterContentInit");
}
ngAfterContentChecked(): void {
console.log("AfterContentChecked");
}
ngAfterViewChecked(): void {
console.log("AfterViewChecked");
}
ngOnDestroy(): void {
console.log("OnDestroy");
}
doEdit = () => {
this.select.emit(this.candidate);
this.cssClasses.animate = true;
setTimeout(() => {
this.cssClasses.animate = false;
}, 750);
};
get experienceLabel() {
return this.candidate.experience < 3
? "junior"
: this.candidate.experience <= 5
? "midlevel"
: "senior";
}
toggleMoreSkills() {
this.showMoreSkills = !this.showMoreSkills;
}
}
Hemos hecho uso de la cláusula else
dentro de ngIf
, referenciando un template generado con ng-template
que sólo va a ser pintado una vez que la condición pasada a *ngIf
sea false. #moreSkills
y #allSkills
son variables de template que hacen referencia a los ng-template
.
ng-container
Angular también provee de la directiva ng-container
, que es útil cuando se quieren usar directivas de estructura sin generar un elemento (un <div></div>
por ejemplo) del DOM. En nuestro candidato, cuando pintamos la etiqueta JUNIOR, INTERMEDIO o SENIOR hemos utilizado un tag <div></div>
para poder usar la directiva *ngIf
. Si quisiéramos ahorrarnos el <div>
, podríamos sustituirlo por <ng-container>
. Modificamos el fichero candidate.component.html
:
<div [ngClass]="cssClasses" [style.color]="colorStyle">
<h2>{{ name }}</h2>
<h3>{{ surname }}</h3>
<p><strong>Age:</strong>{{ candidate.age }}</p>
<p><strong>Position:</strong>{{ candidate.position }}</p>
<p><strong>Experience:</strong>{{ candidate.experience }}</p>
<ng-container [ngSwitch]="experienceLabel">
<div *ngSwitchCase="'junior'"><span class="label junior">JUNIOR</span></div>
<div *ngSwitchCase="'midlevel'">
<span class="label intermediate">INTERMEDIO</span>
</div>
<div *ngSwitchCase="'senior'"><span class="label senior">SENIOR</span></div>
</ng-container>
<p><strong>Skills:</strong> {{ candidate.skills.join(", ") }}</p>
<div class="skills">
<h3>Skills</h3>
<div
*ngIf="candidate.skills.length < 4; else moreSkills"
class="skills__name"
>
{{ candidate.skills.join(", ") }}
</div>
<ng-template #moreSkills>
<div
class="skills__more"
*ngIf="!showMoreSkills; else allSkills"
(click)="toggleMoreSkills()"
>
más de 3 skills.
</div>
<ng-template #allSkills>
<div>
{{ candidate.skills.join(", ") }}
<button (click)="toggleMoreSkills()">X</button>
</div>
</ng-template>
</ng-template>
</div>
<p><button data-testid="candidate-edit" (click)="doEdit()">Edit</button></p>
</div>
Si inspeccionamos en el navegador veremos que la estructura del DOM no tiene los <div>
s,
simplemente se pintan los <span>
s:
.
Listas de componentes - *ngFor
Actualmente el componente AppComponent
pinta los componentes CandidateComponent
uno a uno en su html. Angular provee la directiva ngFor
para pintar elementos consecutivamente.
Modificamos el fichero app.component.html
:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input
type="number"
id="candidate-skill"
(input)="changeInput($event)"
[value]="candidateName"
/>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>Número de candidatos <span>{{ candidatesLength }}</span></h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<app-candidate
*ngFor="let candidate of candidates"
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate
></app-candidate-list>
</div>
La directiva *ngFor
se aplica a cualquier elemento o component Angular. La cadena let candidate of candidates
instruye a Angular a hacer lo siguiente:
- Almacenar cada ítem en el array de items en la variable local de bucle
candidate
. - Hacer disponible cada ítem para el HTML templado en cada iteración.
- Traducir
let candidate of candidates
en un<ng-template>
alrededor del elemento anfitrión. - Repetir el
<ng-template>
para cada ítem en la lista.
Como buena práctica, esta directiva debe ser utilizada junto con trackBy
. trackBy
permite reducer el número de pintados de un elemento de la lista si el identificador que se le dota al elemento no cambia.
Modificamos nuestro app.component.html
:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input
type="number"
id="candidate-skill"
(input)="changeInput($event)"
[value]="candidateName"
/>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>Número de candidatos <span>{{ candidatesLength }}</span></h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<app-candidate
*ngFor="let candidate of candidates; trackBy: trackById"
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate
></app-candidate-list>
</div>
y añadimos la función trackById
en app.component.ts
:
import { Component, OnInit, ViewChild, ViewChildren } from "@angular/core";
import { Candidate } from "./models/candidate.model";
import { CandidateComponent } from "./components/candidate/candidate.component";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
title = "candidates-frontend";
@ViewChildren(CandidateComponent)
private candidateComps!: CandidateComponent[];
public candidatesLength: number = 0;
candidates: Candidate[] = [
{
id: 1,
name: "José Pérez",
age: 25,
position: "Desarrollador Junior",
experience: 1,
skills: ["Java", "SQL"],
},
{
id: 2,
name: "Paco López",
age: 40,
position: "Desarrollador Senior",
experience: 15,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
{
id: 3,
name: "Mireia García",
age: 30,
position: "Desarrolladora Intermedia",
experience: 4,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
];
selectedCandidate: Candidate | null = null;
get candidateName(): string {
return this.selectedCandidate ? this.selectedCandidate.name : "";
}
get candidateExperience(): number {
return this.selectedCandidate ? this.selectedCandidate.experience : 0;
}
set candidateExperience(experience: number) {
if (this.selectedCandidate) {
const candidateIndex = this.candidates.findIndex(
(c) => c.name === this.selectedCandidate?.name
);
if (candidateIndex > -1) {
this.candidates[candidateIndex] = Object.assign(
{},
this.candidates[candidateIndex],
{ experience }
);
}
}
}
ngOnInit() {
setTimeout(() => {
this.candidatesLength =
!!this.candidateComps && "length" in this.candidateComps
? this.candidateComps.length
: 0;
});
}
changeInput(event: Event) {
this.candidateExperience = parseInt((event.target as any).value);
}
selectCandidate(candidate: Candidate) {
this.selectedCandidate = candidate;
}
// Añadimos la función trackById
trackById(index: number, item: Candidate) {
return item.id;
}
}
Además, con *ngFor
podemos recuperar el índice del elemento pintado. Esto puede ser útil para muchos casos de uso. Como ejemplo queremos mostrar el número del candidato:
Modificamos app.component.html
:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input
type="number"
id="candidate-skill"
(input)="changeInput($event)"
[value]="candidateName"
/>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>Número de candidatos <span>{{ candidatesLength }}</span></h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<div
class="candidate-box"
*ngFor="let candidate of candidates; let i = index; trackBy: trackById"
>
<span>{{ i }}</span>
<app-candidate
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
</div>
</app-candidate-list>
</div>
y damos un poco de estilo a app.component.scss
:
.candidate-box {
margin: 0 10px;
width: 300px;
display: flex;
flex-direction: column;
gap: 10px;
}
-
Ejercicio: Crear un botón que permita ordenar la lista por Experiencia. El botón debe mostrar si la lista está ordenada por experiencia ASC o DESC mostrando como texto
experiencia (asc)
oexperiencia (desc)
o soloexperiencia
. -
Ejercicio: Crear un nuevo botón para ordernar por nombre. El botón tiene tres estados
nombre(asc)
,nombre(desc)
onombre
. Si el orden nombre está activo, el orden anterior sobre experiencia debe estar vacío.
Directivas de atributo
Ya hemos visto algunas directivas de atributo el día anterior, como son ngStyle
o ngClass
. Ahora vamos a ver una que es muy útil y nos va a permitir refactorizar nuestro código y hacerlo más sencillo.
ngModel
ngModel
sirve para pintar y tener actualizado un valor en el template en función del valor en el fichero typescript y al revés.
Para poder usar esta directiva hace falta importar el módulo FormsModule
de Angular. @angular/forms
ya lo deberíamos tener declarado en nuestro package.json
como dependencia. Si no haría falta ejecutar
npm install --save @angular/forms
Además tenemos que importarlo en nuestro módulo principal de la aplicación. Es decir, debemos modificar el fichero app.module.ts
:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { CandidateComponent } from "./components/candidate/candidate.component";
import { FormsModule } from "@angular/forms";
@NgModule({
declarations: [AppComponent, CandidateComponent],
imports: [
BrowserModule,
FormsModule /* contiene ngModel */,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Como ejemplo, vamos a modificar nuestro AppComponent
. En el dia 1 hicimos que el input
modificara un valor con JS plano. En el template app.component.html
teníamos:
<input
type="number"
id="candidate-skill"
(input)="changeInput($event)"
[value]="candidateName"
/>
Usamos (input)
para reaccionar al evento input
y [value]
para asignar el valor de candidateName
al elemento <input>
. Nuestro app.component.ts
se quedaba:
import { Component } from '@angular/core';
import { Candidate } from './models/candidate.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'candidates-frontend';
selectedCandidate: Candidate | null = null;
get candidateName(): string {
return this.selectedCandidate ? this.selectedCandidate.name : '';
}
get candidateExperience(): number {
return this.selectedCandidate ? this.selectedCandidate.experience : 0;
}
set candidateExperience(experience: number) {
if (this.selectedCandidate) {
const candidateIndex = this.candidates.findIndex(
(c) => c.name === this.selectedCandidate?.name
);
if (candidateIndex > -1) {
this.candidates[candidateIndex] = Object.assign(
{},
this.candidates[candidateIndex],
{ experience }
);
}
}
}
...
candidates: Candidate[] = [ ... ];
changeInput(event: Event) {
this.candidateExperience = parseInt((event.target as any).value);
}
selectCandidate(candidate: Candidate) {
this.selectedCandidate = candidate;
}
}
Utilizando [(ngModel)]
no hace falta tener en cuenta el evento input
. La directiva ya lo tiene en cuenta por nosotros y asigna el valor automáticamente. Modificamos app.component.html
por:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input
type="number"
id="candidate-skill"
[(ngModel)]="candidateExperience"
/>
<button (click)="unselect()">X</button>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>
Número de candidatos <span>{{ candidatesLength }}</span>
</h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<div
class="candidate-box"
*ngFor="let candidate of candidates; let i = index; trackBy: trackById"
>
<span>{{ i }}</span>
<app-candidate
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
</div>
</app-candidate-list>
</div>
v>
</div>
Creamos unos estilos nuevos en app.component.scss
:
.candidate-input {
display: flex;
.skill {
margin-left: 5rem;
display: flex;
align-items: center;
gap: 5px;
}
}
.candidate-list {
display: flex;
}
.candidate-box {
margin: 0 10px;
width: 300px;
display: flex;
flex-direction: column;
gap: 10px;
.number {
align-self: flex-start;
border: 1px solid black;
border-radius: 50px;
padding: 5px 10px;
}
}
y modificamos app.component.ts
:
import { Component } from "@angular/core";
import { Candidate } from "./models/candidate.model";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent {
title = "candidates-frontend";
selectedCandidate: Candidate | null = null;
get candidateName(): string {
return this.selectedCandidate ? this.selectedCandidate.name : "";
}
get candidateExperience(): number {
return this.selectedCandidate ? this.selectedCandidate.experience : 0;
}
set candidateExperience(experience: number) {
if (this.selectedCandidate) {
const candidateIndex = this.candidates.findIndex(
(c) => c.name === this.selectedCandidate?.name
);
if (candidateIndex > -1) {
this.candidates[candidateIndex] = Object.assign(
{},
this.candidates[candidateIndex],
{ experience }
);
}
}
}
trackById(index: number, item: Candidate) {
return item.id;
}
candidates: Candidate[] = [...];
selectCandidate(candidate: Candidate) {
this.selectedCandidate = candidate;
}
unselect() {
this.selectedCandidate = null;
}
}
Hemos eliminado la función changeInput
. [(ngModel)]
tiene en cuenta el tipo de dato (number) y actualiza correctamente candidateExperience
. Aún necesitamos definir candidateExperience
con un getter y un setter. ¿Por qué?
- Ejercicio: Arreglar los test si fallan. Tip: el módulo FormsModule no está incluido en el módulo que se crea en e
app.component.spec.ts
Construir una directiva de atributo
Las directivas de Angular ofrecen una excelente manera de encapsular comportamientos reutilizables: las directivas pueden aplicar atributos, clases CSS y listeners de eventos a un elemento.
Como ejemplo vamos a construir una directiva que aplicada cambie el border de un elemento para resaltarlo. Lo aplicaremos a uno de nuestros candidatos.
Creamos la directiva ayudándonos de Angular CLI:
ng generate directive directives/highlight
Se habrán creado los ficheros highlight.directive.ts
y highlight.directive.spec.ts
y se habrá modificado el fichero app.module.ts
para incluir la nueva directiva en nuestra aplicación.
Para poder tener acceso al DOM en angular, podemos importar e inyectar ElementRef
en nuestra directiva. Con eso podremos modificar los estilos del elemento sobre el que se aplica la directiva. Modificamos el fichero highlight.directive.ts
:
import { Directive, ElementRef } from "@angular/core";
@Directive({
selector: "[appHighlight]",
})
export class HighlightDirective {
constructor(private el: ElementRef) {
// console.log('elementStyle', this.el.nativeElement.style);
this.el.nativeElement.style.borderColor = "blue";
this.el.nativeElement.style.borderWidth = "5px";
this.el.nativeElement.style.borderStyle = "solid";
this.el.nativeElement.style.borderRadius = "5px";
}
}
Luego aplicamos la directiva a los elementos impares de la lista de candidatos. Modificamos app.component.html
:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input
type="number"
id="candidate-skill"
[(ngModel)]="candidateExperience"
/>
<button (click)="unselect()">X</button>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>Número de candidatos <span>{{ candidatesLength }}</span></h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<div
class="candidate-box"
*ngFor="let candidate of candidates; let i = index; trackBy: trackById"
>
<span>{{ i }}</span>
<app-candidate
*ngIf="i % 2 === 0; else odd"
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
<ng-template #odd>
<app-candidate
appHighlight
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
</ng-template>
</div>
</app-candidate-list>
</div>
Con la modificación que hemos hecho, hemos añadido un borde azul con cierto ancho.
Con una directiva también podemos añadir funcionales en respuesta a eventos del usuario. Podemos añadir el comportamiento anterior, pero solo en el caso de que el usuario pase el ratón por encima. Modificamos el fichero highlight.directive.ts
:
import { Directive, ElementRef, HostListener } from "@angular/core";
@Directive({
selector: "[appHighlight]",
})
export class HighlightDirective {
constructor(private el: ElementRef) {}
@HostListener("mouseenter") onMouseEnter() {
this.el.nativeElement.style.borderColor = "blue";
this.el.nativeElement.style.borderWidth = "5px";
this.el.nativeElement.style.borderStyle = "solid";
this.el.nativeElement.style.borderRadius = "5px";
this.el.nativeElement.style.cursor = "pointer";
}
@HostListener("mouseleave") onMouseLeave() {
this.el.nativeElement.style.borderColor = "";
this.el.nativeElement.style.borderWidth = "";
this.el.nativeElement.style.borderStyle = "";
this.el.nativeElement.style.borderRadius = "";
this.el.nativeElement.style.cursor = "";
}
}
y modificamos app.component.html
:
<div class="main">
<div class="candidate-input">
<h2>{{ candidateName }}</h2>
<div class="skill" *ngIf="selectedCandidate">
<label for="candidate-skill">Skill</label>
<input
type="number"
id="candidate-skill"
[(ngModel)]="candidateExperience"
/>
<button (click)="unselect()">X</button>
</div>
</div>
<div class="candidate-list">
<div
class="candidate-box"
*ngFor="let candidate of candidates; let i = index; trackBy: trackById"
>
<span class="number">{{ i }}</span>
<app-candidate
appHighlight
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
</div>
</div>
</div>
Las directivas también se pueden testear. Modificar el fichero highlight.directive.spec.ts
:
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { HighlightDirective } from "./highlight.directive";
import { Component } from "@angular/core";
import { findEl } from "../utils/testing";
@Component({
template: `<div appHighlight data-testid="host"></div>`,
})
class HostComponent {}
describe("HighlightDirective", () => {
let fixture: ComponentFixture<HostComponent>;
let div: HTMLDivElement;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [HighlightDirective, HostComponent],
});
fixture = TestBed.createComponent(HostComponent);
fixture.detectChanges();
div = findEl(fixture, "host").nativeElement;
});
it("should set the border initially", () => {
expect(div.style["borderColor"]).toBe("blue");
});
});
El mecanismos es un poco más artificioso. Tenemos que crear un componente HostComponent
que actúa como placeholder para poder usar nuestra directiva dentro del TestBed
que nos proporciona Angular.
Como a los componentes, a las directivas también se pueden pasar valores con @Input
. Vamos a hacer que el color del borde que se aplica sea dinámico y sea el padre el que decida el color. En caso de que no se especifique ninguno, será azul como el que hemos puesto antes. Modificamos highlight.directive.ts
:
import { Directive, ElementRef, HostListener, Input } from "@angular/core";
@Directive({
selector: "[appHighlight]",
})
export class HighlightDirective {
private color: string = "blue";
@Input()
set appHighlight(valor: string) {
if (valor) {
this.color = valor;
}
}
constructor(private el: ElementRef) {}
@HostListener("mouseenter") onMouseEnter() {
console.log("color", this.appHighlight);
this.el.nativeElement.style.borderColor = this.color;
this.el.nativeElement.style.borderWidth = "5px";
this.el.nativeElement.style.borderStyle = "solid";
this.el.nativeElement.style.borderRadius = "5px";
this.el.nativeElement.style.cursor = "pointer";
}
@HostListener("mouseleave") onMouseLeave() {
this.el.nativeElement.style.borderColor = "";
this.el.nativeElement.style.borderWidth = "";
this.el.nativeElement.style.borderStyle = "";
this.el.nativeElement.style.borderRadius = "";
this.el.nativeElement.style.cursor = "";
}
}
Para que la directiva acepte un valor por defecto sobre un @Input
del mismo nombre que la directiva, necesitamos utilizar un setter. En este caso mantenemos una variable privada con el color por defecto que solo se cambia si se le pasa un valor. La directiva la podemos usar de las siguientes maneras:
<app-candidate
appHighlight
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
<app-candidate
[appHighlight]="'red'"
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
En el primer caso pintaremos el borde azul, y en el segundo en rojo.
- Ejercicio: Crear una directiva
scale
, que aplicada a un elemento aumente contransform: scale(1.1)
cuando el usuario pasa el ratón por encima. - Ejercicio: Modifica el test de
highlightDirective
para comprobar que el input, modifica el estilo aplicado.
Pipes
Utiliza pipes para transformar cadenas, cantidades monetarias, fechas y otros datos para su visualización. Los pipes son funciones simples que se utilizan en expresiones de template para aceptar un valor de entrada y devolver un valor transformado.
Angular tiene algunas pipes implementadas por defecto:
- DatePipe: Formatea un valor de fecha según las reglas de la localidad.
- UpperCasePipe: Transforma el texto a mayúsculas.
- LowerCasePipe: Transforma el texto a minúsculas.
- CurrencyPipe: Transforma un número a una cadena de moneda, formateada según las reglas de la localidad.
- DecimalPipe: Transforma un número en una cadena con un punto decimal, formateada según las reglas de la localidad.
- PercentPipe: Transforma un número a una cadena de porcentaje, formateada según las reglas de la localidad.
Vamos a utilizar 2 como ejempo: UpperCasePipe para transformar las skills a mayúsculas y CurrencyPipe en un nuevo campo donde indicaremos el mínimo que quiere cobrar la/el candidata/o en caso de ser contratada/a.
Ambos Pipes están definidos en el paquete @angular/common
por lo que no hace falta instalar ninguna dependencia.
Modificamos el fichero candidate.model.ts
, para tener en cuenta la nueva propiedad:
export interface Candidate {
id: number;
name: string;
age: number;
position: string;
experience: number; // years
salary: number;
skills: string[];
}
Actualizamos nuestra lista de candidatos en app.component.ts
:
...
candidates: Candidate[] = [
{
id: 1,
name: 'José Pérez',
age: 25,
position: 'Desarrollador Junior',
experience: 1,
salary: 20000,
skills: ['Java', 'SQL'],
},
{
id: 2,
name: 'Paco López',
age: 40,
position: 'Desarrollador Senior',
experience: 15,
salary: 40000,
skills: ['Java', 'SQL', 'Oracle', 'PL/SQL', 'Cobol', 'C++'],
},
{
id: 3,
name: 'Mireia García',
age: 30,
position: 'Desarrolladora Intermedia',
experience: 4,
salary: 30000,
skills: ['Java', 'SQL', 'Oracle', 'PL/SQL', 'Cobol', 'C++'],
},
];
...
Modificamos el fichero candidate.component.html
:
<div [class]="candidateClasses" [style.color]="getColor()">
<h2>{{ candidate.name }}</h2>
<div>
<p><strong>Age:</strong>{{ candidate.age }}</p>
<p><strong>Position:</strong>{{ candidate.position }}</p>
<p><strong>Experience:</strong> {{ candidate.experience }}</p>
<ng-container *ngIf="candidate.experience < 3">
<span class="label junior">JUNIOR</span>
</ng-container>
<ng-container *ngIf="candidate.experience <= 5 && candidate.experience > 3">
<span class="label intermediate">INTERMEDIO</span>
</ng-container>
<ng-container *ngIf="candidate.experience > 5">
<span class="label senior">SENIOR</span>
</ng-container>
<div class="skills">
<h3>Skills</h3>
<div
*ngIf="candidate.skills.length < 4; else moreSkills"
class="skills__name"
>
{{ candidate.skills.join(", ") | uppercase }}
</div>
<ng-template #moreSkills>
<div
class="skills__more"
*ngIf="!showMoreSkills; else allSkills"
(click)="toggleMoreSkills()"
>
más de 3 skills.
</div>
<ng-template #allSkills>
<div>
{{ candidate.skills.join(", ") | uppercase }}
<button (click)="toggleMoreSkills()">X</button>
</div>
</ng-template>
</ng-template>
</div>
<p>
<strong
>Minimum salary: {{ candidate.salary | currency : "EUR" : "€" : "1.0-0"
: "en" }}</strong
>
</p>
</div>
<div><button (click)="doEdit(candidate)">Edit</button></div>
</div>
Como vemos la Pipe de CurrencyPipe tiene argumentos de entrada. Mirar en la documentación lo que significa cada uno. Además se puede cambiar el locale, pero para ésto hace falta instalar un locale del idioma que se desee. Hace falta seguir esta guía.
- Ejercicio: Mostrar el salario internacionalizado para español.
Crear un Pipe propio
Un pipe es otra clase que se define declarable dentro de Angular. Para crear un pipe Angular CLI provee de una herramienta para tal propósito. Como ejemplo vamos a crear un pipe que transforme el nombre y los apellidos del usuario en el mismo string pero con las iniciales en letras capitales.
Empezamos creando el pipe:
ng g pipe pipes/capitalize
Esto nos ha tenido que crear los ficheros src/pipes/capitalize.pipe.ts
y src/pipes/capitalize.pipe.spec.ts
.
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "capitalize",
})
export class CapitalizePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return null;
}
}
Para que una clase sea considerada Pipe por Angular, debe estar anotada por @Pipe
e implementar la interfaz PipeTransform
. Además debe estar añadida a algún módulo. En este caso, según hemos ejecutado el comando anterior, automáticamente ha sido añadida al módulo app.module.ts
.
El método transform
acepta dos parámetros de entrada. El primero, value
es el valor que queremos transformar. En el segundo parámetro se pasan los atributos o parámetros con los que se utiliza el Pipe.
Modifica capitalize.pipe.ts
para que sea:
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({
name: "capitalize",
})
export class CapitalizePipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
return this.capitalizeWords(value as String);
}
private capitalizeWords(str: String) {
return str
.trim()
.split(/\s+/)
.map((word) => {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
})
.join(" ");
}
}
y modifica alguna persona en la lista de app.component.ts
, ejemplo:
...
candidates: Candidate[] = [
{
id: 1,
name: 'josé pérez',
age: 25,
position: 'Desarrollador Junior',
experience: 1,
salary: 20000,
skills: ['Java', 'SQL'],
},
{
id: 2,
name: 'Paco López',
age: 40,
position: 'Desarrollador Senior',
experience: 15,
salary: 40000,
skills: ['Java', 'SQL', 'Oracle', 'PL/SQL', 'Cobol', 'C++'],
},
{
id: 3,
name: 'Mireia García',
age: 30,
position: 'Desarrolladora Intermedia',
experience: 4,
salary: 30000,
skills: ['Java', 'SQL', 'Oracle', 'PL/SQL', 'Cobol', 'C++'],
},
];
...
Como podrás ver el candidato josé pérez
se muestra correctamente como José Pérez
en nuestra aplicación.
- Ejercicio: Hacer un test para
CapitalizePipe
dónde se compruebe que la funcióntransform funciona bien
.
Inyección de dependencias
La Inyección de Dependencias (ID) es un patrón de diseño utilizado para entregar dependencias de una aplicación a otras partes de la misma que lo necesiten. Angular soporta este patrón, permitiendo más flexibilidad y modularidad. Las dependencias en Angular son usualmente servicios, pero también pueden ser valores. Un inyector en Angular instancia estas dependencias usando un proveedor configurado.
La inyección de dependencias es un sistema con dos roles: dependency provider y dependency consumer. Para que un dependency consumer encuentre la dependencia que se consume en el constructor
, usa un Injector
que es el responsable de mirar en un registro si existe una instancia de la dependencia. Las instancias de la dependencia son creadas por los diferentes dependency provider que se han registrado en runtime.
Fuente: angular.io
Principalmente permite a las clases con decoradores Angular como componentes, directivas, pipes, servicios, etc, configurar las dependencias que necesitan.
Actualmente, hemos usado la DI en la directiva highlight
cuando inyectamos la clase ElementRef
:
...
export class HighlightDirective {
...
constructor(private el: ElementRef) {}
...
}
Angular mira en el inyector más cercano a esta clase e inyecta una instancia de la clase ElementRef
. Hay dos roles principales, dependency consumer y dependency provider. Angular realiza la conexión entre consumer y provider utilizando inyectores. Por defecto, durante el bootstrap, Angular crea el root injector. Las instancias de las dependencias que se crean ahí son accesibles a lo largo de toda la aplicación. Angular permite usar funciones, objetos, tipos primitivos y otros tipos como dependencias.
Cómo se resuelven los injector
s
Los Injector
s de Angular se configuran en jerarquía. La cúspide de la pirámide es la siguiente:
Angular va buscando en todos los inyectores qué instancia resuelve una dependencia declarada en un constructor
. Las dependencias se declaran en el array de providers
de un módulo, mediante la anotación @Injectable({ providedIn: 'root' })
o en el array de providers
de un @Component
(en @Directive
también).
Va mirando en el inyector más cercano (array de providers
de un @Component
), luego en siguiente (array de providers
del módulo dónde ha sido declarado) para acabar al final en NullInjector
que siempre devolverá error (a no ser que la dependencia fuera anotada con @Optional
)
Crear CandidateService
Como ejemplo, vamos a refactorizar nuestro app.component.ts
para eliminar de él el array de candidatos. Ese array va a estar dentro de un servicio candidates.service.ts
que será inyectado allá donde haga falta.
Ejecuta
ng g service services/candidates
Se habrá creado dos ficheros con los nombres src/services/candidates.service.ts
y src/services/candidates.service.spec.ts
. El contenido de candidates.service.ts
es:
import { Injectable } from "@angular/core";
@Injectable({
providedIn: "root",
})
export class CandidatesService {
constructor() {}
}
Es una clase normal, solo que anotada con @Injectable
. Esta anotación indica a Angular que debe ser instanciada por un inyector en algún momento del ciclo de vida de la aplicación. Además por defecto Angular CLI crea el servicio configurado con providedIn: "root"
. Esto significa que la instancia del servicio será creada en el root injector por lo que estará disponible en cualquier parte de la aplicación.
También podríamos haber creado el servicio así:
import { Injectable } from "@angular/core";
@Injectable({})
export class CandidatesService {
constructor() {}
}
y luego en el modulo app.module.ts
incluirlo dentro de la propiedad providers
indicando a Angular que el servicio solo estaría disponible para todos los componentes, directivas y pipes declarados en este módulo o en otro módulo dentro del mismo ModuleInjector:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { CandidateComponent } from "./components/candidate/candidate.component";
import { FormsModule } from "@angular/forms";
import { HighlightDirective } from "./directives/highlight.directive";
import { CapitalizePipe } from "./pipes/capitalize.pipe";
import { CandidatesService } from "./services/candidates.service";
@NgModule({
declarations: [
AppComponent,
CandidateComponent,
HighlightDirective,
CapitalizePipe,
],
imports: [
BrowserModule,
FormsModule /* contiene ngModel */,
AppRoutingModule,
],
providers: [CandidatesService],
bootstrap: [AppComponent],
})
export class AppModule {}
De momento usamos la primera opción.
Modificamos el fichero candidates.service.ts
:
import { Injectable } from "@angular/core";
import { Candidate } from "../models/candidate.model";
@Injectable({
providedIn: "root",
})
export class CandidatesService {
private candidates: Candidate[] = [
{
id: 1,
name: "josé pérez",
age: 25,
position: "Desarrollador Junior",
experience: 1,
salary: 20000,
skills: ["Java", "SQL"],
},
{
id: 2,
name: "Paco López",
age: 40,
position: "Desarrollador Senior",
experience: 15,
salary: 40000,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
{
id: 3,
name: "Mireia García",
age: 30,
position: "Desarrolladora Intermedia",
experience: 4,
salary: 30000,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
];
constructor() {}
getCandidates() {
return this.candidates;
}
updateCandidate(candidate: Candidate) {
const candidateIndex = this.candidates.findIndex(
(c) => c.name === candidate.name
);
if (candidateIndex > -1) {
this.candidates[candidateIndex] = Object.assign(
{},
this.candidates[candidateIndex],
candidate
);
}
}
}
Actualizamos app.component.ts
:
import { Component, OnInit, ViewChild, ViewChildren } from "@angular/core";
import { Candidate } from "./models/candidate.model";
import { CandidateComponent } from "./components/candidate/candidate.component";
import { CandidatesService } from "./services/candidates.service";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
})
export class AppComponent implements OnInit {
title = "candidates-frontend";
@ViewChildren(CandidateComponent)
private candidateComps!: CandidateComponent[];
public candidatesLength: number = 0;
selectedCandidate: Candidate | null = null;
get candidateName(): string {
return this.selectedCandidate ? this.selectedCandidate.name : "";
}
get candidateExperience(): number {
return this.selectedCandidate ? this.selectedCandidate.experience : 0;
}
constructor(public candidates: CandidatesService) {}
ngOnInit() {
setTimeout(() => {
this.candidatesLength =
!!this.candidateComps && "length" in this.candidateComps
? this.candidateComps.length
: 0;
});
}
changeInput(event: Event) {
const updateCandidate = Object.assign({}, this.selectedCandidate, {
experience: parseInt((event.target as any).value),
});
this.candidates.updateCandidate(updateCandidate);
}
selectCandidate(candidate: Candidate) {
this.selectedCandidate = candidate;
}
trackById(index: number, item: Candidate) {
return item.id;
}
unselect() {
this.selectedCandidate = null;
}
}
y ajustamos app.component.html
:
<div class="main">
<div>{{ candidateName }}</div>
<div>
<label for="candidate-skill">Candidate Skill</label>
<input type="number" id="candidate-skill" (input)="changeInput($event)" />
<button (click)="unselect()">X</button>
</div>
<button (click)="list.toggleDirection()">Cambiar lista</button>
<div>
<h3>Número de candidatos <span>{{ candidatesLength }}</span></h3>
</div>
<app-candidate-list #list>
<h2 title>Lista de candidatos</h2>
<div
class="candidate-box"
*ngFor="
let candidate of candidates.getCandidates();
let i = index;
trackBy: trackById
"
>
<span>{{ i }}</span>
<app-candidate
appScale
*ngIf="i % 2 === 0; else odd"
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
<ng-template #odd>
<app-candidate
appHighlight
appScale
[candidate]="candidate"
(select)="selectCandidate($event)"
></app-candidate>
</ng-template>
</div>
</app-candidate-list>
</div>
Nuestro componente principal app.component.ts
ahora inyecta el servicio de candidatas candidates.service.ts
a través de su constructor
usando
constructor(public candidates: CandidatesService) {}
Esto nos permite refactorizar la lógica y extraerla de app.component.ts
, haciendo el componente más sencillo y mantenible.
Formas de configurar una dependencia
Resumimos la documentación de Angular. Angular expande el valor de los proveedores en este caso a un objeto proveedor completo de la siguiente manera:
[{ provide: Logger, useClass: Logger }];
La configuración del proveedor expandido es un objeto literal con dos propiedades:
La propiedad ”provide” contiene el token que sirve como clave tanto para localizar un valor de dependencia como para configurar el inyector. La segunda propiedad es un objeto de definición de proveedor, que le dice al inyector cómo crear el valor de dependencia. La clave de definición del proveedor puede ser una de las siguientes:
- useClass - esta opción le dice a Angular DI que instancie una clase proporcionada cuando se inyecta una dependencia.
- useExisting - te permite crear un alias para un token y hacer referencia a uno ya existente.
- useFactory - te permite definir una función que construye una dependencia.
- useValue - proporciona un valor estático que debe usarse como una dependencia
Como ejemplo, mejoramos el CandidateService
extrayendo la constante inicial de los candidatos en otra dependencia de tipo useValue.
Creamos el fichero src/config/app.config.ts
.
import { InjectionToken } from "@angular/core";
import { Candidate } from "../models/candidate.model";
export interface AppConfig {
candidates: Candidate[];
}
export const APP_CONFIG = new InjectionToken<AppConfig>("app.config");
const Candidates: Candidate[] = [
{
id: 1,
name: "josé pérez",
age: 25,
position: "Desarrollador Junior",
experience: 1,
salary: 20000,
skills: ["Java", "SQL"],
},
{
id: 2,
name: "Paco López",
age: 40,
position: "Desarrollador Senior",
experience: 15,
salary: 40000,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
{
id: 3,
name: "Mireia García",
age: 30,
position: "Desarrolladora Intermedia",
experience: 4,
salary: 30000,
skills: ["Java", "SQL", "Oracle", "PL/SQL", "Cobol", "C++"],
},
];
export const Config: AppConfig = {
candidates: Candidates,
};
En el fichero hemos creado APP_CONFIG
, un Injection Token que nos permite poner el sistema de DI cualquier valor que no sea una clase. Además hemos creado un array
de candidatos constante que incluiremos dentro de la configuración.
Para definir la configuración como dependencia inyectable, definimos en el único módulo de la aplicación, app.module.ts
:
import { NgModule } from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { CandidateComponent } from "./components/candidate/candidate.component";
import { CandidateListComponent } from "./components/candidate-list/candidate-list.component";
import { FormsModule } from "@angular/forms";
import { HighlightDirective } from "./directives/highlight.directive";
import { ScaleDirective } from "./directives/scale.directive";
import { CapitalizePipe } from "./pipes/capitalize.pipe";
import { APP_CONFIG, Config } from "./config/app.config";
@NgModule({
declarations: [
AppComponent,
CandidateComponent,
CandidateListComponent,
HighlightDirective,
ScaleDirective,
CapitalizePipe,
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule /* contiene ngModel */,
],
providers: [
{
provide: APP_CONFIG,
useValue: Config,
},
],
bootstrap: [AppComponent],
})
export class AppModule {}
y ahora tenemos disponible la constante para usarla en candidates.service.ts
:
import { Inject, Injectable } from "@angular/core";
import { Candidate } from "../models/candidate.model";
import { APP_CONFIG, AppConfig } from "../config/app.config";
@Injectable({
providedIn: "root",
})
export class CandidatesService {
candidates: Candidate[] = [];
constructor(@Inject(APP_CONFIG) private config: AppConfig) {
if (this.config && this.config.candidates) {
this.candidates = this.config.candidates;
}
}
getCandidates() {
return this.candidates;
}
updateCandidate(candidate: Candidate) {
const candidateIndex = this.candidates.findIndex(
(c) => c.name === candidate.name
);
if (candidateIndex > -1) {
this.candidates[candidateIndex] = Object.assign(
{},
this.candidates[candidateIndex],
candidate
);
}
}
}
El uso de @Inject(APP_CONFIG)
es porque estamos inyectando una dependencia con Injection Token. CandidatesService
es capaz de acceder a la configuración de la aplicación y recoger el array de candidatos inicial.
Ahora si modificamos app.component.ts
, podemos cambiar la configuración desde ahí y cambiar la lista de candidatos:
import { Component, OnInit, ViewChild, ViewChildren } from "@angular/core";
import { Candidate } from "./models/candidate.model";
import { CandidateComponent } from "./components/candidate/candidate.component";
import { CandidatesService } from "./services/candidates.service";
import { APP_CONFIG } from "./config/app.config";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"],
providers: [
{
provide: APP_CONFIG,
useValue: {
candidates: [
{
id: 1,
name: "josé pérez",
age: 25,
position: "Desarrollador Junior",
experience: 1,
salary: 20000,
skills: ["Java", "SQL"],
},
],
},
},
CandidatesService,
],
})
export class AppComponent implements OnInit {
title = "candidates-frontend";
@ViewChildren(CandidateComponent)
private candidateComps!: CandidateComponent[];
public candidatesLength: number = 0;
selectedCandidate: Candidate | null = null;
get candidateName(): string {
return this.selectedCandidate ? this.selectedCandidate.name : "";
}
get candidateExperience(): number {
return this.selectedCandidate ? this.selectedCandidate.experience : 0;
}
constructor(public candidates: CandidatesService) {}
ngOnInit() {
setTimeout(() => {
this.candidatesLength =
!!this.candidateComps && "length" in this.candidateComps
? this.candidateComps.length
: 0;
});
}
changeInput(event: Event) {
const updateCandidate = Object.assign({}, this.selectedCandidate, {
experience: parseInt((event.target as any).value),
});
this.candidates.updateCandidate(updateCandidate);
}
selectCandidate(candidate: Candidate) {
this.selectedCandidate = candidate;
}
trackById(index: number, item: Candidate) {
return item.id;
}
unselect() {
this.selectedCandidate = null;
}
}
Como comentamos anteriormente, el objeto provider puede estar tanto en los módulos como en los componentes / directivas de Angular. En este caso, nuestro CandidateService
tiene una instancia en el inyector root ya que está anotado con providedIn: root
y otra en el inyector asociado al componente AppComponent
.
Esta segunda instancia empieza a resolver sus dependencias desde el mismo inyector, y en ese encuentra una dependencia para AppConfig
, por lo que al final solo se muestra un candidato.
- Ejercicio: Crear un servicio Logger que permita loguear las operaciones de
CandidateService
por consola.