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 atributopreserveWhitespaces
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 atributointerpolation
, 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, proporcionandointerpolation: ['<<', '>>']
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 decoradorComponent
. 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 deChangeDetectionStrategy.Default
aChangeDetectionStrategy.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 atributodeclarations
, 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 elEventEmitter
de la biblioteca Angular. -
Creamos un nuevo miembro de la clase llamado
toggleFavorite
de tipoEventEmitter
, y renombramos nuestro método aonToggleFavorite
. ElEventEmitter
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 delEventEmitter
para emitir el objeto acción completo. Esto significa que todos los oyentes del eventotoggleFavorite
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 decompositeObj
(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 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.
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 StockItemComponent
. 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
-
En primer lugar, se crea el
AppComponent
. A continuación, se activan los siguientes ganchos enAppComponent
:-
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.
-
-
A continuación, se ejecuta el
StockItemComponent OnChanges
, reconociéndose como cambio la entrada alStockItemComponent
, seguida de los ganchos enumerados aquí dentro delStockItemComponent
:-
On Init
-
Do Check
-
After Content Init
-
After Content Checked
-
After View Init
-
After View Checked
-
-
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>
</div>
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.
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 templateUrl
, 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:
-
Crea un
ProductListComponent
. Inicializa allí una matriz de productos, en lugar de inicializar un único producto en elProductComponent
. Cambia su plantilla para utilizarNgFor
y crear unProductItemComponent
para cada producto. -
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. -
Cambia el
ProductItemComponent
para que tome el producto como entrada. -
Mueve la lógica de aumento/disminución de
ProductItem
aProductListComponent
. Utiliza un índice o ID de producto para encontrar el producto y cambiar su cantidad. -
Mueve el
ProductItemComponent
para que sea óptimo y pasa delChangeDetectionStrategy
por defecto a unOnPush
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.