El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

Directivas en Angular 🤫

Por qué las directivas son la mejor opción para la reutilización de opciones de componentes selectos en Angular

· 3 min de lectura
Directivas en Angular 🤫

Nuestra aplicación tiene varios componentes select que se utilizan con frecuencia en toda la aplicación, como los componentes "select user", "select wallet" y "select blockchain".

Un enfoque para gestionar estos componentes sería consultar individualmente las options y pasarlas al componente select cada vez que se necesiten; sin embargo, este enfoque no es DRY.

Una solución alternativa es crear un componente envoltorio para cada componente select que encapsule su lógica. Por ejemplo, podríamos crear un componente UsersSelect que sea reutilizable en toda la aplicación:

Una solución alternativa es crear un componente envoltorio para cada componente select que encapsule su lógica. Por ejemplo, podríamos crear un componente UsersSelectque sea reutilizable en toda la aplicación:

@Component({
  selector: 'app-users-select',
  standalone: true,
  imports: [SelectComponent, CommonModule],
  template: `
    <app-select
      placeholder="Select user"
      [options]="users$ | async"
      [allowClearSelected]="allowClearSelected"
    ></app-select>
  `
})
export class UsersSelectComponent implements ControlValueAccessor {
  @Input() allowClearSelected: boolean;

  users$ = inject(UsersService).getUsers().pipe(
    map((users) => {
      return users.map((user) => {
        return {
          id: user.id,
          label: user.label,
        };
      });
    })
  )

  // ... ControlValueAccessor...
}

Aunque este enfoque funciona, no es el óptimo. En primer lugar, cada componente requeriría un nuevo accesorio de valor de control. Además, para que los consumidores puedan configurar los inputs  y escuchar los outputs, el componente select debe delegar su API en el componente envuelto.

Para solucionar estos problemas, podemos utilizar directivas. En lugar de crear un componente separado para cada opción de selección, podemos crear una directiva reutilizable que se puede utilizar para añadir opciones al componente de selección:

 import { Directive, inject } from '@angular/core';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';

@UntilDestroy()
@Directive({
  selector: '[usersSelectOptions]',
  standalone: true,
})
export class UsersSelectOptionsDirective {
  private select = inject(SelectComponent);
  private users$ = inject(UserService).getUsers();

  ngOnInit() {
    this.select.placeholder = 'Select user';

    this.users$.pipe(untilDestroyed(this)).subscribe((users) => {
       this.select.options = users.map((user) => {
         return {
           id: user.id,
           label: user.name,
         };
       });
    });
  }
}

Para solucionar estos problemas, podemos utilizar directivas. En lugar de crear un componente separado para cada opción de selección, podemos crear una directiva reutilizable que se puede utilizar para añadir opciones al componente de selección:

Obtenemos una referencia al SelectComponent usando DI. En el hook del ciclo de vida ngOnInit, la propiedad select.placeholder se establece en "Select user".

Obtenemos los usuarios y establecemos las opciones de selección. Ahora, podemos usarlo en nuestro componente select:

<app-select
  usersSelectOptions <=====
  formControlName="userId"
  [allowClearSelected]="false"
></app-select>

Testing de funciones DI en Angular

Se ha añadido un nuevo método estático llamado runInInjectionContext a TestBed en Angular v15.1.0-next.0 para facilitar las pruebas de inject(). La función runInInjectionContext funciona de forma similar a la función runInContext. Digamos que tenemos una función que utiliza el módulo http para obtener usuarios:

export function getUsers() {
  return inject(HttpClient).get<User[]>('users');
}

Podemos probarlo pasando la función getUsers a TestBed.runInInjectionContext:

Veamos un ejemplo más cuando se utiliza un InjectionToken:

export const FOO = new InjectionToken('FOO', {
  providedIn: 'root',
  factory() {
    return 'foo';
  },
});

export function getFoo() {
  return inject(FOO);
}

Digamos que queremos probar la función getFoo pero anulando lo que devuelve FOO:

import { TestBed } from '@angular/core/testing';

describe('Foo', () => {

  it('should get 🔥', () => {
    TestBed.overrideProvider(FOO, { useValue: '🔥' });
    const result = TestBed.runInInjectionContext(getFoo);
    expect(result).toEqual('🔥');
  });

 it('should get 🙌', () => {
    TestBed.overrideProvider(FOO, { useValue: '🙌' });
    const result = TestBed.runInInjectionContext(getFoo);
    expect(result).toEqual('🙌');
  });

});

También funcionará con fakeAsync. Digamos que tenemos un proveedor BAR que devuelve una promesa:

import { TestBed, fakeAsync } from '@angular/core/testing';

describe('Bar', () => {

  it('should work with fakeAsync', fakeAsync(() => {
    TestBed.overrideProvider(BAR, { useValue: Promise.resolve('😄') });
    let result = '';

    TestBed.runInInjectionContext(getBar).then((v) => {
      result = v;
    });

    flushMicrotasks();
    expect(result).toEqual('😄');
  }));

});

En versiones anteriores puedes utilizar el EnvironmentInjector para probarlo:

TestBed.inject(EnvironmentInjector).runInContext(getUsers);

Fuente

Fuente

Gracias por llegar hasta el final de este blog quiero recordarte que todo esto es gratis y posible gracias a que tu compartes. Un fuerte abrazo y recuerda que el conocimiento es poder.

Invertir en conocimientos produce siempre los mejores beneficios. (Benjamín Franklin)
Plataforma de cursos gratis sobre programación

Artículos Relacionados

@for vs *ngFor - Angular 17
· 3 min de lectura
Transformando Input en Angular
· 4 min de lectura