Herramientas de usuario

Herramientas del sitio


clase:daw:diw:angular-atributos-componentes

¡Esta es una revisión vieja del documento!


12. Angular: Atributos de componentes

Hemos visto como crear un componente que genera todo el HTML:

<boton funcion="peligrosa" (pulsado)="alerta()">Hola mundo</boton>

Sin embargo este componente tiene dos problemas.

  • No podemos elegir si genera un <a> o <button>
  • No es tan personalizable porque no podemos cambiar nada si no es modificando el propio componente botón.

Para mejorar eso, podríamos modificar el componente botón y hacer que solo fuera un atributo de la siguiente forma:

<a boton funcion="peligrosa" (click)="alerta()">Hola mundo</a>

¿Qué hemos ganado ahora? Pues que tenemos ahora toda la potencia del tag <a>. Y podemos hacer cosas como las siguientes sin modificar el componente <boton>:

<a boton funcion="peligrosa" routerLink="/login" style="padding:8px" class="destacado">Hola mundo</a>

Es decir podemos añadir todos los atributos que habría en un <a> sin perder la funcionalidad de un <boton>. Y ya no hace falta el output(): el (click) es el del propio <a>.

Empecemos desde el principio del fichero boton.ts:

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

@Component({
  selector: 'button[boton], a[boton]',
  template: '<ng-content></ng-content>',
  styleUrl: './boton.scss',
  encapsulation: ViewEncapsulation.None
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');
}

  • El valor del selector ahora es button[boton], a[boton]. Y es para decir que se aplica este componente cuando un tag <a> o un <button> tiene el atributo boton.
  • Ya no usamos templateUrl sino template que es para indicar directamente la plantilla en vez referenciar un fichero. Y lo hacemos porque ahora la plantilla es solo '<ng-content></ng-content>' y no necesitamos un fichero solo para eso.
  • Por último decimos en encapsulation que ahora los estilos ya no son privados a nuestro componente sino que son "públicos". Eso genera un problema que se explica en ViewEncapsulation.
Como con cualquier componente, hay que importar Boton en el imports del componente que lo usa. Pero ahora, si se olvida, no hay ningún error: para Angular <a boton> es un <a> normal con un atributo que no conoce, y se pinta como un enlace sin estilos. Con <boton> Angular avisaba de que la etiqueta no existía.

Etiqueta Host

¿Que queremos que haga realmente nuestro componente? Pues simplemente es establecer el valor del atributo class, pero en el tag que ha escrito el usuario . Es decir, el que está "fuera" en la página HTML. A ese tag se le llama Host.

Veamos unos ejemplos de qué es Host

  • Host hace referencia a la etiqueta <a>

<a boton funcion="peligrosa" routerLink="/login" style="padding:8px" class="destacado">Hola mundo</a>

  • Host hace referencia a la etiqueta <button>

<button boton funcion="peligrosa" >Hola mundo</button>

La propiedad host

Pues lo que queremos es que nuestro componente modifique el atributo class de nuestro host. Para ello se usa la propiedad host de @Component:

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

@Component({
  selector: 'button[boton], a[boton]',
  template: '<ng-content></ng-content>',
  styleUrl: './boton.scss',
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'clases()'
  }
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');

  clases = computed(() => `c-boton funcion--${this.funcion()}`);
}

  • host: Lo que se pone en el host, escrito igual que en un HTML de Angular. '[class]': 'clases()' es como si el usuario hubiera escrito [class]="clases()" en su <a>.
  • computed(): Un signal calculado a partir de otros. Cada vez que cambia funcion(), Angular vuelve a calcular clases(). Se lee igual que los demás: clases().

Las clases que ya tiene el host no se pierden: Angular añade las nuestras a las que haya escrito el usuario.

<a boton class="destacado">Hola mundo</a>

queda en el navegador como:

<a class="destacado c-boton funcion--normal">Hola mundo</a>

ViewEncapsulation

Nos falta por explicar el valor de encapsulation que depende del enumerado ViewEncapsulation

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

@Component({
  selector: 'button[boton], a[boton]',
  template: '<ng-content></ng-content>',
  styleUrl: './boton.scss',
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'clases()'
  }
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');

  clases = computed(() => `c-boton funcion--${this.funcion()}`);
}

El enumerado ViewEncapsulation indica la visibilidad de los estilos CSS de un componente.

  • ViewEncapsulation.None : Los estilos CSS del componente son visibles desde cualquier otra parte de la aplicación es como hacer los estilos CSS como públicos.
  • ViewEncapsulation.Emulated : Los estilos CSS del componente solo se pueden ver en las etiquetas del propio template del componente es como hacer los estilos CSS como privados. Angular lo consigue añadiendo a cada etiqueta un atributo propio del componente. Este es el valor por defecto.
  • ViewEncapsulation.ShadowDom : Igual que Emulated pero usando el API de Shadow DOM del navegador.

¿Por qué es importante ahora esta propiedad y por qué la hemos cambiado a ViewEncapsulation.None?

Con ViewEncapsulation.Emulated (o ShadowDom) los estilos solo llegan a los tags de dentro de template y, escribiéndolos con el selector :host(.funcion--peligrosa), al propio Host. Se podría hacer así, pero hacemos los estilos públicos con ViewEncapsulation.None porque es lo que hace Angular Material con sus botones y porque así las clases del botón son CSS normal, el mismo que escribimos fuera de Angular.

Vale, pero ahora tenemos un problema. Tenemos las clases CSS funcion--normal tanto en <boton> como en <panel>. Y son distintos. Por lo tanto debemos cambiar los estilos de los componentes para que no choquen entre los distintos componentes.

Para evitar que choquen los nombres de los estilos CSS entre los distintos componentes se usan las nomenclaturas de:

En el tema Sistema de diseño: BEM se explica BEM.

Pero los estilos ahora quedan de la siguiente forma:

.c-boton {
  font-family: sans-serif;
  font-size: 16px;
  padding: 6px;

  border-radius: 6px;
  border-width: 1px;
  border-style: solid;

  display: inline-block;
  cursor: pointer;
  text-decoration: none;


  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

.c-boton--funcion-normal {
  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}

.c-boton--funcion-alternativa {
  border-color: #ed8936;
  background-color: #ed8936;
  color: #ffffff;
}

.c-boton--funcion-peligrosa {
  border-color: #c53030;
  background-color: #c53030;
  color: #ffffff;
}

Y el código queda finalmente así:

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

@Component({
  selector: 'button[boton], a[boton]',
  template: '<ng-content></ng-content>',
  styleUrl: './boton.scss',
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'clases()'
  }
})
export class Boton {
  readonly funcion = input<'normal' | 'alternativa' | 'peligrosa'>('normal');

  clases = computed(() => `c-boton c-boton--funcion-${this.funcion()}`);
}

clase/daw/diw/angular-atributos-componentes.1792878034.txt.gz · Última modificación: por claude