Directivas

<< Anidar componentes Servicios >>

En angular tenemos 3 tipos de directivas:

  • Directivas de componente → Son los componentes que hemos visto. Tienen asociada una plantilla que se inserta dentro del elemento que coincide con el selector (misma sintáxis que en CSS) definido en el decorador @Component. La plantilla es gestionada por la clase del componente.
  • Directivas de atributo → Se le añaden a elementos existentes, pueden ser elementos HTML estándar o componentes Angular. En este caso son clases que añaden funcionalidad y cierto comportamiento a dichos elementos. El contrario que los componentes, aquí no hay plantilla asociada.
  • Directivas estructurales → Se le añaden a elementos cuando queremos controlar si se añaden o quitan del DOM (y los valores que contienen). Las directivas de Angular *ngIf, *ngFor y *ngSwitch, son estructurales (estas en concreto han sido sustituidas en Angular 17 por la nueva sintaxis).

Directivas de atributo

Las directivas ngClass y ngStyle son ejemplos de directivas de atributo, ya que se les pueden poner a cualquier elemento del DOM y gestionan las clases y estilos CSS internos de dicho elemento. Otras directivas serían ngModel que podemos añadir a un elemento de un formulario, o ngForm que no se añade explícitamente pero automáticamente Angular la añade a cualquier element <form> cuando importamos FormsModule en el componente.. 

Se pueden crear nuevas directivas de atributo con el comando ng generate directive (o ng g d). Las directivas son clases que tienen el decorador @Directive.

Ejemplo: Directiva para cambiar el color de fondo

Vamos a hacer un ejemplo simple (en un proyecto diferente al de angular-products) creando una directiva que al asignársela a un elemento, cuando hagamos clic en dicho elemento, le cambie el color de fondo:

ng g d directives/set-color

@Directive({
  selector: '[setColor]',
  standalone: true,
})
export class SetColorDirective {
  constructor() { }
}

Cuando un elemento (cuyo componente haya importado la directiva), se le añada el atributo setColor, se le asociará un objeto de la clase setColorDirective que permitirá manipular atributos, eventos, etc de dicho elemento. Como podemos observar, el selector está entre corchetes, lo que implica que es un atributo añadido a cualquier elemento. Podríamos ser más específicos, por ejemplo, limitando solo a inputs con el selector input[setColor].

El atributo setColor, además podrá tener un valor asociado (el color elegido), que recibiremos con un parámetro de entrada input (como en los componentes). Por ahora, inyectaremos ElementRef (veremos como funciona la inyección de dependencias más adelante) para poder acceder al elemento que contiene la directiva y cambiarle el color de fondo.

export class SetColorDirective {
  color = input.required<string>({alias: 'setColor'});
  elementRef = inject<ElementRef<HTMLElement>>(ElementRef);

  constructor() {
    effect(() => this.elementRef.nativeElement.style.backgroundColor = this.color());
  }
}

Para usar la directiva en la plantilla de un componente, debemos importar su clase primero en el array imports del componente. Después, le ponemos el atributo marcado por el selector de la directiva y el valor del color.

<h1 setColor="yellow">{{title}}</h1>

Es más práctico vincular el valor de la propiedad color al color de fondo vinculando la propiedad. Desde una directiva podemos hacer esto con la propiedad host del decorador @Directive (también posible con @Component). De esta manera vinculamos propiedades  y valores del elemento que recibe la directiva como si lo hiciéramos en una plantilla HTML. Ejemplo:

@Directive({
  selector: '[setColor]',
  standalone: true,
  host: {
    '[style.backgroundColor]': 'color()'
  }
})
export class SetColorDirective {
  color = input.required<string>({alias: 'setColor'});
}

Ahora vamos a hacer que el color pueda cambiar en cualquier momento. Para eso, en el componente donde asignamos la directiva, usaremos la notación de corchetes para vincularla a un valor que puede cambiar.

<select [(ngModel)]="color">
  <option value="yellow">Yellow</option>
  <option value="red">Red</option>
  <option value="cyan">Cyan</option>
</select>
<h1 [setColor]="color()">{{title}}</h1>

Por último, añadiremos un evento para gestionar el click en el objeto host. Haremos que cambie el color de fondo del texto de negro a blanco y viceversa.

@Directive({
  selector: '[setColor]',
  standalone: true,
  host: {
    '[style.backgroundColor]': 'color()',
    '[style.color]': "textColor()",
    '(click)': 'toggleTextColor()',
  }
})
export class SetColorDirective {
  color = input.required<string>({alias: 'setColor'});
  textColor = signal('black');

  toggleTextColor() {
    this.textColor.update(c => c === 'black' ? 'white' : 'black');
  }
}

Ejemplo: Directiva para codificar archivo a base64

Vamos a crear una directiva para nuestro ejemplo de productos (angular-products) que cuando se la asignemos a un input de tipo archivo, al seleccionar un archivo, nos lo devuelva codificado en Base64.

ng g d directives/encode-base64

El selector de la directiva lo modificaremos para que solo se pueda aplicar a elementos <input type="file">, y que además tengan el atributo encodeBase64. Esto último lo podríamos omitir y Angular le asignaría la directiva a todos los inputs de ese tipo automáticamente. Pero en principio, vamos a utilizar ese atributo para discriminar.

@Directive({
  selector: 'input[type=file][encodeBase64]',
  standalone: true,
})
export class EncodeBase64Directive { }

Después implementaremos la funcionalidad correspondiente:

  • Crearemos un parámetro de salida (output) llamado encoded, que emitirá el archivo serializado a base64.
  • Injectaremos la referencia al elemento HTML (input) con ElementRef.
  • Crearemos el método (encodeFile) que transformará el archivo del elemento a base64 y emitirá el resultado.
  • Utilizaremos el atributo host para vincular el evento change del input y que llame al método anterior. 
@Directive({
  selector: 'input[type=file][encodeBase64]',
  standalone: true,
  host: {
    '(change)': 'encodeFile()'
  },
})
export class EncodeBase64Directive {
  encoded = output<string>();
  element = inject<ElementRef<HTMLInputElement>>(ElementRef);

  encodeFile() {
    const fileInput = this.element.nativeElement;
    if (!fileInput.files?.length) return;
    const reader = new FileReader();
    reader.readAsDataURL(fileInput.files[0]);
    reader.addEventListener('loadend', () => {
      this.encoded.emit(reader.result as string);
    });
  }
}

En el componente product-form importaremos la directiva y la utilizaremos con el input de la imagen. Ahora simplemente tendremos que actualizar la imagen del producto que nos devolverá la directiva en el evento (encoded). Ya podemos borrar el método changeImage del componente product-form al no necesitarlo más.

<div class="col-sm-10">
  <!-- La referencia #imgInput ya no hace falta -->
  <input type="file" ... encodeBase64 (encoded)="newProduct.imageUrl = $event" />
</div>

<< Anidar componentes Servicios >>