ng-content

ng-content

Usando directivas de Angular para extender componentes de terceros

Usando directivas de Angular para extender componentes de terceros

Extiende tus componentes sin modificarlos

Dany Paredes's photo
Dany Paredes
·Apr 6, 2022·

6 min read

Subscribe to our newsletter and never miss any upcoming articles

Play this article

Table of contents

  • Directiva por defecto
  • Directiva selectiva
  • Directiva excluyente
  • Directivas para cargar datos
  • Resumen
  • Opinión personal

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?

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

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.

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

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

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.

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

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.

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

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.

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

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

<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. 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 Ethan Robertson on Unsplash

 
Share this