Mejora componentes de terceros con Angular directives

«

Los elementos de Angular que no se aprovechan lo suficiente y creo que es porque no conocemos todas sus capacidades. Si utilizas Angular es probable que estés familiarizado con las directivas estructurales como *ngIf y ngFor, ¿pero deberíamos usar directivas personalizadas en nuestro código?

Traducción en español del artículo original de Tim Deschryver publicado el 11 marzo 2022

La respuesta a esa pregunta probablemente sea negativa, y si es así, es más probable que hayas optado por utilizar componentes en lugar de una directiva porque son más comunes.

En este artículo, quiero presentarte una técnica que utiliza las directivas para configurar componentes de terceros de manera uniforme, lo cual considero una solución elegante en comparación con utilizar un componente contenedor.

Si deseas aprender más sobre Angular te dejo el curso aquí

Vamos a ver el ejemplo:

Directiva por defecto

En mi proyecto utilizamos el componente p-calendar de PrimeNG y como podemos ver, el siguiente código se repite cada vez.

<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 marcado es necesario para configurar el componente de la forma en que queremos por defecto. Si me preguntas, todo este código no solo ensucia el código, sino que también confunde y engaña, haciendo que las cosas parezcan más complejas de lo que son en realidad. Puedo olvidar (o tal vez no saber) que falta un atributo en el p-calendar y esto cambiaría la forma en que se comporta para el usuario.

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

Cuando refactorizamos el código utilizando una directiva, el template se simplifica y nos aseguramos de ofrecer siempre la misma experiencia al usuario.

La versión final sería:

<p-calendar [(ngModel)]="date" required id="date" name="date"></p-calendar>

Pero al pasar de 14 líneas de HTML a solo una, la respuesta está en el uso de una directiva.

La directiva utiliza el selector de p-calendar, para buscar y modificar todos los elementos de p-calendar e inyecta el calendario en la directiva, configurándolo como necesitamos.

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 creamos aplica esa configuración a todos los <p-calendars, pero podemos encontrarnos con casos en los que esto no se aplique, por lo que podemos sobrescribir los valores predeterminados en aquellos que necesiten algo diferente.

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

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

Directiva selectiva

En lugar de una directiva que cambia el comportamiento de todos los elementos, podemos modificar el selector para elementos específicos y tener diferentes casos de uso.

Por ejemplo, si tenemos un dropdown que tiene un contrato genérico y queremos afectar solo aquellos que coincidan con el selector p-dropdown[codes], podemos configurarlos. Observa que tenemos el atributo codes en el selector para afectar solo esos elementos.

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, solo los p-dropdown que tengan el atributo codes se verán afectados y modificados por la directiva anterior, y para usarla solo tenemos que agregar el atributo codes.

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

Directiva excluyente

Otra opción es agregar el pseudo selector :not() en nuestra directiva, para que aplique la configuración a la mayoría de los casos, pero excluyendo aquellos con el atributo resetDropdown.

Por ejemplo, si el 90% de nuestros dropdown en la aplicación tiene un datasource de «codes», no queremos tener que añadir el atributo codes al 90% de los elementos, en su lugar, solo agregamos la directiva al 10% restante.

En lugar de utilizar el atributo codes para identificar los dropdown, asumimos que es el comportamiento por defecto, pero utilizamos el atributo resetDropdown para excluir ese comportamiento.

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, lo utilizaríamos de la siguiente manera.

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

Podemos hacer aún más con las directivas, por ejemplo, ver cómo una directiva puede servir como fuente de datos para poblar un dropdown con información.

Esto es muy útil para fuentes de datos que se repiten o se utilizan con frecuencia, y al mismo tiempo permite que la fuente de datos sea configurable.

En el siguiente ejemplo, agregamos el atributo countries para vincularse con los dropdown que utilizan la lista de countries como fuente de datos. Esta directiva puede anidarse con otras, e incluye un @Ouput() para emitir un evento cuando los countries han sido cargados.

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 podemos usar la directiva de la siguiente manera

<p-dropdown
  [(ngModel)]="country"
  countries
  required
  id="country"
  name="country"
  (loaded)="countriesLoaded($event)"
></p-dropdown>

Resumen

Las directivas de Angular son muy poderosas, pero lamentablemente no se utilizan lo suficiente

Las directivas cumplen con el . El componente está cerrado para modificaciones, pero las directivas nos permiten extender el componente sin realizar cambios internos.

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

Aunque podríamos lograr lo mismo utilizando un componente contenedor, con componentes que tienen configuraciones complejas o muchas opciones, esto requiere más código y es difícil de mantener.

Al enfocarnos en los elementos que requieren comportamientos o configuraciones diferentes, podemos aprovechar 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 líneas no son parte del post original.

En mi opinión, utilizar las directivas nos permite mucha flexibilidad al trabajar con componentes de terceros o librerías, ya que podemos encapsular ciertos casos sin modificar el comportamiento original.

La parte de cargar datos utilizando una directiva es muy poderosa y hace que nuestro código sea muy flexible.

Para obtener el servidor GRATIS debes de escribir el cupon «LEIFER»


Si quieres aprender más sobre Angular, recuerda que te dejo material

»

En Grupo MET podemos ayudarte a implementar esta y muchas mas herramienta para optimizar tu trabajo. ¡Contáctanos para saber más!

Contactanos