Anidar Componentes

<< Pipes (filtros) Directivas >>

Ciclo de vida de los componentes

Hay varias etapas por las que pasa un componente durante su ciclo de vida (creado → destruido). Angular nos permite establecer ciertos comportamientos en cada una de dichas etapas, creando métodos en el componente que se ejecutan cuando se alcanza cada una.

Ciclo de vida de los componentes

  • ngOnInit → Se ejecuta una vez cuando el componente está listo. Se utiliza para realizar operaciones iniciales como obtener datos del servidor, ya que se recomienda no usar el constructos nada más que para inicializaciones simples de valores. Si el componente tiene parámetros de entrada (@Input), será cuando estén disponibles por primera vez (en el constructor no lo están).
  • ngAfterContentInit → Se ejecuta (después de ngOnInit) una vez se haya incluido el contenido proyectado externamente dentro del componente. Normalmente dentro de elementos ng-content. Si usas los decoradores @ContentChild, o @ContentChildren, será cuando las referencias a dichos elementos estarán disponibles. Más información: Proyección de contenido en componentes
  • ngAfterViewInit → Se ejecuta (después de ngAfterContentInit) una vez justo después de ngOnInit. Se ejecutan cuando cuando la vista (plantilla) del componente y de los componentes hijos están listos para mostrarse por primera vez. Si se usan los decoradores @ViewChild o @ViewChildren en el componente. Este evento indica que ya están disponibles esas referencias a elementos internos de la plantilla para trabajar con ellas.
  • ngOnChanges → Se ejecuta cada vez que una propiedad enlazada a la vista cambia. El método asociado recibe un objeto de tipo SimpleChanges con los nuevos valores y los anteriores. Se ejecuta con bastante frecuencia (aunque depende de la estrategia de detección de cambios elegida), así que cuidado con poner código costoso de computar.
  • ngDoCheck → Este método se utiliza para detectar cambios no detectados automáticamente por Angular. Es decir, hacer comprobaciones extra. Se ejecuta justo después de ngOnChanges, es decir, a menudo.
  • ngAfterContentChecked y ngAfterViewChecked → Se ejecutan después de cada ngDoCheck, en ese orden. Normalmente no se implementan.
  • ngOnDestroy → Se llama una vez cuando el componente se destruye (se elimina del DOM). Utiliza este método si necesitas limpiar algunos datos, o eliminar subscripciones a eventos, observables, etc.

Más información: Ciclo de vida de los componentes

Para implementar estos métodos asociados al ciclo de vida, primero debemos implementar la interfaz correspondiente en el componente, que se llama igual que el método pero sin el prefijo 'ng'. Veamos un ejemplo muy sencillo utilizando ngOnInit:

Podrás observar como si imprime el mensaje en la consola al cargar la aplicación, ya que es cuando se crea el componente.

Componentes anidados

Los componentes se pueden anidar. Esto implica utilizar el selector de un componente dentro de la plantilla de otro componente. Angular detectará este selector y cargará la plantilla del componente dentro del selector. Es importante saber que antes de utilizar el selector de otro componente en la plantilla, se debe añadir al array de imports del componente padre.

Hay que tener cuidado de no anidar, por ejemplo, un componente B dentro de la plantilla del componente A y viceversa, ya que formaríamos un bucle recursivo y el correspondiente error.

La finalidad de esto es la de dividir nuestra aplicación en fragmentos o componentes reutilizables, tanto en otras páginas de la misma aplicación, como en otras aplicaciónes. Cada componente encapsula su plantilla (HTML), el código que la gestiona y los estilos asociados.

Para nuestro ejemplo de productos, vamos a crear el componente product-item, que representará cada producto que vamos a mostrar en la vista, y star-rating, que usaremos para valorar los productos con una puntuación del 1 al 5.

Componentes anidados

Creando el componente product-item

Primero vamos a crear el componente:

ng g c product-item

Ahora vamos a implementar el componente. La plantilla del mismo tendrá el fragmento correspondiente a un producto (lo que había dentro del @for que recorre el array de productos). Hay que trasladar también el CSS específico del componente padre al componente hijo (si no, no se aplicarán los estilos).

Por ahora usaremos datos estáticos, y en breve veremos como pasar la información del producto del componente padre al hijo.

Para que se muestre este nuevo componente, lo importaremos dentro de products-page y lo incluiremos en su plantilla, sustituyendo las filas de la tabla por este nuevo componente anidado.

Veremos como se inserta la plantilla del nuevo componente en la página. Sin embargo, el resultado no será el deseado:

Componentes anidados

El problema, en este caso en particular, es que el navegador espera que el elemento tr esté directamente dentro de tbody, por lo que al crear un nodo extra entre ambos, hace que no reconozca la fila como parte de la tabla. Tenemos 2 posibles soluciones para esto.

Componentes anidados

Solución 1: Cambiar selector componente

Por defecto el selector del componente implica que debemos crear una etiqueta con ese nombre en la plantilla. Este selector funciona como los selectores CSS, por lo que podríamos cambiar para que reconozca una clase o un atributo en lugar de un elemento (también podríamos hacer un selector más complejo). Angular solo renderiza la plantilla del componente dentro del elemento que cumpla con las condiciones del selector.

En nuestro caso, vamos a indicar que el componente se renderiza dentro de un elemento tr que tenga un atributo llamado product-item (debemos quitar el elemento tr de la plantilla del componente hijo y pasarla al padre):

Después adaptamos la plantilla del componente products-page para reflejar el cambio en el selector:

Esta es una forma de solucionar el problema.

Componentes anidados

Componentes anidados

Solución 2: Cambiar estructura HTML

Otra opción es usar elementos genéricos como div para estructurar el contenido y crear un sistema de filas y columnas como el de una tabla, utilizando flex o grid. En este caso usaremos el sistema de filas y columnas de Bootstrap que internamente utiliza CSS flex.

Así quedaría el componente padre (products-page):

Y así quedaría el componente hijo (product-item)

El resultado sería muy similar a lo que teníamos con la tabla.

Componentes anidados

Creando el componente star-rating

Vamos a crear un componente para puntuar los productos con un sistema de 5 estrellas. Este sistema de puntuación será un componente aislado ya que podríamos reutilizarlo para puntuar cualquier cosa (usuarios, comentarios, etc.).

ng g c star-rating

Para representar las estrellas podríamos utilizar los caracteres unicode ☆ y ★. Sin embargo, para este ejemplo, vamos a utilizar los iconos de Font Awesome . Lo primero que haremos será instalar esta dependencia en nuestro proyecto:

npm install @fortawesome/fontawesome-free

Posteriormente, añadiremos el CSS de la librería a nuestra aplicación:

Por ahora, en el componente star-rating, solo añadiremos una propiedad que indique una puntuación entre 1 y 5. El valor será estático, pero más adelante añadiremos la posibilidad de pasarle la puntuación asignada al producto, así como la posibilidad de cambiar dicha puntuación.

En la plantilla generaremos 5 estrellas usando la estructura @for para recorrer un array con los números del 1 al 5. Después comprobaremos si el número de cada estrella es inferior o igual al de la puntuación para ponerla rellena (clase fa-solid), o si es superior para ponerla vacía (clase fa-regular).

Vamos a actualizar el componente product-item para que importe el componente star-rating y lo pondremos en su plantilla, añadiendo una columna al final con la puntuación (más adelante le pasaremos la puntuación real del producto).

Por último, añadimos la cabecera para la puntuación en el componente principal products-page.

El resultado será satisfactorio a nivel visual, pero los datos no reflejan lo que hay en el array de productos, por lo que a continuación veremos como enviar datos del componente padre al componente anidado.

Componentes anidados

Parámetros de entrada (@Input)

En la sección anterior, aprendimos cómo anidar componentes. Sin embargo, todavía no tenemos la funcionalidad necesaria (mostrar el producto correcto, ocultar/mostrar la imagen, mostrar la puntuación de estrellas correcta) ya que aún no sabemos cómo se comunican los componentes.

Pasarle el producto a product-item

Para indicar que un componente recibe datos de entrada de un componente padre, utilizamos el decorador @Input antes de la declaración de la propiedad. Esto le dice a Angular que esta propiedad está vinculada a un atributo con el mismo nombre en la plantilla. Este atributo debe colocarse en el elemento del selector del componente (en la plantilla del componente padre). Veamos un ejemplo de cómo pasar el producto que deseamos mostrar y el valor booleano showImage al componente product-item.

En el decorador @Input se pueden pasar opciones como required, que marca si el parámetro es obligatorio o no. En el elemento padre le pasamos los valores creando atributos en el selector que coincidan con el nombre de las propiedades que acabamos de crear en la clase. Los corchetes se utilizan como ya sabemos, para vincular el valor a una propiedad contenida en la clase padre.

Con esto ya deberíamos ver los productos del array en el listado. Y el botón para ocultar las imágenes también funcionará.

Componentes anidados

Pasarle la puntuación a star-rating

Vamos a hacer lo mismo pasándole la puntuación del producto, desde el componente product-item a star-rating.

Ahora, cada producto aparece con su puntuación establecida en el objeto dentro del array original de products-page.

Componentes anidados

Eventos de salida (@Output)

En primer lugar, queremos que cuando pasemos el puntero del ratón sobre una estrella, se establezca una calificación temporal que corresponda a la posición de esa estrella. Creamos una propiedad auxiliar privada llamada auxRating y la inicializamos con el mismo valor que recibimos del padre (en el método ngOnInit, ya que en el constructor los valores de las propiedades @Input no están accesibles todavía).

También crearemos un método (restoreRating) para restaurar auxRating a la puntuación actual del producto cuando saquemos el ratón del elemento que contiene las estrellas (prueba a pasar el ratón por encima de las estrellas):

Para notificar al componente padre cuando se produce algún cambio o evento, tenemos el decorador @Output() que se utiliza en combinación con un objeto de tipo EventEmitter. Este objeto emite valores al padre y este los recoge en forma de evento.

Vamos a hacer que cuando hagamos clic en una estrella (changeRating), se le pase al componente padre la puntuación elegida. Crearemos la propiedad de salida ratingChanged que emitirá la puntuación, y el componente product-item actualizará la puntuación del producto. Cambiar el valor del @Input no sería correcto, ya que no cambiaría en el producto original que está en el componente product-item, por lo que tenemos que avisar a dicho componente padre mediante este parámetro de salida para que lo edite.

Al editar la puntuación del producto en product-item automáticamente actualizará el valor del @Input recibido en star-rating. Modificar la propiedad de un objeto que se recibe por parámetro (en product-item) también afecta fuera, es decir al objeto que hay en el array de products-page, al igual que si le pasamos el objeto a una función y esta modifica una propiedad dentro. Esto no pasa con la puntuación, ya que es un valor primitivo (número).

En el componente padre, product-item, pondremos el parámetro entre paréntesis (evento). El valor especial $event contiene lo que el evento emite, es decir, la nueva puntuación del producto.

Creando el componente product-form

Vamos a mover también el formulario de añadir producto a un componente aparte. De esta manera dividimos más la lógica de la aplicación. Y además, en el futuro, cuando veamos como funciona el router de Angular, este componente del formulario lo moveremos a una página diferente de la aplicación.

ng g c product-form

A este componente, movemos el HTML del formulario para añadir un nuevo producto, así como toda la lógica relacionada con dicho formulario. La principal diferencia es que al añadir producto, este se envía al padre (products-page) mediante un EventEmitter llamado add, para que este lo añada a la lista de productos.

En el componente products-page insertamos el nuevo componente (previa importación) en la plantilla donde estaba el formulario. Escucharemos el evento add y con $event obtenemos el producto que ha producido el formulario y que debemos añadir a la lista. El componente en general quedará mucho más limpio, limitándose a gestionar el listado de productos.

Angular DevTools

Si quieres inspeccionar los cambios en los valores que contienen o reciben los componentes, Angular DevTools es una extensión para el navegador que ayuda bastante con eso.

Angular DevTools para Chrome

Angular DevTools para Firefox

Documentación oficial Angular DevTools

<< Pipes (filtros) Directivas >>