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

  1. Setup del proyecto
  2. Directivas de estructura
    1. Renderizado condicional - *ngIf
    2. NgTemplate y NgContainer
    3. Listas de componentes - *ngFor
  3. Directivas de atributo
    1. ngModel
    2. Construir una directiva de atributo
    3. Pipes
      1. Crear un Pipe propio
  4. Inyección de dependencias
    1. Cómo se resuelven los inyectores
    2. Crear CandidatesService
    3. Formas de configurar una dependencia
  5. 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,

Ejemplo previo a NgContainer

simplemente se pintan los <span>s:

Ejemplo de NgContainer.

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:

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;
}

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é?

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.

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:

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.

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.

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.

Inyección de dependencias en un componente 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 injectors

Los Injectors de Angular se configuran en jerarquía. La cúspide de la pirámide es la siguiente:

Inyectores principales de una app Angular

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:

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.

Referencias