Directivas en Angular – Guía avanzada – Parte #2

Retomamos esta serie sobre las Directivas en Angular, después de ver la parte #1, donde hicimos un repaso de las built-in directives y aprendimos a crear nuestra propia attribute directive, primero de manera básica y luego de manera mejorada, con el renderer.

🤷‍♀️ ¿MÁS PERDID@ QUE UN PULPO EN UN GARAJE? 🐙

Si acabas de aterrizar aquí y estos temas de Angular te suenan a chino, te recomiendo que empieces por este artículo de introducción a Angular con las claves para entenderlo.

Cómo usar el @HostListener para escuchar y reaccionar a eventos del DOM

Sin embargo, nuestra attribute directive no es muy interactiva, porque lo único que hace es darle un color de fondo siempre, sin aplicar ninguna condición. Vamos a cambiar eso 💪.

👉 Vamos a configurar nuestra directiva de manera que cuando pasemos el ratón por encima del elemento HTML al que se le aplica, el fondo se vuelva azul, y al quitar el ratón, el fondo vuelva a ser transparente. 

O sea, vamos a jugar con y a reaccionar a eventos como el mouseenter. Angular nos ofrece una manera sencilla y rápida de lidiar con esto, y es usando el decorador @HostListenerque debe ser importando desde angular/core. 

El @HostListener se añade al principio del método que tengamos que crear para interactuar con el DOM.

1. Creamos un método llamado mouseHover, por ejemplo, y le añadimos el decorador. Para informar a Angular sobre qué evento se debe aplicar a nuestro método, le pasamos el nombre de dicho evento como parámetro al @HostListener, en forma de string. El evento que queremos es el mouseenter

Aquí una lista completa de los eventos del DOM de Mozilla (MDN). Yo la consulto habitualmente. 😌 Ten en cuenta que podríamos pasarle cualquiera de estos eventos al @HostListener.

2. El método podría recibir datos del evento, así que se lo indicamos por parámetro.

3. Cortamos el código del ngOnInit y lo pegamos dentro del método mouseHover.

  ngOnInit() {
  }

  @HostListener('mouseenter') mouseHover(eventData: Event) {
    this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'blue');
  }

4. Creamos otro método casi idéntico al mouseHover, pero para cuando el ratón no esté encima del elemento HTML. Lo podemos llamar mouseLeavedonde el @HostListener escuchará el evento mouseleave. 👂

  @HostListener('mouseleave') mouseLeave(eventData: Event) {
    this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'transparent');
  }

Con estos cambios hemos conseguido crear una directiva reactiva. ¡Genial!

Cómo usar el @HostBinding para vincular propiedades del host element

Aclaremos que toda esta terminología del HostListener y el HostBinding se llama así porque son características vinculadas al host element. ¿Y qué es el host element? Simplemente, el elemento HTML sobre el que se aplicará la directiva. 👩‍🏫

Aclarado esto, veamos una manera más sencilla de hacer lo mismo que arriba, pero sin utilizar el renderer. Podemos conseguir eso usando el decorador @HostBindingaunque debe quedar claro que no tiene nada de malo usar el rendererEsto es sencillamente otra manera de conseguir el mismo resultado que antes. 

1. Importamos el @HostBinding desde angular/core.

2. Creamos una propiedad para manipular una cierta propiedad (valga la redundancia) de un elemento del DOM. La llamamos backgroundColorpor ejemplo, aunque podríamos darle el nombre que quisiéramos. 

Al @HostBinding le pasamos (por parámetro como string) la propiedad del elemento HTML que queremos manipular. Es decir, la propiedad style + la subpropiedad backgroundColor. Debemos pensar en modo CSS aquí. Si queremos cambiar el color de fondo, debemos acceder al backgroundColor desde la propiedad style. BackgroundColor coincide con el nombre de esa propiedad de CSS del DOM, pero en camelCaseporque TypeScript (en adelante, TS) no entiende los guiones

3. Decoramos la propiedad TS con el @HostBinding y le damos transparent como valor inicial.

  @HostBinding('style.backgroundColor') backgroundColor = 'transparent';

4. Comentamos el código de dentro de los métodos porque ya no lo vamos a necesitar. Lo sustituimos por un nuevo valor de la propiedad backgroundColor. "Azul" para el primer método y "transparente" para el segundo.

  @HostListener('mouseenter') mouseHover(eventData: Event) {
    // this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'blue');
    this.backgroundColor = 'blue';
  }

  @HostListener('mouseleave') mouseLeave(eventData: Event) {
    // this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'transparent');
    this.backgroundColor = 'transparent';
  }

¡Y así hemos conseguido el mismo efecto que antes! 👏

Cómo utilizar property binding en nuestra directiva

Nuestra directiva ya resulta algo más útil que antes, pero todavía podemos añadirle más funcionalidad, porque de momento no podemos elegir qué colores se aplican al hacer hover sobre el elemento, ya que los hemos escrito hardcodedSi esta fuese una directiva que nos descargamos de un tercero, lo lógico sería que nosotros como desarrolladores pudiésemos elegir los colores que mostrar.

Para implementar ese cambio vamos a usar property binding

1. Añadimos dos propiedades cuyo valor (el color) obtendremos desde fuera del componente, por tanto, las decoramos con @InputLas llamamos defaultColor highlightColor y les asignamos valores iniciales.

2. Cambiamos el valor de la propiedad backgroundColor en el @HostBinding por defaultColor y en los métodos (highlightColor cuando pasamos el ratón por encima y defaultColor cuando retiramos el ratón).

  @Input() defaultColor = 'transparent';
  @Input() highlightColor = 'blue';
  @HostBinding('style.backgroundColor') backgroundColor = this.defaultColor;

  constructor(private eleRef: ElementRef, private renderer: Renderer2) { }

  ngOnInit() {
  }

  @HostListener('mouseenter') mouseHover(eventData: Event) {
    // this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'blue');
    this.backgroundColor = this.highlightColor;
  }

  @HostListener('mouseleave') mouseLeave(eventData: Event) {
    // this.renderer.setStyle(this.eleRef.nativeElement, 'background-color', 'transparent');
    this.backgroundColor = this.defaultColor;
  }

Con estos cambios, en tu navegador el comportamiento no debería haber cambiado. Pero ahora ya podemos vincular y manipular nuestras propiedades que lleven el @Input desde fuera. 👌

3. En el archivo app.component.html, vinculamos esas propiedades en el <p> que está usando la directiva appBetterHighlight, dándole los valores que queramos.

      <p appBetterHighlight [defaultColor]="'yellow'"
         [highlightColor]="'red'">
         Style me with a better Directive!
      </p>

👀 Fíjate que incluimos el valor entre comillas simples, porque estamos pasándole un string. 

Si ahora vas a tu navegador, verás que tenemos un pequeño bugya que nada más cargar la página, el <p> no está subrayado de amarillo. Esto es porque en el momento de carga, Angular no detecta el nuevo valor de la propiedad backgroundColor. Para solucionarlo, dejamos la propiedad sin inicializar, sólo la declaramos, y la inicializamos dentro del ngOnInit. 

  @HostBinding('style.backgroundColor') backgroundColor: string;

  constructor(private eleRef: ElementRef, private renderer: Renderer2) { }

  ngOnInit() {
    this.backgroundColor = this.defaultColor;
  }

¡Solucionado! Ahora nuestro <p> tiene un color amarillo de fondo por defecto. 👍

Hay un par de cosas interesantes sobre cómo le pasamos información a ese <p>.

Sabemos que los elementos HTML, como ese <p>, tienen atributos. Y la forma de vincular atributos es igual que la forma de vincular propiedades, es decir, con los corchetes.

Angular sabe distinguir entre lo que son atributos de un elemento HTML (los atributos del <p>, en este caso) y propiedades vinculadas mediante "property binding".

Lo hace comprobando primero las propiedades de nuestras propias directivas y en segundo lugar, los atributos de los elementos HTML.

Podemos pasarle el nombre de nuestra directiva como alias del @Input de alguna de nuestras propiedades, por ejemplo, del highlightColor. 

  @Input('appBetterHighlight') highlightColor = 'blue';

Pero ahora la configuración de nuestro <p> ha dejado de funcionar:

      <p appBetterHighlight [defaultColor]="'yellow'"
         [highlightColor]="'red'">
         Style me with a better Directive!
      </p>

Para arreglarlo, debemos dejar de vincular la propiedad highlightColor y vincular directamente la directiva, así:

      <p [appBetterHighlight]="'red'"
         [defaultColor]="'yellow'">
         Style me with a better Directive!
      </p>

 Existe un atajo referente a la sintaxis del property binding, es decir:

[TSproperty]="'string'"

Ya que podemos sintetizarlo, dejándolo así:

TSproperty="string"

Vamos a probarlo sobre nuestro <p>.

      <p [appBetterHighlight]="'red'"
         defaultColor="yellow">
         Style me with a better Directive!
      </p>

Y ahora en tu navegador deberías ver el mismo comportamiento que antes. 👀 Ojo con usar ese atajo, porque debes dejar muy claro que esto es property binding y no un atributo del elemento HTML sobre el que se encuentre.

Cómo funcionan las structural directives en realidad

Las structural directives son los otros tipos de directivas que trae Angular, que identificamos con un asterisco o estrella. Antes de crear nuestra propia structural directive, vamos a ver el mecanismo que hay detrás de ellas. La sintaxis brindada por Angular es en realidad una manera de facilitarnos la vida. Al escribirla con la estrella, Angular lo traduce a otra sintaxis más larga.

Vamos a hacer nosotros la traducción para entender lo que sucede, a partir de este <li> del app.component.html:

  <div *ngIf="!onlyOdd">
     <li class="list-group-item" *ngFor="let evenNumber of evenNumbers"
         [ngClass]="{odd: evenNumber % 2 !== 0}"
         [ngStyle]="{backgroundColor: evenNumber % 2 !== 0 ? 'slateblue' : 'transparent'}">
         {{ evenNumber }}
     </li>
  </div>

1. Debajo de ese <div>, creamos una <ng-template>. Es la base que usa Angular para crear la estructura de un ngIf. Dentro de esa etiqueta escribimos el contenido que queremos que se muestre bajo una condición.

 Lo que hace <ng-template> es no mostrarse por defecto hasta que una condición se cumpla.

2. Añadimos un ngIf pero sin la estrella, sino con el formato de property binding y lo vinculamos a la misma condición que en el snippet (!onlyOdd).

<ng-template [ngIf]="!onlyOdd">
  <div>
    <li class="list-group-item" *ngFor="let evenNumber of evenNumbers" 
        [ngClass]="{odd: evenNumber % 2 !== 0}"
        [ngStyle]="{backgroundColor: evenNumber % 2 !== 0 ? 'slateblue' : 'transparent'}">
        {{ evenNumber }}
    </li>
  </div>
</ng-template>

¡Y ya está! Ahora en tu navegador deberías ver la lista de números pares doble

Cómo construir una structural directive

¡Ya estamos listos para crear nuestra propia structural directive! En esta ocasión vamos a crear una directiva que haga lo opuesto a la directiva ngIf, que muestra algo en el DOM si una condición se cumple (es decir, si es true). Nuestra directiva hará lo contrario: mostrar algo en el DOM si una condición resulta en false.

1. Con la ayuda de la CLI, creamos nuestra directiva (a la altura de las otras dos directivas), a la que llamaremos unless. 

ng g d unless --skipTests

La condición la vamos a recibir desde fuera del componente, es decir, será otro componente el que le pase la información, lo que en inglés se conoce como "to give input"y de ahí el nombre del decorador que necesitamos usar, el @Input. 👩‍🏫

2. Añadimos el decorador y lo importamos.

3. Lo vinculamos a una propiedad llamada unless, que queremos que sea una condición

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

@Directive({
  selector: '[appUnless]'
})
export class UnlessDirective {

  @Input() unless; 

  constructor() { }

}

 😮 Pero cada vez que esa condición cambie su estado, necesitamos ejecutar un método. Para eso, implementamos un setter con la palabra clave set. Esto permite que la propiedad unless se convierta en un método cuando su estado cambie (de true false o viceversa).

La propiedad unless recibirá una condición de tipo boolean, así que se la pasamos como parámetro. 

4. Comprobamos si la condición no es true (usando la sintaxis de negación). Este será el caso en el que mostraremos algo en el DOM. En caso contrario (el else del if/else statement), no mostraremos nada

  @Input() set unless(condition: boolean) {
    if(!condition) {

    } else {
      
    }
  } 

Recuerda que cualquier structural directive se aplicará sobre una <ng-template>, cosa que hará Angular al detectar que es una structural directive por el uso de la estrella. Por eso, debemos tener acceso a esa <ng-template> y también al lugar concreto del documento donde queremos implementar la directiva. Ambas cosas pueden ser inyectadas en el constructor. 💉

5. Inyectamos la referencia al ng-template usando un data type de Angular, el TemplateRefEs el mismo concepto que el ElementRef, pero en este caso obtenemos una referencia a un elemento de Angular (el <ng-template>), en lugar de a un elemento HTML. TemplateRef es un tipo genérico que debemos importar desde angular/core. 

Lo siguiente que necesitamos inyectar es el view containerque nos sirve para saber dónde (en qué lugar de la template de un componente) debemos aplicar la directiva. Para eso utilizamos un data type de Angular llamado ViewContainerRefque debemos importar desde angular/core.

QUÉ

DÓNDE

TemplateRef ➡ el elemento sobre el que se aplica la directiva

ViewContainerRef ➡ la template del componente donde está el elemento sobre el que se aplica la directiva

6. Para inyectar estos dos data types, creamos dos propiedades, templateRef vcRef, aunque puedes llamarlas como quieras.

  constructor(private templateRef: TemplateRef<any>, private vcRef: ViewContainerRef) { }

Con estas dos herramientas disponibles, podemos manipular la vcRef en nuestro método unless cuando la condición cambie. Es decir, en el bloque del if statement. Lo hacemos llamando a un método del vcRef, el createEmbeddedViewEste método crea una "vista", es decir, algo que mostrar. En este caso lo que queremos mostrar es nuestra templateRef, así que se lo pasamos como parámetro.

Si la condición es true, no mostraremos nada. Para eso llamamos al método clear del vcRef, que eliminará el contenido del DOM.   

  @Input() set unless(condition: boolean) {
    if(!condition) {
      this.vcRef.createEmbeddedView(this.templateRef);
    } else {
      this.vcRef.clear();
    }
  }

Con estos cambios, nuestra directiva está lista para usar 💁. La usaremos en el app.component.html.

7. Comentamos el código del <ng-template> y sustituimos el bloque del *ngIf="!onlyOdd" por nuestra nueva directiva. Pero ahora no queremos comprobar si la condición onlyOdd es falsa, sino verdadera.

        <div *appUnless="onlyOdd">
          <li class="list-group-item" *ngFor="let evenNumber of evenNumbers" 
              [ngClass]="{odd: evenNumber % 2 !== 0}"
              [ngStyle]="{backgroundColor: evenNumber % 2 !== 0 ? 'slateblue' : 'transparent'}">
              {{ evenNumber }}
          </li>
        </div>
        <!-- <ng-template [ngIf]="!onlyOdd">
          <div>
            <li class="list-group-item" *ngFor="let evenNumber of evenNumbers" 
                [ngClass]="{odd: evenNumber % 2 !== 0}"
                [ngStyle]="{backgroundColor: evenNumber % 2 !== 0 ? 'slateblue' : 'transparent'}">
                {{ evenNumber }}
            </li>
          </div>
        </ng-template> -->

Sin embargo, es posible que tu IDE te marque un error en el div, diciendo "Property binding appUnless not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations".

Can't bind to 'appUnless' since it isn't a known property of 'div'."

Si no te marca el error ahí, lo podrás ver cuando guardes y vayas a tus developer tools. 

Esto ocurre porque desde el archivo unless.directive.ts estamos usando custom property binding sobre la propiedad unless. Debemos asegurarnos de que el nombre de la propiedad coincida con el nombre de la directiva. 🤓

  @Input() set appUnless(condition: boolean) {

¡Y ahora sí! Ya todo funciona como esperado, mostrando lo mismo que antes.

structural-directive propia

Directiva ngSwitch: qué es y cómo usarla

La directiva ngSwitch es otra structural directive que viene por defecto con Angular. Está basada en la sintaxis pura de JavaScript del switch statement

El "ngSwitch" resulta muy útil cuando tenemos múltiples condiciones entre las que elegir.

Imaginemos que tenemos una propiedad en el app.component.ts a la que le asignamos un número. La llamamos value, por ejemplo.

  value = 8;

1. Elegimos un lugar en el app.component.html donde ese número cambiará, y cuando lo haga, queremos mostrar sólo el número al que ha cambiado. Por ejemplo, si cambia del 8 al 10, queremos sustituir el 8 por el 10, mostrando así sólo el número 10. Este es un caso perfecto para usar ngSwitch, donde tendremos varios casos (en inglés, casesque pueden darse. 

2. Englobamos nuestro código en un div debajo del <p> de la directiva BetterHighlight. Vinculamos el div al ngSwitch mediante property binding. Ese ngSwitch está vinculado al value, porque esa es nuestra condición que va ir cambiando.

3. Creamos varios <p>, donde cada <p> será un case diferente. Es decir, un número distinto asignado a la propiedad value.

      <div [ngSwitch]="value">
        <p>value is 5</p>
        <p>value is 10</p>
        <p>value is 100</p>
        <p>value is 15</p>
        <p>value is default</p>
      </div>

Debemos añadir algo a los <p> para controlar cuál se muestra y cuál no, porque sólo debería mostrarse uno de ellos a la vez. Conseguimos eso usando el ngSwitchCase en cada <p>, pasándole el value correspondiente en cada caso como argumento. Aquí sí que necesitamos la estrella para que Angular detecte que es una structural directive.

Para el caso por defecto (el default, el último <p>) usamos ngSwitchDefault.

        <p *ngSwitchCase="5">value is 5</p>
        <p *ngSwitchCase="10">value is 10</p>
        <p *ngSwitchCase="100">value is 100</p>
        <p *ngSwitchCase="15">value is 15</p>
        <p *ngSwitchDefault>value is default</p>

¡Y listo! Ahora verás que sólo se muestra un <p> en tu navegador. Prueba a cambiar el value en el archivo TS y verás cómo cambia dependiendo del valor que le des. 👌

THE END!

¡Y con esto terminamos nuestra serie avanzada sobre Directivas en Angular! Espero que hayas aprendido algo nuevo 😊.  Si te queda alguna duda, ¡nos vemos en los comentarios!

Sobre la autora de este post

Soy Rocío, una abogada reconvertida en programadora. Soy una apasionada de aprender cosas nuevas y ferviente defensora de que la única manera de ser feliz es alcanzando un equilibrio entre lo que te encanta hacer y lo que te saque de pobre. Mi historia completa, aquí. 

Más recursos de aprendizaje

En mi experiencia, la manera más eficaz para aprender Angular es combinando varias vías de aprendizaje. Uno de mis métodos favoritos son los vídeo-cursos y mi plataforma predilecta para eso es Udemy. He hecho varios cursos pero sólo recomiendo aquellos que verdaderamente me han sido útiles. Aquí van:


Si necesitas apoyo en forma de libro, puede que éstos te sirvan de ayuda:

La programación es un mundo que evoluciona a una velocidad de vértigo. Los autores de estos libros lo saben, por eso suelen encargarse de actualizar su contenido regularmente. Asegúrate de que así sea antes de adquirirlos 😌.

Participo en el programa de afiliados de Udemy y Amazon, lo que significa que, si compras alguno de estos cursos y/o libros, yo me llevaré una pequeña comisión y a ti no costará nada extra. Vamos, lo que se dice un win-win 😊.

Otros artículos que pueden interesarte

Días del 353 al 386
Objetivos versus realidad Y nuevamente, llegó otro día clave. Llegó…y pasó. El pasado 4 de marzo este Reto Computer Geek[...]
Construye Minioland: Tu primera aplicación con Angular | Parte #1
¿Qué vamos a construir?Minioland, o así he decidido llamar a esta sencilla app, totalmente responsive y de tipo Single Page[...]
Angular: Entendiendo la Directiva ngModel
Angular es un framework que nos permite, entre otras cosas, añadir contenido dinámico a nuestros archivos HTML. Una de las formas[...]
Si crees que este post puede serle útil a alguien, por favor, ¡compártelo!:

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Como toda web legal que se precie, utilizamos cookies para asegurar que damos la mejor experiencia al usuario en nuestro sitio web. Si continúas utilizando este sitio asumiremos que estás de acuerdo. más información

Los ajustes de cookies de esta web están configurados para "permitir cookies" y así ofrecerte la mejor experiencia de navegación posible. Si sigues utilizando esta web sin cambiar tus ajustes de cookies o haces clic en "Aceptar" estarás dando tu consentimiento a esto.

Cerrar