Componentes

<< Crear un proyecto Pipes (filtros) >>

En Angular, una aplicación se divide en componentes. Un componente representa una página o una porción de una página, de nuestra aplicación web. Engloba el HTML de dicho apartado (plantilla), el CSS específico que solo afecta a dicho HTML, y una clase que gestiona dicha plantilla.

Angular Componentes

Cuando creamos el proyecto, se generó el componente AppComponent (src/app/app.component.ts). El código debería ser similar al siguiente:

Un componente es una clase que tiene un decorador @Component y unos parámetros de configuración dentro del mismo:

  • selector → Este es el nombre de etiqueta que se usará en la vista (HTML) donde se insertará la plantilla de este componente. Dado que HTML no distingue entre mayúsculas y minúsculas, se utiliza '-' para separar palabras. Observa src/index.html y verás la etiqueta <app-root>. Cuando Angular se carga, reemplazará el contenido de esa etiqueta con la plantilla del componente AppComponent.
    • Nota: Los selectores personalizados son ignorados por defecto por el navegador (pero no su contenido), sin embargo, se les pueden aplicar estilos CSS.
    • Por defecto, cada componente se crea con un selector prefijado con "app". Puedes cambiar este prefijo (o eliminarlo) al crear el proyecto. Ejemplo: ng new my-project --prefix mp. Pero también puedes hacerlo más tarde en angular.json (busca el atributo "prefix").
  • standalone → Si se establece a true , este componente no depende de ningún módulo y puede ser importado y utilizado en cualquier parte de nuestra aplicación.
  • imports → Array de componentes (standalone), directivas o módulos que el componente importará para poder usar en su plantilla.
  • templateUrl → Este es el archivo donde se encuentra la vista controlada por este componente. Contendrá código HTML (plantilla) que se insertará dentro del selector del componente cuando se cargue.
  • styleUrls → Angular te permite asignar uno o más archivos CSS al componente. Estos estilos se aplicarán solo a la plantilla del componente. Esto nos permite crear componentes modulares y reutilizables con su propio estilo. ◦ Nota: Los estilos generales para toda la aplicación pueden incluirse en el archivo src/styles.css.

Además, la clase ha sido creada con una propiedad llamada "title". En la plantilla (que es un ejemplo que puedes eliminar completamente) busca este código:

Esto se llama interpolación, como veremos en la siguiente sección. Cuando el componente se carga, cambiará "{{title}}" por el texto "angular-products". Cambia la propiedad "title" en la clase del componente para observar como cambia su valor en el HTML generado.

En lugar de utilizar un template externo (archivo HTML), podríamos definir el template en el decorador del componente como una cadena utilizando "template" en lugar de "templateUrl". Lo mismo se puede aplicar al CSS si usamos "styles" en lugar de "styleUrls".

Prefijo de los selectores

Por defecto, todos los selectores de componentes (y directivas) comienzan con el prefijo 'app'. Esto se puede establecer al crear un proyecto con la opción -p, o --prefix. Si vamos a crear una librería de componentes reutilizables, es mejor elegir un prefijo apropiado relacionado con nuestros componentes. Si los componentes son solo para la aplicación que estamos construyendo, podemos eliminar el prefijo. Para actualizar o eliminar el prefijo, en el archivo angular.json vamos a establecer un prefijo vacío para los selectores de los componentes.

Vamos a editar también el archivo .eslintrc.json (si hemos instalado ESLint) y quitar el prefijo app ahí también. Por defecto obliga que los componentes y directivas tengan este prefijo en el selector.

Configurar estilo global: Bootstrap

Para añadir algo de estilo a la aplicación, vamos a instalar Bootstrap e incluir el CSS de dicha librería en nuestro proyecto de forma global.

npm i bootstrap

Ahora tenemos 2 opciones para importar el CSS de Boostrap en nuestra aplicacion. La primera sería desde el archivo src/styles.css, usando la instrucción @import de CSS:

Otra forma sería incluyendo el archivo CSS en el array styles dentro del archivo de configuración angular.json:

Creando un nuevo componente

Para crear un nuevo componente ejecutamos ng generate component nombre-component, o simplemente ng g c nombre-componente. Algunas opciones que podemos añadir son:

  • --inline-template → En lugar de crear un archivo HTML separado, insertará la plantilla en la cadena template del componente.
  • --inline-style → Igual que la anterior pero para el archivo CSS.
  • --flat → No crea un directorio para el componente, simplemente los archivos. Esta opción tiene más sentido en combinación con las opciones --inline-template y --inline-style, ya que solo nos creará un archivo.

Vamos a crear un componente donde mostraremos un listado de productos. Le pondremos el sufijo Page en el nombre, porque más adelante, cuando añadamos enrutamiento a la aplicación, este componente representará una de las múltiples páginas que contendrá la aplicación.

ng g c products-page

Como podremos observar, dentro del directorio app, se habrá creado la carpeta products-page con los archivos del nuevo componente. La clase del componente (products-page.component.ts) tendrá un aspecto similar a este:

En el archivo de la plantilla (products-page.component.html), vamos a añadir el esqueleto de una tabla donde añadiremos los productos que vamos a mostrar.

Plantilla

Al estilo de lo que sería un framework de tipo Modelo-Vista-Controlador (MVC), se puede considerar la clase del componente como el controlador, y el HTML o plantilla asociada como la vista. La clase del componente se encarga de manipular e interactual con la plantilla. Una forma de hacerlo es mediante interpolación y vinculación de datos (data-binding).

Selector de componente

Vamos a editar la plantilla de AppComponent (app.component.html) para establecer el HTML principal de nuestra aplicación. Recordemos que la plantilla de AppComponent se carga directamente dentro del elemento body de index.html. En esta plantilla usaremos el selector del componente que acabamos de crear (products-page) para insertar su plantilla ahí.

Antes de incluir el selector del componente ProductsPageComponent, debemos importarlo en la clase AppComponent.

Si ejecutamos la aplicación (ng serve) veremos este resultado en pantalla:

Angular Products

Interpolación

Como vimos en la sección de "Componentes", podemos mostrar las propiedades de nuestra clase de componente utilizando interpolación {{doble llave}} en la plantilla, y serán reemplazadas por el valor de esa propiedad.

Al realizar interpolación, podemos imprimir cualquier valor: concatenar valores, propiedades, llamar a métodos (de la clase del componente), cálculos matemáticos, etc.:

{{ title }}
{{ "Title: " + title }}
{{ "Title: " + getTitle() }}
{{ 4 * 54 + 6 / 2 }}

Vamos a poner los encabezados de la tabla en propiedades del componente products-page, y posteriormente las vincularemos en la plantilla:

Control de flujo: @if

Se pueden añadir estructuras de control en la plantilla de un componente como condicionales o bucles. Desde la versión 17 de Angular se han introducido nuevas instrucciones que empiezan por el carácter '@', que sustituyen a las directivas que se venían usando (se pueden seguir utilizando).

La instrucción @if sustituye a la directiva *ngIf. Todo lo que pongamos entre las llaves se añadirá al DOM en el momento en el cual la condición sea verdadera. Si la condición fuera falsa, se eliminan del DOM, es decir, no es que estén ocultos.

También se puede crear un bloque @else asociado, e incluso @if else

Antes de utilizar la nueva sintaxis en nuestro ejemplo de productos, vamos a crear una interfaz que represente a un producto en nuestra aplicación:

ng g interface interfaces/product

A continuación crearemos un array vacío de productos en el componente products-page e indicaremos en la plantilla que la tabla solo se muestre cuando haya productos. En caso de no haber productos, se mostrará un mensaje en su lugar.

Control de flujo: @for

La instrucción @for sustituye a la directiva *ngFor. Con esta estructura recorremos los elementos de una colección, añadiendo al DOM los elementos HTML tantas veces como elementos por los cuales se itera.

En la nueva sintaxis, se puede añadir la instrucción @empty que es como @else y se muestra cuando la lista está vacía. Además, la opción track es obligatoria (antes era opcional y se llamaba trackBy). Con esta opción definimos qué propiedad define de forma única a cada elemento de la lista. Esto se hace para que Angular detecte mejor qué elementos han cambiado y solo modifique las partes del DOM precisas en cada cambio en lugar de redibujar todos los elementos (por defecto).

El bloque for también permite declarar variables locales que hagan referencia a valores implícitos que el bloque for permite acceder. Estos valores son:

  • $count → Número de elementos de la colección Number of items in a collection iterated over
  • $index → Posición del elemento actual
  • $first → Indica si el elemento actual es el primero (booleano)
  • $last → Indica si el elemento actual es el último (booleano)
  • $even → Indica si el elemento actual tiene un índice par (0,2,4,...)
  • $odd → Indica si el elemento actual tiene un índice impar (1,3,5,...)

Para acceder a estos valores, debemos declarar variables con let dentro del paréntesis del @for:

Para el ejemplo de productos, vamos a introducir datos en el array de productos del componente. Después los recorreremos generando una fila de tabla por cada producto:

Aquí podemos ver el resultado y como se imprimen los productos:

Angular Products

Control de flujo: @switch

La sintaxis de la estructura @switch es muy parecida a @if. La diferencia con la estructura swtich de JavaScript y otros lenguajes es que no se necesita la instrucción break, ya que solo puede entrar en uno de los bloques. Si el valor no se encuentra en ningún bloque @case, se mostrará lo que hay en el bloque @default (opcional). Esta nueva estructura viene a sustituir a ngSwitch.

Vincular atributos (Data-binding)

La vinculación de atributos es un concepto similar al de la interpolación, pero aplicado a atributos de los elementos HTML. Se vincula el valor del atributo al de una propiedad de la clase del componente. Para esto, debemos envolver el nombre del atributo entre corchetes (ejemplo: <img [src]="product.imageUrl">).

Vamos a añadir la imagen del producto en la tabla vinculando el atributo src. Como la imagen puede ser un poco grande, vamos a añadir CSS al componente para que se vea mejor la tabla. Añade también el encabezado a la tabla y la propiedad image al objeto headers en el componente.

Angular Products

ngClass

La directiva [ngClass] sirve para asignar clases CSS a un elemento de forma dinámica. Como valor se le asigna un objecto cuyos atributos son los nombres de las clases CSS que se le pueden asignar o quitar al elemento y el valor de cada propiedad es un booleano que indica si se le asigna dicha clase o no(puede ser una expresión o llamada a un método siempre que devuelva un booleano).

El objeto que define las clases también puede estar definido en el componente de la clase. En ese caso, el valor de [ngClass] debe apuntar a dicha propiedad.

Importante: Si usamos esta metodología, no se detectan los cambios que pueda haber en los valores de la propiedades del objeto asignado. Si queremos que Angular detecte que algo ha cambiado, tendríamos que reasignar la propiedad del componente a un nuevo objeto.

ngStyle

La directiva [ngStyle] sirve para establecer estilos CSS individuales a un elemento. Es equivalente al atributo style, pero los valores de las propiedades CSS pueden ser variables en este caso.

Además, los nombres de las propiedades CSS que indican medidas pueden llevar el sufijo .px, .mm. em, etc. para indicar la unidad de medida. En este caso el valor será de tipo number directamente.

Igual que con ngClass, el objeto puede estar en una propiedad del componente, o que te lo devuelva un método al que llamemos.

Gestión de eventos (Event-binding)

Al contrario que vincular atributos, la vinculación de eventos va en dirección contraria. Es decir, son acciones sobre la plantilla (eventos) que afectan al componente, o más bien que ejecutan un método del componente. Para vincular un evento, ponemos un atributo en el elemento con el nombre del evento entre paréntesis → (click), (mouseenter), (keypress), etc. Como valor ponemos la llamada al metódo del componente que queramos ejecutar.

Los paréntesis, al contrario que los corchetes, representan que la dirección del vínculo va desde la plantilla al componente.

En el ejemplo de productos que estamos desarrollando. Vamos a poner un botón que permita ocultar o mostrar las imágenes de los productos cada vez que le hagamos click. Además, vincularemos clases CSS del botón (y el contenido) al booleano que indica si las imágenes se muestran o no.

Angular Products

Angular Products

En lugar de usar la instrucción @if, se podría haber conseguido lo mismo con [ngClass] usando la clase d-none de Bootstrap, que básicamente añade al elemento la propiedad CSS display: none.

Vinculación en 2 direcciones [(ngModel)]

La directiva [(ngModel)] se usa para vincular el valor de un campo de formulario a una propiedad del componente. Los corchetes indican que el valor del campo (value) se obtiene del valor de la propiedad de la clase, mientras que los paréntesis indican que si el usuario cambia el valor del input, este cambio se aplica también a la propiedad del componente. Por eso se dice que está vinculado en ambas direcciones.

No hace falta que los elementos tipo input, textarea, o select, estén dentro de un formulario para poder usar esta directiva. Lo único que hay que tener en cuenta, es que los elementos del formulario que usen ngModel, deben tener el atributo name. Más adelante estudiaremos la gestión de formularios con Angular, incluyendo temas como la validación de campos.

Como ejemplo, vamos a crea un formulario en la página de productos para añadir un nuevo producto. Como se puede observar, el evento de envío del formulario es ahora (ngSubmit). Entre otras cosas, este evento nos permite evitar llamar a preventDefault() para que no se recargue la página. Los campos del formulario van asociados a las propiedades de un objeto de tipo Product.

Importante: Para poder usar ngModel y ngSubmit, debemos importar en el componente el módulo FormsModule (@angular/forms).

Una vez se envía el formulario, las propiedades del objeto ya tendrán valor, por lo que simplemente añadimos el producto al array del componente. La id del producto por ahora nos la inventaremos hasta que tengamos comunicación con el servidor e inserte el producto en una base de datos.

Como podemos ver, es importante reasignar el producto asociado al formulario a un nuevo objeto en lugar de simplemente vaciar los campos. Si no lo hacemos así, se borrarían también los campos del objeto añadido al array, ya que en ambos casos (propiedad newProduct y última posición del array) se estaría apuntando al mismo objeto en memoria (referencia). Esto implicaría que si borramos el valor de los campos de newProduct, también estarían vacíos en el último producto del array y así se mostrarían en el HTML.

Otras cosas que podemos observar son la propiedad fileName, creada simplemente para que contenga el nombre del archivo y poder vacíar el input del archivo una vez añadido el producto. También el evento change asociado al campo del archivo, útil para poder convertir la imagen a base64 y previsualizarla. En angular, la variable $event en la plantilla representa al objeto emitido por el evento.

Encapsulación de estilos

Los estilos definidos en los componentes (CSS) solamente afecta a la plantilla del componente. No afecta a otros componentes, aunque estén dentro del mismo (El css de AppComponent no afecta a ProductsPageComponent por ejemplo).

Esto lo consigue Angular añadiendo un atributo cuyo nombre genera aleatoriamente tanto a los selectores CSS como a los elementos HTML de la plantilla. Por cada componente se genera un atributo diferente.

Angular Products

Angular Products

Los navegadores modernos soportan una característica llamada shadow DOM . Esto permite crear componentes web aislados en cuando al DOM y a los estilos CSS que se le aplican. Por defecto, Angular utiliza una emulación de esta técnica como ya hemos visto, añadiendo un atributo aleatorio a todos los elementos de la plantilla de un componente.

Se puede activar esta característica nativa con la opción encapsulation del componente. De esta manera, sin necesidad de crear un atributo extra, los estilos del componente no afectarán a otros, ya que su DOM estará aislado del DOM general. Hay que tener en cuenta que en este caso, los estilos globales tampoco afectarán al componente, por lo que este debe incluir e importar en su CSS propio aquellos estilos que quiera usar. Las variables CSS son una excepción y sí podrá acceder a las variables globales definidas en :root.

Por curiosidad, activar esta opción nos obliga a que el nombre del selector del componente deba tener un guión '-'. En este caso es buena idea establecer un prefijo global para los componentes de la aplicación (que anteriormente habíamos eliminado).

<< Crear un proyecto Pipes (filtros) >>