Tabla de Contenidos

11. Angular: Atributos de componentes

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

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

Sin embargo este componente tiene dos problemas.

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>.

Empecemos desde el principio del fichero boton.ts:

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

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


}

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

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

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

@HostBinding

Pues lo que queremos es que nuestro componente modifique el atributo class de nuestro host. Pues para ello usamos el decorador de Angular @HostBinding(hostPropertyName?: string)

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

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

  @HostBinding('class')
  get clazz(): string {
    return `boton funcion--${this.funcion}`;
  }

}

El decorador @HostBinding('class') hace que el valor de la propiedad class siempre sea el valor que tiene la propiedad clazz

Recuerda que clazz es una propiedad solo que calculada a través de la función clazz()
get clazz(): string {
  return `boton funcion--${this.funcion}`;
}

El problema aquí es que no podríamos tener valores en class en el Host ya que la propiedad clazz elimina lo que hay y pone los datos de únicamente nuestras clases.

Es decir que el siguiente ejemplo no funcionaría, ya que eliminaría destacado:

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

Aunque sí que funcionaría el siguiente ya que nuestro componente no modifica ni el atributo routerLink ni style:

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

Así que ¿cómo lo arreglamos? Pues retornando un objeto con las clases que vamos a añadir en vez de un string y de esa forma no se modifica lo que ya hay.

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

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


  @HostBinding('class')
  get clazz(): Record<string, boolean> {
    return {
      'boton': true,

      'funcion--normal': this.funcion === 'normal',
      'funcion--alternativa': this.funcion === 'alternativa',
      'funcion--peligrosa': this.funcion === 'peligrosa',
    };
  }

}

Ahora retornamos un objeto con las clases CSS que podrían haber y son un booleano indicando si está finalmente o no, dejando sin tocar el resto de clases.

Fíjate en que el tipo de datos de la propiedad clazz es Record<string, boolean> que es simplemente como decir que retorna { [key: string]: boolean }. Que significa que es un objeto cuyas claves son un string y cuyos valores son un boolean.

La definición de Record es exactamente la siguiente:

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Mas información:

ViewEncapsulation

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

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

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

  @HostBinding('class')
  get clazz(): Record<string, boolean> {
    return {
      'boton': true,

      'funcion--normal': this.funcion === 'normal',
      'funcion--alternativa': this.funcion === 'alternativa',
      'funcion--peligrosa': this.funcion === 'peligrosa',

    };
  }

}

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

Existe ViewEncapsulation.Emulated además de ViewEncapsulation.ShadowDom ya que el soporte del API de Shadow DOM en el navegador ha sido muy malo durante mucho tiempo.

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

Resulta que si ponemos el valor por defecto de ViewEncapsulation.Emulated o su alternativa ViewEncapsulation.ShadowDom los estilos solo se pueden usar en los tag que hay dentro de template. Sin embargo , con el cambio que hemos hecho , los estilos CSS se van a usar ahora fuera de los tags de template porque los vamos a usar en Host, así que es necesario hacer los estilos CSS públicos, no encapsularlos y por lo tanto usar ViewEncapsulation.None

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:

.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;
}
 
.boton--funcion-normal {
  border-color: #0056b8;
  background-color: #0056b8;
  color: #ffffff;
}
 
.boton--funcion-alternativa {
  border-color: #ed8936;
  background-color: #ed8936;
  color: #ffffff;
}
 
.boton--funcion-peligrosa {
  border-color: #c53030;
  background-color: #c53030;
  color: #ffffff;
}

Y el código queda finalmente así:

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

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

  @HostBinding('class')
  get clazz(): Record<string, boolean> {
    return {
      'boton': true,

      'boton--funcion-normal': this.funcion === 'normal',
      'boton--funcion-alternativa': this.funcion === 'alternativa',
      'boton--funcion-peligrosa': this.funcion === 'peligrosa',

    };
  }

}