Componentes y databinding en Angular: Guía avanzada | Parte #3

Retomamos esta saga sobre el uso avanzado de componentes y databinding en Angular. Aquí tienes las partes #1 y #2 si te las perdiste.

🤷‍♀️ ¿MÁS PERDID@ QUE UN PULPO EN UN GARAJE? 🐙

Si acabas de aterrizar aquí y estos temas de Angular te suenan a chino, te recomiendo que empieces por este artículo de introducción a Angular con las claves para entenderlo.

Qué es el ciclo de vida de un componente

Para entender el ciclo de vida de un componente (en inglés, component lifecycledebemos explicar ese método que se añade automáticamente cada vez que creamos un componente: el ngOnInitVe a cualquier de tus componentes, al archivo TypeScript (en adelante, TS), y lo encontrarás. Por defecto tiene este aspecto:

ngOnInit() {
}

NgOnInit es una de las etapas (en inglés, hooks) del lifecycle de un componente. Vamos a verlas todas en detalle.

Cuando creamos un componente, Angular instancia dicho componente y lo insertar en el DOM. Durante este proceso de creación del componente (en inglés, instantiation), Angular pasa por distintas fases.

Lo que Angular hace es diferenciar dichas fases para que nosotros podamos ejecutar código dentro de la fase que necesitemos. 

Estas fases son los llamados lifecycle hooks de Angular  👩‍🏫 

Yo me imagino estas fases como las distintas etapas de un tour, en las que Angular nos deja "entrar" y ejecutar código (nos ofrece métodos específicos) dentro de cada una de ellas. 🚴‍♀️ 

Aquí un esquemita para entendernos mejor:

lifecycle hooks
  • 🚴‍♀️ ngOnChanges: el primer hook en el que podemos entrar (el primero al que Angular llama después del constructor). Se puede ejecutar múltiples veces: una al principio, cuando un componente es creado, y cada vez que una de nuestras propiedades decoradas con @Input sufra algún cambio.
  • 🚴‍♀️ ngOnInit: este segundo hook se ejecuta una sola vez, cuando el componente es instanciado. Se ejecuta después del constructor.
  • 🚴‍♀️ ngDoCheck: se ejecutará múltiples veces, siempre que el sistema de change detection se active. Este sistema es lo que permite a Angular detectar si ha habido alguna actividad en la template de un componente. Ojo, porque "alguna actividad" no significa que se haya producido un cambio. Por ejemplo, podemos haber hecho click en un botón que no cambia nada. Aún así, Angular detecta esta actividad (porque igualmente es un evento) usando este hook. Así que, como comprenderás, este hook se ejecuta constantemente. Esto no supone un problema de eficiencia o performance para Angular, ya que el sistema de change detection funciona de una manera muy eficiente.
  • 🚴‍♀️ ngAfterContentInit: este hook es ejecutado cuando el contenido proyectado a través de <ng-content> se inicializa. Es decir, el contenido que va entre las etiquetas de apertura y cierre de tu HTML custom element.
  • 🚴‍♀️ ngAfterContentChecked: este hook se ejecuta cada vez que el contenido proyectado a través de <ng-content> tiene alguna actividad, aunque eso no haya supuesto ningún cambio. Es el mismo caso que en el hook ngDoCheck. Es decir, Angular usa su sistema de change detection.
  • 🚴‍♀️ ngAfterViewInit: esta fase se ejecuta después de que la template de un componente se haya inicializado.
  • 🚴‍♀️ ngAfterViewChecked: esta fase se ejecuta cada vez que Angular detecta una actividad (usando change detection) en la template del componente.
  • 🚴‍♀️ ngOnDestroy: se ejecuta al destruir un componente. Un caso típico de destrucción de un componente es cuando le aplicamos un ngIf y su valor resulta ser false. En este caso, el componente será eliminado del DOM. Este hook es llamado justo antes de la destrucción del componente. Por eso, es una etapa perfecta para "hacer limpieza".

Lifecycle hooks en acción

Basta de teoría, ¡vamos a la práctica! ‍👩‍🍳 Vamos a utilizar el ServerElementComponent para practicar los hooks, que ya trae el constructor y el ngOnInit. 

ngOnInit

Hacemos un console.log en el constructor para que podamos ver en qué momento es llamado el constructor. Lo mismo para el método ngOnInit. Este será el componente donde añadiremos todos los hooks, pero empecemos por estos dos de momento.

  constructor() {
    console.log('constructor was called!');
  }

  ngOnInit() {
    console.log('ngOnInit was called!');
  }

Si ahora guardas y vas a las dev tools del navegador, verás que ambos métodos han sido llamados, primero el constructor y luego el ngOnInit. No tiene nada que ver el orden en el que los hayamos escrito en el archivo TS, porque el orden de ejecución será siempre el mismo. 

constructor y ngoninit called

Prueba a escribir algo en los dos inputs y añadir un nuevo server blueprint. Verás que el constructor y el ngOnInit se vuelven a ejecutar, ya que una nueva instancia (en inglés, instance) del componente ServerElement ha sido creada. 

ngOnChanges

El segundo hook que vamos a probar es el ngOnChanges, que como ya hemos dicho, es el primer lifecycle hook que se ejecuta después del constructor. Como regla general, declararlo como método sería suficiente, pero es una buena práctica (aplicable a todos los hooks) implementar la interface en la clase, como de hecho Angular hace por defecto con la interface de ngOnInit.  

🧐Además también debemos importar el hook desde el módulo angular/core, y lo mismo es aplicable para todos los hooks.

import { Component, OnInit, Input, OnChanges } from '@angular/core';

@Component({
  selector: 'app-server-element',
  templateUrl: './server-element.component.html',
  styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit, OnChanges {

1. Para probar el ngOnChanges hacemos lo mismo que antes: escribimos un console.log para que podamos ver en las dev tools cuándo se ejecuta.

Ojo, porque ngOnChanges es el único hook que recibe un argumento (changes: SimpleChanges), que también debe ser importado desde angular/core.

2. Hacemos un console.log del argumento changes para que ver qué nos devuelve la consola.

  ngOnChanges(changes: SimpleChanges) {
    console.log('ngOnChanges was called!');
    console.log(changes);
  }

Si guardas y vas a tus dev tools, verás que ngOnChanges ha sido llamado el primero (después del constructor) y que el parámetro changes que le habíamos pasado al método ngOnChanges nos devuelve un objeto que, ¡sorpresa 😲! contiene nuestra propiedad element declarada en el archivo server-element.component.ts.

Esto ocurre porque es una propiedad decorada con el @Input decorator. 

ngOnChanges en acción

Podemos observar que Angular nos proporciona cierta información, como el contenido de dicha propiedad, si es el primer cambio que hacemos o no, o cuál era su valor previamente, que en este caso es ninguno (undefined), porque no ha habido ningún cambio desde que este componente ha sido instanciado.

3. Para poder ver cómo el ngOnChanges se dispara más de una vez, vamos a crearnos un botón en el template del AppComponent, justo antes del <app-server-element>Le añadimos un click event vinculado a un método llamado onChangeFirst que posteriormente crearemos.

<button class="btn btn-primary" (click)="onChangeFirst()">Change first element</button>

El objetivo de esa función será cambiar el nombre del título del primer elemento (en la imagen de arriba, cambiaría el nombre del "Rocket Server", para entendernos).

Para que nuestro ejemplo funcione, tenemos que hacer unos cambios en el archivo server-element.component.tsVerás que ahora estamos obteniendo el elemento completo:

@Input('srvElement') element: { type: string, name: string, content: string };

Pero a partir de ahora sólo vamos a necesitar el nameporque de hecho esa es la única propiedad que estábamos reclamando en la template del ServerElementComponent (échale un vistazo).

4. Así que nos olvidamos de la propiedad element y creamos una nueva propiedad (name), que será de tipo string.

  @Input() name: string;

5. Vamos a mostrar esa propiedad en la template del ServerElement mediante string interpolationNos deshacemos también del código donde mostrábamos el element.name.

  <!-- <div class="card-header">{{ element.name }}</div> -->
  <div class="card-header">{{ name }}</div>

Recuerda que el resto de elementos de la card (el card-body, vaya) se proyectan vía ngContent.

6. Ahora en el app.component.html, cuando iteramos el array serverElements, ya no necesitamos vincular la propiedad srvElement, sino la propiedad que acabamos de crear (name). La vinculamos mediante property binding al serverElement.name.

      <app-server-element *ngFor="let serverElement of serverElements"
                          [srvElement]="serverElement"
                          [name]="serverElement.name">

De hecho, puedes eliminar la vinculación al alias srvElement, pero yo la dejo ahí como referencia.

7. Creamos el método onChangeFirst en el archivo app.component.ts. Vamos simplemente a cambiar el nombre del primer elemento del array serverElements por el que tú quieras. 

  onChangeFirst() {
    this.serverElements[0].name = 'Name changed!';
  }

Y ahora, si guardas y vas a tus dev tools, mira lo que pasa cuando haces click en "Change first element".

ngOnChanges en acción

¡Exacto! El nombre cambia, ngOnChanges se dispara al hacer click en el botón y te devuelve un conjunto de información sobre el elemento que acabas de cambiar. 👏 

ngDoCheck

Igual que sus otros compañeros hooks, ngDoCheck también debe implementar su interface y ser importado desde angular/core. 👩‍🏫 Recuerda que este hook es llamado cada vez que change detection se ejecuta cuando Angular detecta que ha habido alguna actividad.

1. Añadimos el método al archivo server-element.component.ts y hacemos un console-log para ver en las dev tools en qué momento Angular llama a este método.

  ngDoCheck() {
    console.log('ngDoCheck was called!');
  }

Si vas a tus dev tools, verás que el hook ha sido llamado dos veces. La segunda es simplemente porque estamos trabajando en modo de desarrollo (en inglés, development mode), y en este modo Angular ejecuta una comprobación extra

Verás que al hacer click en cualquiera de nuestros botones, el ngDoCheck se dispara. 🔫

ngDoCheck es un hook muy útil para el caso en el que quieras comprobar algo manualmente que por alguna razón Angular no detecta. 🙈

AfterContentInit

Como siempre, impórtalo desde angular/core e implementa su interface en la clase de TS. 

Vamos a crear el método ngAfterContentInit y a escribir un console.log para ver en qué momento nos lo imprime la consola, como con los hooks anteriores.

  ngAfterContentInit() {
    console.log('ngAfterContentInit was called!');
  }

Si guardas y vas a tus dev tools, verás que ngAfterContentInit es llamado una sola vez, después de ngDoCheck. Recuerda que sólo es llamado al inicializar el contenido (initialize 😮 ), que hemos proyectado a través de <ng-content>. Es decir, verás que sólo es llamado cuando creamos una de nuestras cards al hacer click en "Add server" o "Add server blueprint".

ngAfterContentChecked

Lo importamos y declaramos su interface como hemos hecho con el resto de hooks. Vamos a hacer nuevamente un console.log en el método para ver qué nos devuelven las dev tools. 

ngAfterContentChecked() {
    console.log('ngAfterContentChecked was called!');
  }

Si guardas y observas tus dev tools, verás que sigue el mismo patrón que el ngDoCheck: es llamado después de cada ciclo de change detection.

ngAfterViewInit

Lo importamos y declaramos su interface como hemos hecho con el resto de hooks. Vamos a hacer nuevamente un console.log en el método para ver qué nos devuelven las dev tools. Verás que este hook ocurre después de que la view del componente se ha inicializado. 

 ngAfterViewInit() {
    console.log('ngAfterViewInit was called!');
  }

ngAfterViewChecked

Lo importamos y declaramos su interface como hemos hecho con el resto de hooks. Vamos a hacer nuevamente un console.log en el método para ver qué nos devuelven las dev tools. Este hook es llamado cada vez que Angular comprueba (mediante su sistema de change detection) si la view del componente ha tenido alguna actividad (que no necesariamente un cambio).

  ngAfterViewChecked() {
    console.log('ngAfterViewChecked was called!');
  }

ngOnDestroy

El último de nuestros hooks es llamado justo antes de que nuestro componente sea destruído.

No olvidemos importarlo y declarar su interface como hemos hecho con el resto de hooks. Vamos a hacer nuevamente un console.log en el método para ver qué nos devuelven las dev tools.

  ngOnDestroy() {
    console.log('ngOnDestroy was called!');
  }

1. Para poder ver este hook en acción necesitamos crear una manera de destruir nuestro componente, así que nos creamos un botón "destructor" en el archivo app.component.html, justo después del botón "Change first element".

2. Le añadimos un click event vinculado a un evento llamado onDestroyFirsten referencia al componente que queremos destruir. 

<button class="btn btn-danger mx-2" (click)="onDestroyFirst()">
   Destroy first component
</button>

3. Añadimos nuestro método al archivo TS del AppComponent, y escribimos el código que eliminará el primer elemento del array serverElements.

  onDestroyFirst() {
    this.serverElements.splice(0, 1);
  }

Y con esta lógica, al pulsar el botón "Destroy first component" verás que en tus dev tools se dispara el hook ngOnDestroy,  💣 ya que hemos eliminado el componente del DOM. 

ngOnDestroy en acción

Cómo acceder a la template de un componente en distintas fases de su ciclo de vida

Si recordamos la sección del @ViewChild en la parte #2 de esta serie, éste servía para acceder al DOM a través de una propiedad del archivo TS de un componente y una local reference en el archivo HTML. Vamos a seguir trabajando en el ServerElementComponent, donde hemos probado todos los lifecycle hooks. 

1. Queremos tener acceso al título de nuestra card, así que colocamos una local reference en el <div class="card-header"> del archivo server-element.component.html.

  <div class="card-header" #heading>{{ name }}</div>

2. En el TS del ServerElementComponent declaramos una variable (llamada header, por ejemplo) con el decorador @ViewChild para poder tener acceso al templateRecuerda que debemos pasarle la local reference como un argumento de tipo string y añadirle el objeto {static: true} como segundo parámetro. 

También debemos importar @ViewChild y ElementRef desde angular/core. Recuerda que el ElementRef es el data type de nuestra propiedad.

  @ViewChild('heading', {static: true}) header: ElementRef;

3. Para ver en qué etapa de Angular tenemos acceso a esta propiedad, vamos a hacer un console.log en el ngOnInit del nativeElement del <div> que contiene la local reference. Como es un <div>, tenemos acceso a la propiedad textContentasí que la usamos para ver si en este primer hook se imprime.

  ngOnInit() {
    console.log('ngOnInit was called!');
    console.log('The content is:' + this.header.nativeElement.textContent);
  }

Si guardas y exploras tus dev tools, verás que no es posible tener acceso a nuestra propiedad en el momento en el que el ngOnInit es llamado (mira cómo el textContent del console.log se imprime vacío en la consola).

hook ngOnInit

Esto es así porque el div <div class="card-header" #heading>{{ name }}</div> es parte de la view del componente ServerElement. Y como hemos visto, la view de un componente se renderiza en la etapa del ngAfterViewInit, es decir, después del ngOnInit, no antes.

Si ahora coges el mismo código (el console.log) y lo pegas en el ngAfterViewInit, verás que ahora el código sí se imprime en la consola.

  ngAfterViewInit() {
    console.log('ngAfterViewInit was called!');
    console.log('The content is:' + this.header.nativeElement.textContent);
  }

 ¡Genial!

Cómo acceder al contenido proyectado con ngContent a través de @ContentChild

Ya hemos visto en qué etapa de los lifecycle hooks podemos acceder a las propiedades que usen @ViewChild. Ahora vamos a utilizar otra herramienta para un supuesto distinto: el caso en el que queramos acceder a contenido proyectado mediante ngContent. 

Utilizamos @ContentChild para acceder a contenido de un componente X que se encuentra proyectado en un componente Z vía "ngContent".

¿Y dónde se da este caso? En el AppComponent, en cuya template tenemos el selector <app-server-element> con contenido proyectado desde el ServerElementComponent mediante el <ng-content>. El contenido al que queremos acceder es el que se encuentra dentro de las etiquetas de apertura y cierre del <app-server-element>. Aquí un esquemita aclarativo:

viewChild y contentChild esquema

1. En el app.component.html, añadimos una local reference en el único <p> dentro del selector <app-server-element>, al que llamamos contentParagraph

Lo que queremos es poder usar esa local reference en el ServerElementComponent. Para esto no podemos usar el ViewChild, sino el ContentChildporque son dos cosas distintas: uno da acceso a la template (view) del componente cuyo contenido proyectamos, y otro al contenido proyectado con ngContent.  

        <p class="card-text" #contentParagraph>

2. En el archivo server-element.component.ts importamos @ContentChild desde angular/core y nos creamos la propiedad a la que le aplicaremos el @ContentChild. Al igual que con el decorador @ViewChild, la propiedad será de tipo ElementRef y a este decorador hay que pasarle dos parámetros:

  • el selector (nuestra local reference como string)
  • un objeto {static: true}
  @ContentChild('contentParagraph', {static: true}) paragraph: ElementRef;

Ten en cuenta que no podremos acceder a nuestra propiedad hasta que llegamos a cierta etapa: el ngAfterContentInit.

3. Si ahora hacemos un console.log en el ngOnInit

  ngOnInit() {
    console.log('ngOnInit was called!');
    console.log('The content is:' + this.header.nativeElement.textContent);
    console.log('The content of the paragraph is: ' + this.paragraph.nativeElement.textContent);
  }

 y en el ngAfterContentInit

  ngAfterContentInit() {
    console.log('ngAfterContentInit was called!');
    console.log('The content of the paragraph is: ' + this.paragraph.nativeElement.textContent);
  }

 verás que el console.log del ngOnInit se imprime vacío, mientras que en el ngAfterContentInit se imprimirá el contenido del párrafo. Así, hemos demostrado que el contenido proyectado en un componente mediante ngContent sólo se ejecuta a partir de la etapa ngAfterContentInit.

ngAfterContentInit en acción

¡FIN DE LA SERIE! 

Y hasta aquí esta saga sobre el uso avanzado de componentes en Angular. Si te queda alguna duda, ¡nos vemos en los comentarios! Y si quieres seguir aprendiendo, aquí tienes un coding challenge sobre el tema

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Más recursos de aprendizaje

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

angular the complete guide - curso Max S.

  Max Schwarzmüller

curso angular fernando herrera

   Fernando Herrera

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

libro 1 angular
libro 2 angular

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

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

Otros artículos que pueden interesarte

Guía de iniciación al data binding en Angular
¿Qué es el databinding?El databinding es la forma que tiene Angular para permitirnos mostrar contenido dinámico en lugar de estático (en inglés, hardcoded). Podríamos[...]
Cómo aprendí a programar cuando estaba «programada» para ser de letras
[tcb-script src="https://player.vimeo.com/api/player.js"][/tcb-script]A nadie le gusta su trabajo. Eso es lo que me decía a mí misma cuando conseguí mi primer[...]
Cómo construir un pop-up con vanilla JavaScript
¿Qué vamos a construir? Vamos a construir una ventana emergente, popup, overlay, dialog o como quieras llamarla, pero esos 3 nombres son[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

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

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

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

Cerrar