Usando directivas de Angular para extender componentes de terceros
Tags: angular, javascript
Published: 2022-04-06 07:30:24

> Traducción en español del artículo original de [Tim Deschryver](https://timdeschryver.dev/blog/use-angular-directives-to-extend-components-that-you-dont-own/) publicado el 11 marzo 2022

Las directivas en Angular son poco usadas y yo creo que es porque nosotros no sabemos todo de lo que son capaces de hacer. Si utilizas Angular tú probablemente se te hará familiar las directivas estructurales como `*ngIf` y `ngFor`, ¿pero debería tu código contener directivas propias? 

La respuesta a esa pregunta es probablemente que no, y si en caso de, tú lo has resuelto empleando componentes en vez de una directiva porque estos son más familiares.

En este artículo, yo quiero mostrarte una técnica usando las directivas para configurar componentes de terceros de forma unificada, Yo considero está como una solución elegante en comparación de utilizar un componente contenedor.

Vamos a mirar el ejemplo:

## Directiva por defecto
En mi proyecto usamos el componente `p-calendar` de PrimeNG y como podemos ver el siguiente código se repite cada vez.

```typescript
<p-calendar
  [(ngModel)]="date"
  required
  id="date"
  name="date"
  dateFormat="dd/mm/yy"
  [showIcon]="true"
  [showButtonBar]="true"
  [monthNavigator]="true"
  [yearNavigator]="true"
  yearRange="1900:2050"
  [firstDayOfWeek]="1"
>
</p-calendar>
```
Este markup es requerido para configurar el componente del modo que queremos por defecto. Y si me preguntas que todo este código no solo ensucia el código, sino también confunde y engaña, haciendo creer que las cosas son más complejas de lo que son en realidad. Yo puedo olvidar (O no saber que tengo) que agregar un atributo al `p-calendar` y este se comporta de otra manera para el usuario.

Además, cuando el componente, elimina, cambia o agrega un nuevo atributo, yo tendría que cambiar todos los `p-calendar` en nuestro código. En resumen, esto tiene  impacto en nuestros developers y también en los usuarios.

Cuando nosotros refactorizamos el código utilizando una directiva, el template se vuelve más simple y nos aseguramos de siempre brindar la misma experiencia al usuario.

La versión final seria:

```typescript
<p-calendar [(ngModel)]="date" required id="date" name="date"></p-calendar>
```
Pero como hemos pasado de 14 líneas de HTML a únicamente una, la respuesta es usando una directiva.

La directiva emplea el selector de `p-calendar`, para buscar modificar todos los elementos de `p-calendar` e inyectamos el calendar en la directiva, configurándolo como nosotros necesitamos.

```typescript
calendar.directive.ts
import { Directive } from '@angular/core';
import { Calendar } from 'primeng/calendar';

@Directive({
    selector: 'p-calendar',
})
export class CalenderDirective {
    constructor(private calendar: Calendar) {
        this.calendar.dateFormat = 'dd/mm/yy';
        this.calendar.showIcon = true;
        this.calendar.showButtonBar = true;
        this.calendar.monthNavigator = true;
        this.calendar.yearNavigator = true;
        this.calendar.yearRange = '1900:2050';
        this.calendar.firstDayOfWeek = 1;
    }
}
```
### Cambiando la configuración por defecto
La directiva que hemos creado aplica esa configuración a todos los `<p-calendars`, pero podemos tener algún caso que esto no aplique, para eso podemos sobreescribir los valores predefinidos en aquellos que requieren algo diferente.

En el siguiente ejemplo, podemos desactivar la opción de navegación asignándole a las propiedades el valor a falso.

```html
<p-calendar [monthNavigator]="false" [yearNavigator]="false"></p-calendar>
```

## Directiva selectiva
En vez que una directiva que cambia el comportamiento de todos los elementos, podemos modificar el selector para elementos específicos  y tengas diferentes casos de uso.

Por ejemplo, tenemos un dropdown que tiene un contrato genérico y queremos afectar solos los que coincidan con selector `p-dropdown[codes]`, estos pueden ser configurados. Notar que tenemos el atributo `codes` en el selector para únicamente afectar estos elementos.

```typescript
import { Directive, OnInit } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { sortByLabel } from '@core';

@Directive({
    selector: 'p-dropdown[codes]',
})
export class CodesDropdownDirective implements OnInit {
    constructor(private dropdown: Dropdown) {
        this.dropdown.optionLabel = 'label';
        this.dropdown.optionValue = 'key';
        this.dropdown.showClear = true;
    }

    public ngOnInit(): void {
        this.dropdown.options = [...this.dropdown.options].sort(sortByLabel);
        if(this.dropdown.options.length > 10) {
            this.dropdown.filter = true;
            this.dropdown.filterBy = 'label';
            this.dropdown.filterMatchMode = 'startsWith';
        }
    }
}
```

De esta manera, únicamente los `p-dropdown` que tenga el atributo `codes`, se comportan y se modifican con la directiva anterior y para utilizar solamente tenemos que agregar el atributo `codes`.

```html
<p-dropdown [(ngModel)]="favoriteSport" codes required id="sport" name="sport"></p-dropdown>
```

## Directiva excluyente
Otra es agregar el pseudo selector `:not()`, en nuestra directiva, para que aplique  la configuración para todos los casos comunes, pero excluyendo aquellos con el atributo `resetDropdown`. 

Por ejemplo, el 90% de nuestros dropdown de los elementos dropdown en nuestra aplicación tiene un datasource con "codes", no queremos tener que agregar el atributo `codes` al 90% de los elementos, otra manera solo agregar la directiva al 10% restante.

En vez usar el atributo `codes` para marcar los dropdown, nosotros asumimos que es el comportamiento por defecto, pero usando el atributo `resetDropdown` para excluir el comportamiento.

```typescript
import { Directive, OnInit } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { sortByLabel } from '@core';

@Directive({
    selector: 'p-dropdown:not(resetDropdown)',
})
export class CodesDropdownDirective implements OnInit {
    constructor(private dropdown: Dropdown) {
        this.dropdown.optionLabel = 'label';
        this.dropdown.optionValue = 'key';
        this.dropdown.showClear = true;
    }

    public ngOnInit(): void {
        this.dropdown.options = [...this.dropdown.options].sort(sortByLabel);
        if(this.dropdown.options.length > 10) {
            this.dropdown.filter = true;
            this.dropdown.filterBy = 'label';
            this.dropdown.filterMatchMode = 'startsWith';
        }
    }
}
```
En el HTML, emplearía de la siguiente manera.

```html
<!-- Usando la directiva codes por defecto -->
<p-dropdown [(ngModel)]="favoriteSport" required id="sport" name="sport"></p-dropdown>
<!-- Excluyendo el p-dropdown porque contiene el atributo resetDropdown -->
<p-dropdown
  [(ngModel)]="preference"
  resetDropdown
  required
  id="preference"
  name="preference"
></p-dropdown>
```

## Directivas para cargar datos
Nosotros podemos hacer aún más con la directiva, por ejemplo podemos ver como una directiva funciona como fuente de datos para llenar un dropdown con datos.

Esto es muy útil para datasources que se repiten o se utilizan con frecuencia y a su vez hacer que el datasource sea configurable.

En el siguiente ejemplo, nosotros agregamos el atributo `countries`,  para hacer el binding a los dropdown que utilizan la lista de `countries` como datasources. Esta directiva puede usarse con anidadas con otras, además incluye un `@Ouput()` para emitir un evento cuando los countries están cargados.
 
```typescript
import { Directive, EventEmitter, OnInit, Output } from '@angular/core';
import { Dropdown } from 'primeng/dropdown';
import { GeoService, sortByLabel } from '@core';

@Directive({
    selector: 'p-dropdown[countries]',
})
export class CountriesDropdownDirective implements OnInit {
    @Output() loaded = new EventEmitter<ReadonlyArray<Countries>>();

    constructor(private dropdown: Dropdown, private geoService: GeoService) {}

    public ngOnInit(): void {
        this.geoService.getCountries().subscribe((result) => {
            this.dropdown.options = result.map((c) => ({ label: c.label, key: c.id })).sort(sortByValue);
            this.loaded.emit(this.dropdown.options);
        });
    }
}
```
Ahora usamos la directiva
```html
<p-dropdown
  [(ngModel)]="country"
  countries
  required
  id="country"
  name="country"
  (loaded)="countriesLoaded($event)"
></p-dropdown>
````
## Resumen
Las directivas de Angular son muy poderosas, pero tristemente poco utilizadas.

Las directivas cumplen con el [principio Open-closed](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle). El componente es cerrado para modificaciones, pero las directivas nos permiten extender el componente sin realizar cambios a lo interno.

Por ejemplo, con las directivas podemos modificar el comportamiento de componentes de terceros o de una librería que no tenemos acceso al código del componente.

Es cierto que podemos lograr lo mismo empleando un componente contenedor y con componentes que tienes configuraciones complejas o muchas opciones, pero esto requiere más código y es difícil de mantener.

Enfocarnos en los elementos que requieren unos comportamientos o configuración diferente, nosotros podemos aprovecharnos de utilizar los selectores para elegir esos elementos específicos, y como las directivas se pueden anidar podemos limitar esa responsabilidad para que haga una sola cosa.
 
> Thanks to @timdeschryver 

## Opinión personal
> Las siguientes lineas no son parte del post original.

Desde mi punto de vista, utilizar las directivas nos permite mucha flexibilidad al trabajar con componentes de terceros o librerías, podemos lograr encapsular ciertos casos sin modificar el comportamiento original.

La parte de cargar datos usando una directiva es algo muy poderoso y hace muy flexible nuestro código.

Photo by <a href="https://unsplash.com/@ethanrobertson?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ethan Robertson</a> on <a href="https://unsplash.com/s/photos/beach?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>