Capítulo 4. Comprender y utilizar los componentes de Angular

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

En el capítulo anterior, hicimos una inmersión profunda en las directivas integradas que ofrece Angular y que nos permiten realizar funciones comunes de como ocultar y mostrar elementos, repetir plantillas, etc. Trabajamos con directivas como ngIf y ngForOf y nos hicimos una idea de cómo y cuándo utilizarlas.

En este capítulo, profundizaremos un poco más en los componentes, esos elementos que hemos estado creando para representar la interfaz de usuario y permitir que los usuarios interactúen con las aplicaciones que construimos. Cubriremos algunos de los atributos más útiles que puedes especificar al crear componentes, cómo pensar en el ciclo de vida del componente y los distintos ganchos que te proporciona Angular y, por último, cubriremos cómo pasar datos dentro y fuera de tus componentes personalizados. Al final del capítulo, deberías ser capaz de realizar la mayoría de las tareas comunes relacionadas con los componentes, entendiendo qué estás haciendo y por qué.

Componentes-Resumen

En el capítulo anterior, vimos que Angular sólo tiene directivas, y que las directivas se reutilizan para múltiples propósitos. Nos ocupamos de las directivas de atributo y estructurales, que nos permiten cambiar el comportamiento de un elemento existente o cambiar la estructura de la plantilla que se está renderizando.

El tercer tipo de directivas son los componentes, que hemos estado utilizando prácticamente desde el primer capítulo. Hasta cierto punto, puedes considerar que una aplicación Angular no es más que un árbol de componentes. Cada componente tiene a su vez un comportamiento y una plantilla que se renderiza. Esta plantilla puede seguir utilizando otros componentes, formando así un árbol de componentes, que es la aplicación Angular que se renderiza en el navegador.

En su forma más simple, un componente no es más que una clase que encapsula un comportamiento (qué datos cargar, qué datos renderizar y cómo responder a las interacciones del usuario) y una plantilla (cómo se renderizan los datos). Pero también hay múltiples formas de definir eso, junto con otras opciones, que cubriremos en las siguientes secciones.

Definir un componente

Definimos un componente utilizando el decorador TypeScript Component. Esto nos permite anotar cualquier clase con algunos metadatos que enseñan a Angular cómo funciona el componente, qué debe renderizar, etc. Echemos un vistazo de nuevo al componente stock-item que hemos creado para ver qué aspecto tendría un componente sencillo, y partiremos de ahí:

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}

El componente muy básico sólo necesita un selector (para indicarle a Angular cómo encontrar instancias del componente que se está utilizando) y una plantilla (que Angular debe renderizar cuando encuentre el elemento). Todos los demás atributos del decorador Component son opcionales. En el ejemplo anterior, hemos definido que el StockItemComponent se renderice siempre que Angular encuentre el selector app-stock-item, y que renderice el archivo stock-item.component.html cuando encuentre el elemento. Hablemos de los atributos del decorador con un poco más de detalle.

Selector

El atributo selector, como ya comentamos brevemente en el Capítulo 2, nos permite definir cómo identifica Angular cuando se utiliza el componente en HTML. El selector toma un valor de cadena, que es el selector CSS que Angular utilizará para identificar el elemento. La práctica recomendada cuando creamos nuevos componentes es utilizar selectores de elementos (como hicimos con app-stock-item), pero técnicamente también podrías utilizar cualquier otro selector. Por ejemplo, aquí tienes algunas formas en las que podrías especificar el atributo selector y cómo lo utilizarías en el HTML:

  • selector: 'app-stock-item' haría que el componente se utilizara como <app-stock-item></app-stock-item> en el HTML.

  • selector: '.app-stock-item' haría que el componente se utilizara como una clase CSS como <div class="app-stock-item"></div> en el HTML.

  • selector: '[app-stock-item]' haría que el componente se utilizara como atributo en un elemento existente como <div app-stock-item></div> en el HTML.

Puedes hacer el selector tan simple o complejo como quieras, pero como regla general, intenta ceñirte a selectores de elementos simples a menos que tengas una razón de peso para no hacerlo.

Plantilla

Hasta ahora hemos estado utilizando templateUrl para definir cuál es la plantilla que se utilizará junto con un componente. La ruta que pases al atributo templateUrl es relativa a la ruta del componente. En el caso anterior, podemos especificar el templateUrl como:

templateUrl: './stock.item.component.html'

o:

templateUrl: 'stock.item.component.html'

y funcionaría. Pero si intentas especificar una URL absoluta o cualquier otra cosa, la compilación se rompería. Algo interesante a tener en cuenta es que, a diferencia de AngularJS (1.x), la aplicación que Angular construye no carga la plantilla por URL en tiempo de ejecución. En lugar de eso, Angular precompila una compilación y se asegura de que la plantilla se inline como parte del proceso de compilación.

En lugar de templateUrl, también podríamos especificar la plantilla inline en el componente, utilizando la opción template. Esto nos permite que el componente contenga toda la información en lugar de dividirla en código HTML y TypeScript.

Consejo

Sólo se puede especificar uno de template y templateUrl en un componente. No puedes utilizar ambos, pero al menos uno es esencial.

No tiene ningún impacto en la aplicación final generada, ya que Angular compila el código en un único paquete. La única razón por la que podrías querer dividir el código de tus plantillas en un archivo separado es para obtener mejores funciones del IDE, como la finalización de sintaxis y similares, que son específicas de las extensiones de archivo. En general, es posible que quieras mantener tus plantillas separadas si tienen más de tres o cuatro líneas o tienen alguna complejidad.

Veamos qué aspecto podría tener nuestro componente stock-item con una plantilla en línea:

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

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  template: `
	<div class="stock-container">
	  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
	  <div class="price"
	      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
	      $ {{stock.price}}
   </div>
	  <button (click)="toggleFavorite($event)"
	          *ngIf="!stock.favorite">Add to Favorite</button>
	</div>
  `,
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}
Consejo

ECMAScript 2015 (y TypeScript) nos permite definir plantillas multilínea utilizando el símbolo ` (backtick), en lugar de hacer la concatenación de cadenas a través de varias líneas utilizando el operador + (plus). Solemos aprovechar esto cuando definimos plantillas en línea.

Puedes encontrar el código completo en la carpeta chapter4/component-template del repositorio de GitHub.

Lo único que hemos hecho es coger la plantilla y moverla al atributo template del decorador Component. Sin embargo, en este caso concreto, como hay más de unas pocas líneas en las que se realiza cierto trabajo, yo recomendaría no moverla inline. Ten en cuenta que al moverlo a template, hemos eliminado el atributo templateUrl anterior.

Estilos

Un componente determinado puede tener varios estilos adjuntos. Esto te permite extraer CSS específico del componente, así como potencialmente cualquier otro CSS común que deba aplicársele. Al igual que con las plantillas, puedes alinear tu CSS utilizando el atributo styles, o si hay una cantidad significativa de CSS o quieres aprovechar tu IDE, puedes extraerlo en un archivo separado e introducirlo en tu componente utilizando el atributo styleUrls. Ambos toman una matriz como entrada.

Una cosa que Angular promueve desde el principio es la encapsulación y el aislamiento completos de los estilos. Esto significa que, por defecto, los estilos que definas y utilices en un componente no afectarán/impactarán a ningún otro componente padre o hijo. Esto garantiza que puedes estar seguro de que las clases CSS que definas en cualquier componente no afectarán, sin saberlo, a ningún otro, a menos que introduzcas explícitamente los estilos necesarios.

De nuevo, al igual que las plantillas, Angular no incorporará estos estilos en tiempo de ejecución, sino que precompilará y creará un paquete con los estilos necesarios. Por tanto, la elección de utilizar styles o styleUrls es personal, sin mayor repercusión en tiempo de ejecución.

Advertencia

No utilices a la vez styles y styleUrls. Angular acabará eligiendo uno u otro y provocará un comportamiento inesperado.

Veamos rápidamente cómo quedaría el componente si inlineáramos los estilos:

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

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: 'stock-item.component.html',
  styles: [`
    .stock-container {
      border: 1px solid black;
      border-radius: 5px;
      display: inline-block;
      padding: 10px;
    }

    .positive {
      color: green;
    }

    .negative {
      color: red;
    }
  `]
})
export class StockItemComponent implements OnInit {
   // Code omitted here for clarity
}

Puedes encontrar el código completo en la carpeta chapter4/component-style del repositorio de GitHub.

Por supuesto, puedes optar por pasar varias cadenas de estilo al atributo. La decisión entre utilizar styles y styleUrls es una cuestión de preferencia personal y no tiene ningún impacto en el rendimiento final de la aplicación.

Estilo Encapsulado

En la sección anterior, hablamos de cómo Angular encapsula los estilos para asegurarse de que no contamina ninguno de tus otros componentes. De hecho, puedes decirle a Angular si necesita hacer esto o no, o si los estilos pueden ser accesibles globalmente. Puedes establecerlo utilizando el atributo encapsulation en el decorador Component. El atributo encapsulation toma uno de tres valores:

ViewEncapsulation.Emulated

Este es el valor por defecto, en el que Angular crea CSS calado para emular el comportamiento que proporcionan los DOMs sombra y las raíces sombra.

ViewEncapsulation.Native

Este es el ideal, en el que Angular utilizará raíces sombra. Esto sólo funcionará en navegadores y plataformas que lo soporten de forma nativa.

ViewEncapsulation.None

Utiliza CSS global, sin ninguna encapsulación.

¿Qué es el DOM en la sombra?

HTML, CSS y JavaScript tienen una tendencia por defecto a ser globales en el contexto de la página actual. Lo que esto significa es que un ID dado a un elemento puede chocar fácilmente con otro elemento en otra parte de la página. Del mismo modo, una regla CSS dada a un botón en una esquina de la página puede acabar afectando a otro botón totalmente ajeno.

Acabamos teniendo que idear convenciones de nomenclatura específicas, utilizar trucos de CSS como !important y muchas otras técnicas para evitarlo en nuestro desarrollo cotidiano.

Shadow DOM soluciona este problema al delimitar el DOM HTML y el CSS. Ofrece la posibilidad de asignar estilos a un componente (evitando así que los estilos se filtren y afecten al resto de la aplicación) y también la posibilidad de aislar y hacer que el DOM sea autónomo.

Puedes leer más sobre ello en la documentación de los componentes web autónomos.

La mejor manera de ver cómo afecta esto a nuestra aplicación es hacer un ligero cambio y ver cómo se comporta nuestra aplicación en circunstancias diferentes.

En primer lugar, vamos a añadir el siguiente fragmento de código al archivo app.component.css. Estamos utilizando la misma base que en el capítulo anterior, y el código completo está disponible en la carpeta chapter4/component-style-encapsulation:

.name {
  font-size: 50px;
}

Si ejecutamos la aplicación ahora mismo, no habrá ningún impacto en nuestra aplicación. Ahora, probemos a cambiar la propiedad encapsulation en el componente principal AppComponent. Cambiaremos el componente de la siguiente manera

import { Component, ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class AppComponent {
  title = 'app works!';
}

Añadimos la línea encapsulation: ViewEncapsulation.None a nuestro decorador Component (por supuesto, después de importar el enum ViewEncapsulation de Angular). Ahora, si refrescamos nuestra aplicación, verás que el nombre de la acción se ha ampliado a 50px. Esto se debe a que los estilos aplicados en AppComponent no se limitan sólo al componente, sino que ahora toman el espacio de nombres global. Así, a cualquier elemento que se añada a sí mismo la clase name se le aplicará este tamaño de fuente.

ViewEncapsulation.None es una buena forma de aplicar estilos comunes a todos los componentes hijos, pero definitivamente añade el riesgo de contaminar el espacio de nombres CSS global y tener efectos no intencionados.

Otros

Hay muchos más atributos de los que hemos tratado en el decorador Component. Aquí repasaremos brevemente algunos de ellos, y reservaremos la discusión de otros para capítulos posteriores, cuando sean más relevantes. Aquí tienes un breve resumen de algunos de los otros atributos principales y sus usos:

Eliminar espacios en blanco

Angular te permite eliminar cualquier espacio en blanco innecesario de tu plantilla (tal y como lo define Angular, incluyendo más de un espacio, espacio entre elementos, etc.). Esto puede ayudar a reducir el tamaño de compilación al comprimir tu HTML. Puedes establecer esta función (que está establecida por defecto en false ) utilizando el atributo preserveWhitespaces en el componente. Puedes leer más sobre esta función en la documentación oficial.

Animaciones

Angular te ofrece múltiples activadores para controlar y animar cada parte del componente y su ciclo de vida. Para ello, proporciona su propio DSL, que permite a Angular animar sobre los cambios de estado dentro del elemento.

Interpolación

Hay ocasiones en las que los marcadores de interpolación predeterminados de Angular (los rizos dobles {{ y }}) interfieren en la integración con otros frameworks o tecnologías. Para esos escenarios, Angular te permite anular los identificadores de interpolación a nivel de componente especificando los delimitadores de inicio y fin. Puedes hacerlo utilizando el atributo interpolation, que toma una matriz de dos cadenas, los delimitadores de apertura y cierre de la interpolación. Por defecto, son ['{{', '}}'], pero puedes anularlo, por ejemplo, proporcionando interpolation: ['<<', '>>'] para sustituir los símbolos de interpolación de ese componente por << y >>.

Ver proveedores

Los proveedores de vistas te permiten definir proveedores que inyectan clases/servicios en un componente o en cualquiera de sus hijos. Normalmente, no lo necesitarás, pero si hay determinados componentes en los que quieres anular o restringir la disponibilidad de una clase o un servicio, puedes especificar una serie de proveedores a un componente utilizando el atributo viewProviders. Trataremos esto con más detalle en el Capítulo 8.

Exportar el componente

Hasta ahora hemos estado trabajando utilizando las funciones de la clase del componente dentro del contexto de la plantilla. Pero hay casos de uso (especialmente cuando empezamos a tratar con directivas y componentes más complejos) para los que quizá queramos permitir que el usuario del componente llame a funciones del componente desde fuera. Un caso de uso podría ser que proporcionemos un componente carrusel, pero queramos proporcionar una funcionalidad que permita al usuario del componente controlar la funcionalidad siguiente/anterior. En estos casos, podemos utilizar el atributo exportAs del decorador Component.

changeDetection

Por defecto, Angular comprueba cada enlace de la IU para ver si necesita actualizar algún elemento de la IU cada vez que cambia algún valor en nuestro componente. Esto es aceptable para la mayoría de las aplicaciones, pero a medida que nuestras aplicaciones aumentan de tamaño y complejidad, es posible que queramos controlar cómo y cuándo Angular actualiza la IU. En lugar de que Angular decida cuándo tiene que actualizar la IU, es posible que queramos ser explícitos y decirle a Angular cuándo tiene que actualizar la IU manualmente. Para ello, utilizamos el atributo changeDetection, donde podemos anular el valor por defecto de Change​DetectionStrategy.Default a ChangeDetectionStrategy.OnPush. Esto significa que, tras la renderización inicial, dependerá de nosotros que Angular sepa cuándo cambia el valor. Angular no comprobará automáticamente los enlaces del componente. Trataremos esto con más detalle más adelante en el capítulo.

Hay muchos más atributos y características en relación con los componentes que no cubrimos en este capítulo. Deberías echar un vistazo a la documentación oficial de los componentes para familiarizarte con qué más es posible, o profundizar en los detalles.

Componentes y módulos

Antes de entrar en los detalles del ciclo de vida de un componente, vamos a desviarnos rápidamente hacia cómo se vinculan los componentes a los módulos y cuál es su relación. En el Capítulo 2, vimos cómo cada vez que creábamos un nuevo componente, teníamos que incluirlo en un módulo. Si creas un nuevo componente y no lo añades a un módulo, Angular se quejará de que tienes componentes que no forman parte de ningún módulo.

Para que cualquier componente pueda utilizarse en el contexto de un módulo, debe importarse en tu archivo de declaración del módulo y declararse en la matriz declarations. Esto garantiza que el componente sea visible para todos los demás componentes del módulo.

Hay tres atributos específicos en NgModule que afectan directamente a los componentes y a su uso, y que es importante conocer. Aunque inicialmente sólo es importante declarations, una vez que empiezas a trabajar con varios módulos, o si estás creando o importando otros módulos, los otros dos atributos se vuelven esenciales:

declarations

El atributo declarations garantiza que los componentes y directivas estén disponibles para su uso dentro del ámbito del módulo. La CLI de Angular añadirá automáticamente tu componente o directiva al módulo cuando crees un componente a través de él. Cuando empiezas a crear aplicaciones Angular, puedes olvidarte fácilmente de añadir tus componentes recién creados al atributo declarations, así que no lo pierdas de vista (¡si no utilizas la CLI de Angular, claro!) para evitar este error tan común.

imports

El atributo imports te permite especificar los módulos que quieres importar y que sean accesibles dentro de tu módulo. Esto se utiliza principalmente como una forma de importar módulos de terceros para que los componentes y servicios estén disponibles dentro de tu aplicación. Si quieres utilizar un componente de otros módulos, asegúrate de importar los módulos pertinentes en el módulo que has declarado y en el que existe el componente.

exports

El atributo exports es relevante si tienes varios módulos o necesitas crear una biblioteca que utilizarán otros desarrolladores. A menos que exportes un componente, no se podrá acceder a él ni utilizarlo fuera del módulo directo en el que se declara el componente. Como regla general, si vas a necesitar utilizar el componente en otro módulo, asegúrate de exportarlo.

Consejo

Si te enfrentas a problemas al utilizar un componente, en los que Angular no reconoce un componente o dice que no reconoce un elemento, lo más probable es que se deba a módulos mal configurados. Comprueba, en orden, lo siguiente:

  • Si el componente se añade como declaración en el módulo.

  • En caso de que no sea un componente que hayas escrito tú, asegúrate de que has importado el módulo que proporciona/exporta el componente.

  • Si has creado un nuevo componente que debe utilizarse en otros componentes, asegúrate de que exportas el componente en su módulo para que cualquier aplicación que incluya el módulo tenga acceso a tu componente recién creado.

Entrada y salida

Un caso de uso común cuando empezamos a crear componentes es que queremos separar el contenido que utiliza un componente del propio componente. Un componente es verdaderamente útil cuando es reutilizable. Una de las formas de hacer que un componente sea reutilizable (en lugar de tener valores predeterminados y codificados en su interior) es pasándole diferentes entradas en función del caso de uso. Del mismo modo, puede haber casos en los que queramos ganchos de un componente cuando ocurra una determinada actividad dentro de su contexto.

Angular proporciona ganchos para especificar cada uno de ellos mediante decoradores, acertadamente denominados Input y Output. Éstos, a diferencia de los decoradores Component y NgModule, se aplican a nivel de variable miembro de una clase.

Entrada

Cuando añadimos un decorador Input en una variable miembro, automáticamente te permite pasar valores a el componente para esa entrada concreta mediante la sintaxis de enlace de datos de Angular.

Veamos cómo podemos ampliar nuestro componente stock-item del capítulo anterior para que nos permita pasar el objeto stock, en lugar de codificarlo dentro del propio componente. El ejemplo terminado está disponible en el repositorio de GitHub, en la carpeta chapter4/component-input. Si quieres codificar y no tienes el código anterior, puedes utilizar la base de código chapter3/ng-if como punto de partida.

Primero modificaremos el componente stock-item para marcar la acción como entrada al componente, pero en lugar de inicializar el objeto acción, lo marcaremos como Input al componente. Lo haremos importando el decorador y utilizándolo para la variable stock. El código del archivo stock-item.component.ts debería tener el siguiente aspecto:

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

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent {

  @Input() public stock: Stock;

  constructor() { }

  toggleFavorite(event) {
    this.stock.favorite = !this.stock.favorite;
  }
}

Hemos eliminado toda la lógica de instanciación del componente app-stock-item, y hemos marcado la variable stock como entrada. Esto significa que la lógica de inicialización se ha desplazado fuera, y el componente sólo es responsable de recibir el valor de la acción del componente padre y limitarse a renderizar los datos.

A continuación, echemos un vistazo a AppComponent y a cómo podemos cambiarlo para que ahora pase los datos a StockItemComponent:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'Stock Market App';

  public stockObj: Stock;

  ngOnInit(): void {
    this.stockObj = new Stock('Test Stock Company', 'TSC', 85, 80);
  }
}

Acabamos de trasladar la inicialización del objeto acción de StockItemComponent a AppComponent. Por último, echemos un vistazo a la plantilla de AppComponent para ver cómo podemos pasar la acción a StockItemComponent:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stockObj"></app-stock-item>

Utilizamos el enlace de datos de Angular para pasar la acción de AppComponent a StockItemComponent. El nombre del atributo (stock) tiene que coincidir con el nombre de la variable del componente que se ha marcado como entrada. El nombre del atributo distingue entre mayúsculas y minúsculas, así que asegúrate de que coincide exactamente con el nombre de la variable de entrada. El valor que le pasamos es la referencia del objeto de la clase AppComponent, que es stockObj.

¿HTML y atributos sensibles a mayúsculas y minúsculas?

Te preguntarás cómo es posible. Angular tiene su propio analizador HTML oculto que analiza las plantillas en busca de sintaxis específica de Angular, y no depende de la API DOM para algunas de ellas. Por eso los atributos de Angular distinguen y pueden distinguir entre mayúsculas y minúsculas.

Estas entradas están vinculadas a datos, por lo que si acabas cambiando el valor del objeto en AppComponent, se reflejará automáticamente en el hijo StockItemComponent.

Salida

Al igual que podemos pasar datos a un componente, también podemos registrar y escuchar eventos de un componente. Utilizamos la vinculación de datos para introducirlos y la sintaxis de vinculación de eventos para registrarlos. Para ello utilizamos el decorador Output.

Registramos un EventEmitter como salida de cualquier componente. A continuación, podemos activar el evento utilizando el objeto EventEmitter, lo que permitirá que cualquier componente vinculado al evento reciba la notificación y actúe en consecuencia.

Podemos utilizar el código del ejemplo anterior en el que registramos un decorador Input y continuar a partir de ahí. Extendamos ahora el StockComponent para que active un evento cuando se le dé un favorito, y traslademos la manipulación de los datos del componente a su padre. Esto también tiene sentido porque el componente padre es el responsable de los datos y debería ser la única fuente de verdad. Así, dejaremos que el componente padre AppComponent se registre en el evento toggleFavorite y cambie el estado de la acción cuando se active el evento.

El código terminado para esto está en la carpeta chapter4/component-output.

Echa un vistazo al código StockItemComponent en src/app/stock/stock-item/stock-item.component.ts:

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

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }
}

Hay que tener en cuenta algunas cosas importantes:

  • Importamos el decorador Output así como el EventEmitter de la biblioteca Angular.

  • Creamos un nuevo miembro de la clase llamado toggleFavorite de tipo EventEmitter, y renombramos nuestro método a onToggleFavorite. El EventEmitter se puede tipar para mayor seguridad de tipo.

  • Tenemos que asegurarnos de que se inicializa la instancia EventEmitter, ya que no se autoinicializa por nosotros. Puedes hacerlo en línea o en el constructor, como hicimos antes.

  • El onToggleFavorite ahora sólo llama a un método del EventEmitter para emitir el objeto acción completo. Esto significa que todos los oyentes del evento toggleFavorite obtendrán como parámetro el objeto acción actual.

También cambiaremos stock-item.component.html para que llame al método onToggleFavorite en lugar de a toggleFavorite. Por lo demás, el marcado HTML sigue siendo prácticamente el mismo:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <button (click)="onToggleFavorite($event)"
          *ngIf="!stock.favorite">Add to Favorite</button>
</div>

A continuación, añadimos un método a AppComponent que debe activarse cada vez que se active el método onToggleFavorite, al que añadiremos la vinculación de eventos:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';

  public stock: Stock;

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company', 'TSC', 85, 80);
  }

  onToggleFavorite(stock: Stock) {
    console.log('Favorite for stock ', stock, ' was triggered');
    this.stock.favorite = !this.stock.favorite;
  }
}

Lo único nuevo es el método onToggleFavorite que hemos añadido, que toma una acción como argumento. En este caso concreto, no utilizamos la acción que se le pasa más que para el registro, pero podrías basar cualquier decisión/trabajo en ello. Ten en cuenta también que el nombre de la función no es relevante, y podrías llamarla como quieras.

Por último, vamos a unirlo todo suscribiéndonos a la nueva salida de nuestro StockComponent en el archivo app-component.html:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stock"
                (toggleFavorite)="onToggleFavorite($event)">
</app-stock-item>

Acabamos de añadir un evento vinculante utilizando la sintaxis de eventos vinculantes de Angular a la salida declarada en el componente stock-item. Observa de nuevo que distingue entre mayúsculas y minúsculas y que tiene que coincidir exactamente con la variable miembro que hemos decorado con el decorador Output. Además, para acceder al valor emitido por el componente, utilizamos la palabra clave $event como parámetro de la función. Sin ella, la función seguiría activándose, pero no obtendrías ningún argumento con ella.

Con esto, si ejecutas la aplicación (recuerda, ng serve), deberías ver la aplicación completamente funcional, y cuando pulses el botón Añadir a Favoritos, debería activarse el método en AppComponent.

Detección de cambios

Hemos mencionado changeDetection como atributo del decorador Component. Ahora que hemos visto cómo funcionan los decoradores Input y Output, vamos a profundizar un poco en cómo realiza Angular su detección de cambios a nivel de componente.

Por defecto, Angular aplica el mecanismo ChangeDetectionStrategy.Default al atributo changeDetection. Esto significa que cada vez que Angular advierta un evento (por ejemplo, una respuesta del servidor o una interacción del usuario), recorrerá cada componente del árbol de componentes y comprobará individualmente cada uno de los enlaces para ver si alguno de los valores ha cambiado y debe actualizarse en la vista.

En una aplicación muy grande, tendrás muchos enlaces en una página determinada. Cuando un usuario realiza alguna acción, tú como desarrollador puedes saber con seguridad que la mayor parte de la página no cambiará. En tales casos, puedes dar una pista al detector de cambios de Angular para que compruebe o no determinados componentes según te convenga. Para cualquier componente dado, podemos conseguirlo cambiando el ChangeDetectionStrategy del predeterminado a ChangeDetectionStrategy.OnPush. Lo que esto le dice a Angular es que las vinculaciones para este componente en particular se comprobarán sólo en función de Input a este componente.

Consideremos algunos ejemplos para ver cómo puede funcionar esto. Digamos que tenemos un árbol de componentes A → B → C. Es decir, tenemos un componente raíz A, que utiliza un componente B en su plantilla, que a su vez utiliza un componente C. Y digamos que el componente B pasa un objeto compuesto compositeObj al componente C como entrada. Tal vez algo como

<c [inputToC]="compositeObj"></c>

Es decir, inputToC es la variable de entrada marcada con el decorador Input en el componente C, y se le pasa el objeto compositeObj desde el componente B. Ahora digamos que marcamos el atributo changeDetection del componente C como ChangeDetectionStrategy​.OnPush. He aquí las implicaciones de ese cambio:

  • Si el componente C tiene enlaces a cualquier atributo de compositeObj, funcionarán como siempre (sin cambios respecto al comportamiento por defecto).

  • Si el componente C realiza algún cambio en alguno de los atributos de compositeObj, también se actualizarán inmediatamente (sin cambios respecto al comportamiento por defecto).

  • Si el componente padre B crea un nuevo compositeObj o cambia la referencia de compositeObj (piensa en un nuevo operador, o en asignar desde una respuesta del servidor), entonces el componente C reconocería el cambio y actualizaría sus bindings para el nuevo valor (no cambia el comportamiento por defecto, pero cambia el comportamiento interno sobre cómo Angular reconoce el cambio).

  • Si el componente padre B cambia algún atributo en el compositeObj directamente (como respuesta a una acción del usuario fuera del componente B), estos cambios no se actualizarían en el componente C (cambio importante respecto al comportamiento por defecto).

  • Si el componente padre B cambia cualquier atributo en respuesta a un emisor de eventos del componente C, y luego cambia cualquier atributo en el compositeObj (sin cambiar la referencia), esto seguiría funcionando y los enlaces se actualizarían. Esto se debe a que el cambio se origina en el componente C (no cambia el comportamiento predeterminado).

Angular nos proporciona formas de señalar cuándo comprobar los enlaces también desde dentro del componente, para tener un control absoluto sobre el enlace de datos de Angular. Lo veremos en "Detección de cambios". Por ahora, es bueno entender la diferencia entre las dos estrategias de detección de cambios que proporciona Angular.

Ahora vamos a modificar el código de ejemplo para ver esto en acción. En primer lugar, modifica el archivo stock-item.component.ts para cambiar el ChangeDetectionStrategy en el componente hijo:

import { Component, OnInit, Input, Output } from '@angular/core';
import { EventEmitter, ChangeDetectionStrategy } from '@angular/core';

import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class StockItemComponent {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }

  changeStockPrice() {
    this.stock.price += 5;
  }
}

Además de cambiar el ChangeDetectionStrategy, también añadimos otra función a changeStockPrice(). Utilizaremos estas funciones para demostrar el comportamiento de la detección de cambios en el contexto de nuestra aplicación.

A continuación, vamos a modificar rápidamente stock-item.component.html para que nos permita activar la nueva función. Simplemente añadiremos un nuevo botón para activar y cambiar el precio de las acciones cuando se pulse el botón:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <button (click)="onToggleFavorite($event)"
          *ngIf="!stock.favorite">Add to Favorite</button>
  <button (click)="changeStockPrice()">Change Price</button>
</div>

No hay ningún cambio en el HTML de la plantilla, aparte de añadir un nuevo botón para cambiar el precio de las acciones. Del mismo modo, cambiemos rápidamente el archivo principal app.component.html para añadir otro botón que active el cambio del precio desde el componente padre (similar al componente B del ejemplo hipotético anterior):

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stock"
                (toggleFavorite)="onToggleFavorite($event)">
</app-stock-item>
<button (click)="changeStockObject()">Change Stock</button>
<button (click)="changeStockPrice()">Change Price</button>

Hemos añadido dos nuevos botones a esta plantilla: uno que cambiará la referencia del objeto acción directamente, y otro que modificará la referencia existente del objeto acción para cambiar el precio desde el padre AppComponent. Ahora, por último, podemos ver cómo se conecta todo esto en el archivo app.component.ts:

import { Component, OnInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  title = 'app works!';

  public stock: Stock;
  private counter: number = 1;

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company - ' + this.counter++,
        'TSC', 85, 80);
  }

  onToggleFavorite(stock: Stock) {
    // This will update the value in the stock item component
    // Because it is triggered as a result of an event
    // binding from the stock item component
    this.stock.favorite = !this.stock.favorite;
  }

  changeStockObject() {
    // This will update the value in the stock item component
    // Because we are creating a new reference for the stock input
    this.stock = new Stock('Test Stock Company - ' + this.counter++,
        'TSC', 85, 80);
  }

  changeStockPrice() {
    // This will not update the value in the stock item component
    // because it is changing the same reference and angular will
    // not check for it in the OnPush change detection strategy.
    this.stock.price += 10;
  }
}

El archivo app.component.ts es el que ha experimentado más cambios. El código anterior también está bien anotado con comentarios para explicar el comportamiento esperado cuando se activa cada una de estas funciones. Hemos añadido dos nuevos métodos: changeStockObject(), que crea una nueva instancia del objeto stock en AppComponent, y changeStockPrice(), que modifica los precios del objeto stock en AppComponent. También hemos añadido un contador sólo para llevar la cuenta de cuántas veces creamos un nuevo objeto de stock, pero no es estrictamente necesario.

Ahora, cuando ejecutes esta aplicación, deberías esperar ver el siguiente comportamiento:

  • Hacer clic en Añadir a Favoritos dentro de StockItemComponent sigue funcionando como se esperaba.

  • Si haces clic en Cambiar precio dentro de StockItemComponent, el precio de la acción aumentará 5 $ cada vez.

  • Si haces clic en Cambiar acción fuera de StockItemComponent, cambiará el nombre de la acción con cada clic. (¡Por eso hemos añadido el contador!)

  • Hacer clic en Cambiar precio fuera de StockItemComponent no tendrá ningún impacto (aunque el valor real de la acción saltará si haces clic en Cambiar precio dentro después de esto). Esto demuestra que el modelo se está actualizando, pero Angular no está actualizando la vista.

También deberías volver a cambiar ChangeDetectionStrategy a predeterminado para ver la diferencia en acción.

Ciclo de vida de los componentes

Los componentes (y directivas) en Angular tienen su propio ciclo de vida, desde la creación, renderización, modificación hasta la destrucción. Este ciclo de vida se ejecuta en orden de recorrido de árbol preordenado, de arriba a abajo. Cuando Angular renderiza un componente, inicia el ciclo de vida de cada uno de sus hijos, y así sucesivamente hasta que se renderiza toda la aplicación.

Hay ocasiones en que estos eventos del ciclo de vida nos resultan útiles para desarrollar nuestra aplicación, por lo que Angular proporciona ganchos en este ciclo de vida para que podamos observarlos y reaccionar según sea necesario. La Figura 4-1 muestra los ganchos del ciclo de vida de un componente, en el orden en que se invocan.

Angular Component Lifecycle
Figura 4-1. Ganchos del ciclo de vida de los componentes de Angular (original de https://angular.io/guide/lifecycle-hooks)

Angular llamará primero al constructor de cualquier componente y, a continuación, a los distintos pasos mencionados anteriormente en orden. Algunos de ellos, como los ganchos OnInit y AfterContentInit (básicamente, cualquier gancho del ciclo de vida que termine en Init) sólo se llaman una vez, cuando se inicializa un componente, mientras que los demás se llaman cada vez que cambia algún contenido. El gancho OnDestroy también se llama una sola vez para un componente.

Cada uno de estos pasos del ciclo de vida viene con una interfaz que debe implementarse cuando un componente se preocupa por ese ciclo de vida en concreto, y cada interfaz proporciona una función que empieza por ng y que debe implementarse. Por ejemplo, el paso del ciclo de vida OnInit necesita que se implemente en el componente una función llamada ngOnInit y así sucesivamente.

Recorreremos aquí cada uno de los pasos del ciclo de vida, y luego utilizaremos un ejemplo para ver todo esto en acción y el orden de los pasos del ciclo de vida dentro de un componente y entre componentes.

También hay un concepto más que aprender, que tocaremos brevemente en este capítulo y al que volveremos más adelante con más detalle: el concepto de ViewChildren y ContentChildren.

ViewChildren es cualquier componente hijo cuyas etiquetas/selectores (en su mayoría elementos, ya que ésa es la recomendación para los componentes) aparezcan dentro de la plantilla del componente. Así, en nuestro caso, app-stock-item sería un ViewChild del AppComponent.

ContentChildren es cualquier componente hijo que se proyecta en la vista del componente, pero que no se incluye directamente en la plantilla dentro del componente. Imagina algo como un carrusel, donde la funcionalidad está encapsulada en el componente, pero la vista, que podrían ser imágenes o páginas de un libro, proviene del usuario del componente. Esto se consigue generalmente a través de ContentChildren. Trataremos esto con más profundidad más adelante en este capítulo.

Interfaces y funciones

La Tabla 4-1 muestra las interfaces y funciones en el orden en que son llamadas, junto con detalles específicos sobre el paso si hay algo que destacar. Ten en cuenta que sólo cubrimos los pasos del ciclo de vida específicos de un componente, y que son ligeramente diferentes del ciclo de vida de una directiva.

Tabla 4-1. Ganchos y métodos del ciclo de vida de Angular
Interfaz Método Aplicable a Propósito

OnChanges

ngOnChanges(changes: SimpleChange)

Componentes y directivas

ngOnChanges se llama justo después del constructor para establecer y después cada vez que cambian las propiedades de entrada a una directiva. Se llama antes del método ngOnInit.

OnInit

ngOnInit()

Componentes y directivas

Este es tu típico gancho de inicialización, que te permite realizar cualquier inicialización única específica de tu componente o directiva. Este es el lugar ideal para cargar datos del servidor, etc., en lugar del constructor, tanto para la separación de preocupaciones como para la comprobabilidad.

DoCheck

ngDoCheck()

Componentes y directivas

DoCheck es la forma que tiene Angular de dar al componente una forma de comprobar si hay algún enlace o cambio que Angular no pueda o no deba detectar por sí mismo. Esta es una de las formas que podemos utilizar para notificar a Angular un cambio en el componente, cuando anulamos el ChangeDetectionStrategy predeterminado para un componente de Default a OnPush.

After​Con⁠tent​Init

ngAfterContent​Init()

Sólo componentes

Como ya se ha dicho, el gancho AfterContentInit se activa durante los casos de proyección del componente, y sólo una vez durante la inicialización del componente. Si no hay proyección, se activa inmediatamente.

After​Con⁠tent​Checked

ngAfterContent​Checked()

Sólo componentes

AfterContentChecked se dispara cada vez que se ejecuta el ciclo de detección de cambios de Angular, y en caso de que sea de inicialización, se dispara justo después del hook AfterContentInit.

AfterView​Init

ngAfterView​Init()

Sólo componentes

AfterViewInit es el complemento de AfterContent​Init, y se activa después de que todos los componentes hijos que se utilizan directamente en la plantilla del componente hayan terminado de inicializarse y sus vistas se hayan actualizado con enlaces. Esto no significa necesariamente que las vistas se rendericen en el navegador, sino que Angular ha terminado de actualizar sus vistas internas para renderizarlas lo antes posible. AfterViewInit se activa sólo una vez durante la carga del componente.

AfterViewChecked

ngAfterViewChecked()

Sólo componentes

AfterViewChecked se activa cada vez que se han comprobado y actualizado todos los componentes hijos. De nuevo, una buena forma de pensar en esto y en AfterContent​Checked es como un recorrido en profundidad por el árbol, en el sentido de que se ejecutará sólo después de que todos los ganchos AfterViewChecked de los componentes hijos hayan terminado de ejecutarse.

OnDestroy

ngOnDestroy()

Componentes y directivas

El gancho OnDestroy se activa cuando un componente está a punto de ser destruido y eliminado de la interfaz de usuario. Es un buen lugar para hacer todas las limpiezas, como dar de baja a los oyentes que hayas inicializado y cosas similares. En general, es una buena práctica limpiar todo lo que hayas registrado (temporizadores, observables, etc.) como parte del componente.

Intentemos añadir todos estos ganchos a nuestra aplicación existente para ver el orden de ejecución en un escenario del mundo real. Añadiremos todos estos ganchos tanto a nuestro AppComponent como al StockItemComponent, con un simple console.log para ver simplemente cuándo y cómo se ejecutan estas funciones. Utilizaremos la base del ejemplo de salida para construir a partir de ella, así que en caso de que no estés codificando, puedes tomar el ejemplo del capítulo4/componente-salida para construir a partir de ahí.

El ejemplo final terminado también está disponible en el capítulo4/componente-ciclo-de-vida.

En primer lugar, podemos modificar el archivo src/app/app.component.ts y añadir los ganchos como se indica a continuación:

import { Component, SimpleChanges, OnInit, OnChanges, OnDestroy,
         DoCheck, AfterViewChecked, AfterViewInit, AfterContentChecked,
         AfterContentInit } from '@angular/core';
import { Stock } from 'app/model/stock';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnChanges, OnDestroy,
                                     DoCheck, AfterContentChecked,
                                     AfterContentInit, AfterViewChecked,
                                     AfterViewInit {
  title = 'app works!';

  public stock: Stock;

  onToggleFavorite(stock: Stock) {
    console.log('Favorite for stock ', stock, ' was triggered');
    this.stock.favorite = !this.stock.favorite;
  }

  ngOnInit(): void {
    this.stock = new Stock('Test Stock Company', 'TSC', 85, 80);
    console.log('App Component - On Init');
  }

  ngAfterViewInit(): void {
    console.log('App Component - After View Init');
  }
  ngAfterViewChecked(): void {
    console.log('App Component - After View Checked');
  }
  ngAfterContentInit(): void {
    console.log('App Component - After Content Init');
  }
  ngAfterContentChecked(): void {
    console.log('App Component - After Content Checked');
  }
  ngDoCheck(): void {
    console.log('App Component - Do Check');
  }
  ngOnDestroy(): void {
    console.log('App Component - On Destroy');
  }
  ngOnChanges(changes: SimpleChanges): void {
    console.log('App Component - On Changes - ', changes);
  }
}

Puedes ver que hemos implementado las interfaces para OnInit, OnChanges, OnDestroy, DoCheck, AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit en la clase AppComponent, y luego hemos seguido adelante e implementado las funciones respectivas. Cada uno de los métodos simplemente imprime una declaración de registro mencionando el nombre del componente y el nombre del método de activación.

Del mismo modo, podemos hacer lo mismo para la StockItemComponent:

import { Component, SimpleChanges, OnInit,
         OnChanges, OnDestroy, DoCheck, AfterViewChecked,
         AfterViewInit, AfterContentChecked,
         AfterContentInit, Input,
         Output, EventEmitter } from '@angular/core';
import { Stock } from '../../model/stock';

@Component({
  selector: 'app-stock-item',
  templateUrl: './stock-item.component.html',
  styleUrls: ['./stock-item.component.css']
})
export class StockItemComponent implements OnInit, OnChanges,
                                           OnDestroy, DoCheck,
                                           AfterContentChecked,
                                           AfterContentInit,
                                           AfterViewChecked,
                                           AfterViewInit {

  @Input() public stock: Stock;
  @Output() private toggleFavorite: EventEmitter<Stock>;

  constructor() {
    this.toggleFavorite = new EventEmitter<Stock>();
   }

  onToggleFavorite(event) {
    this.toggleFavorite.emit(this.stock);
  }

  ngOnInit(): void {
    console.log('Stock Item Component - On Init');
  }
  ngAfterViewInit(): void {
    console.log('Stock Item Component - After View Init');
  }
  ngAfterViewChecked(): void {
    console.log('Stock Item Component - After View Checked');
  }
  ngAfterContentInit(): void {
    console.log('Stock Item Component - After Content Init');
  }
  ngAfterContentChecked(): void {
    console.log('Stock Item Component - After Content Checked');
  }
  ngDoCheck(): void {
    console.log('Stock Item Component - Do Check');
  }
  ngOnDestroy(): void {
    console.log('Stock Item Component - On Destroy');
  }
  ngOnChanges(changes: SimpleChanges): void {
    console.log('Stock Item Component - On Changes - ', changes);
  }
}

Hemos hecho exactamente lo mismo que hicimos en AppComponent con el Stock​ItemComponent. Ahora, podemos ejecutar esta aplicación para verla en acción.

Cuando lo ejecutes, abre la consola de JavaScript en el navegador. Deberías ver, por orden de ejecución

  1. En primer lugar, se crea el AppComponent. A continuación, se activan los siguientes ganchos en AppComponent:

    • On Init

    • Do Check

    • After Content Init

    • After Content Checked

    Las dos anteriores se ejecutan inmediatamente porque hasta ahora no tenemos ninguna proyección de contenido en nuestra aplicación.

  2. A continuación, se ejecuta el StockItemComponent OnChanges, reconociéndose como cambio la entrada al Stock​ItemComponent, seguida de los ganchos enumerados aquí dentro del StockItemComponent:

    • On Init

    • Do Check

    • After Content Init

    • After Content Checked

    • After View Init

    • After View Checked

  3. Por último, no hay más subcomponentes que recorrer hacia abajo, por lo que Angular retrocede hasta el padre AppComponent, y ejecuta lo siguiente:

    • After View Init

    • After View Checked

Esto nos da una buena visión de cómo y en qué orden se inicializa Angular y del recorrido por el árbol que hace en secreto. Estos ganchos resultan muy útiles para ciertas lógicas de inicialización más complicadas, y son esenciales para la limpieza una vez que el componente está terminado, para evitar fugas de memoria.

Ver proyección

Lo último que cubriremos en este capítulo es el concepto de proyección de vistas. La proyección es una idea importante en Angular, ya que nos da más flexibilidad a la hora de desarrollar nuestros componentes y, de nuevo, nos proporciona otra herramienta para hacerlos realmente reutilizables en diferentes contextos.

La proyección es útil cuando queremos construir componentes pero establecer que algunas partes de la IU del componente no formen parte innata de él. Por ejemplo, supongamos que estamos construyendo un componente para un carrusel. Un carrusel tiene unas cuantas capacidades sencillas: es capaz de mostrar un elemento y permitirnos navegar al elemento siguiente/anterior. Tu componente de carrusel también puede tener otras funciones, como la carga lenta, etc. Pero algo que no es competencia del componente carrusel es el contenido que muestra. Un usuario del componente puede querer mostrar una imagen, una página de un libro o cualquier otra cosa aleatoria.

Así, en estos casos, la vista la controlaría el usuario del componente, y la funcionalidad la proporcionaría el propio componente. Éste no es más que un caso de uso en el que podríamos querer utilizar la proyección en nuestros componentes.

Veamos cómo podemos utilizar la proyección de contenido en nuestra aplicación Angular. Utilizaremos la base del ejemplo de entrada para construir a partir de ella, así que en caso de que no estés codificando, puedes tomar el ejemplo del capítulo4/component-input para construir a partir de ahí.

El ejemplo final terminado está disponible en el capítulo4/proyección de componentes.

En primer lugar, modificaremos nuestro StockItemComponent para permitir la proyección de contenido. No hay ningún cambio de código en nuestra clase componente; sólo tenemos que modificar el archivo src/app/stock/stock-item/stock-item.component.html como sigue:

<div class="stock-container">
  <div class="name">{{stock.name + ' (' + stock.code + ')'}}</div>
  <div class="price"
      [class]="stock.isPositiveChange() ? 'positive' : 'negative'">
      $ {{stock.price}}
  </div>
  <ng-content></ng-content>             1
</div>
1

El nuevo elemento ng-content para proyección

Simplemente hemos eliminado los botones que teníamos anteriormente, y vamos a dejar que el usuario del componente decida qué botones se muestran. Para ello, hemos sustituido los botones por un elemento ng-content. No es necesario realizar ningún otro cambio en el componente.

A continuación, haremos un cambio en AppComponent, simplemente para añadir un método con fines de prueba. Modifica el archivo src/app/app.component.ts como sigue:

/** Imports and decorators skipped for brevity **/

export class AppComponent implements OnInit {
  /** Constructor and OnInit skipped for brevity **/

  testMethod() {
    console.log('Test method in AppComponent triggered');
  }
}

Simplemente hemos añadido un método que registrará en la consola cuando se active. Con esto en su sitio, veamos ahora cómo podemos utilizar nuestro StockItemComponent actualizado y utilizar el poder de la proyección. Modifica el archivo app.component. html como sigue:

<h1>
  {{title}}
</h1>
<app-stock-item [stock]="stockObj">
  <button (click)="testMethod()">With Button 1</button>
</app-stock-item>

<app-stock-item [stock]="stockObj">
  No buttons for you!!
</app-stock-item>

Hemos añadido dos instancias del componente app-stock-item en nuestro HTML. Y ambos tienen ahora algo de contenido en su interior, a diferencia de lo que ocurría anteriormente, cuando estos elementos no tenían contenido. En uno, tenemos un botón que activa el testMethod que añadimos en el AppComponent, y el otro simplemente tiene contenido de texto.

Cuando ejecutemos nuestra aplicación Angular y la abramos en el navegador, deberíamos ver algo como la Figura 4-2.

Observa que los dos componentes de elementos de acciones de nuestro navegador, cada uno con un contenido ligeramente distinto, se basan en lo que hemos proporcionado. Si haces clic en el botón del primer widget de acciones, verás que se llama al método de AppComponent y se activa console.log.

Así, los usuarios del componente tienen ahora la capacidad de cambiar parte de la interfaz de usuario del componente según les convenga. Incluso podemos acceder también a la funcionalidad del componente padre, lo que lo hace realmente flexible. También es posible proyectar múltiples secciones y contenidos diferentes en nuestro componente hijo. Aunque la documentación oficial de Angular es parca en este tema, hay un gran artículo que puede darte más información sobre la proyección de contenido.

Angular Content Projection
Figura 4-2. Aplicación angular con proyección de vista

Conclusión

En este capítulo, profundizamos mucho más en los componentes y vimos algunos de los atributos más utilizados al crear componentes. Echamos un vistazo detallado al decorador Component, hablando de atributos como template frente a template​Url, estilos, y también cubrimos a alto nivel cómo funciona la detección de cambios de Angular y cómo podemos anularla.

Luego cubrimos el ciclo de vida de un componente, así como los ganchos que Angular nos proporciona para engancharnos a algunos de estos eventos del ciclo de vida y reaccionar ante ellos. Por último, cubrimos la proyección en componentes y cómo podemos hacer algunos componentes realmente potentes que permitan al usuario del componente decidir partes de la IU.

En el próximo capítulo, daremos un pequeño rodeo para entender las pruebas unitarias de componentes, y veremos cómo podemos probar tanto la lógica que controla el componente como la vista que se muestra.

Ejercicio

Para nuestro tercer ejercicio, podemos basarnos en el ejercicio anterior(capítulo3/ejercicio) incluyendo conceptos de este capítulo:

  1. Crea un ProductListComponent. Inicializa allí una matriz de productos, en lugar de inicializar un único producto en el ProductComponent. Cambia su plantilla para utilizar NgFor y crear un ProductItemComponent para cada producto.

  2. Utiliza plantillas y estilos en línea en ProductListComponent. Genéralo utilizando la CLI de Angular con esa configuración en lugar de generarlo y cambiarlo manualmente.

  3. Cambia el ProductItemComponent para que tome el producto como entrada.

  4. Mueve la lógica de aumento/disminución de ProductItem a ProductListComponent. Utiliza un índice o ID de producto para encontrar el producto y cambiar su cantidad.

  5. Mueve el ProductItemComponent para que sea óptimo y pasa del ChangeDetectionStrategy por defecto a un OnPush ChangeDetectionStrategy .

Todo esto puede lograrse utilizando los conceptos tratados en este capítulo. Puedes ver la solución terminada en el capítulo4/ejercicio/ecommerce.

Get Angular: En marcha now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.