@Input ({ transform })

akotech

akotech

Posted on April 26, 2024

@Input ({ transform })

A partir de Angular 16.1 tenemos una nueva opción transform en los @Input, que nos permite transformar los valores recibidos antes de que estos sean asignados en la propiedad.

@Input({ transform: (value: any) => SomeType) }) aProperty: SomeType
Enter fullscreen mode Exit fullscreen mode

En este artículo hablaremos de su funcionamiento y veremos cual es la problemática principal que viene a solucionar.

Si lo prefieres, el contenido de este artículo también puedes encontrarlo en formato video aquí.


@Input que no son string

Los inputs son una pieza fundamental ya que nos permiten pasar información desde un componente padre a un hijo.

Pero tienen una limitación y es que si declaramos el input con un tipo distinto a string estamos obligados a usar el formato del vínculo de propiedades.

Si tomamos como ejemplo un input disabled del tipo boolean

@Input() disabled: boolean;
Enter fullscreen mode Exit fullscreen mode

Lo ideal sería poder asignar este input utilizando cualquiera de los siguientes formatos:

//❌ Error: '""' no se puede asignar a boolean
<ako-child disabled /> 

//❌ Error: '"disabled"' no se puede asignar a boolean
<ako-child disabled="disabled" />

<ako-child [disabled]="true" /> // ✅
<ako-child [disabled]="false" /> // ✅
Enter fullscreen mode Exit fullscreen mode

Pero si intentamos asignarlo usando el atributo directamente, como se pasan siempre como string, nos dará error.


La solución naíf

En principio, la solución parece sencilla. Simplemente añadimos esos dos string literales al tipo y problema solucionado.

@Input() disabled: '' | 'disabled' | boolean;
Enter fullscreen mode Exit fullscreen mode

Pero esto nos obligaría a que cada vez que esta propiedad fuera usada, tener que implementar una lógica que chequeara los diferentes tipos. Algo realmente tedioso.


Usando un setter

Tanto es así, que la solución de facto hasta ahora cuando se daba este tipo de situaciones era:

  • Añadir una propiedad para uso interno marcándola con el tipo deseado.
  • Asociar el @Input a un setter que aceptara todos esos diferentes tipos de valores, los transformara al tipo deseado y guardara dicho valor en dicha propiedad de uso interno.
_disabled: boolean = false;

@Input() set disabled(value: '' | 'disabled' | boolean | undefined) {
  this._disabled = typeof value === 'string' ? true : !!value;
}
Enter fullscreen mode Exit fullscreen mode

Aunque este procedimiento en sí es sencillo, según el número de @Input va aumentando, nos hace incrementar las líneas de código de nuestra aplicación sin agregar ningún valor añadido.


Las funciones transformadoras (16.1+)

Es principalmente por esto por lo que Angular en la versión 16.1 ha introducido en los @Input una nueva opción transform en la cual podremos asignar una función transformadora.

@Input({ transform: (value:any) => any })
Enter fullscreen mode Exit fullscreen mode

Estas funciones tendrán que definir un parámetro por el que recibirán el valor original pasado desde el padre. Y devolver el valor transformado, el cual será finalmente asignado en la propiedad asociada al input.

La función transformadora tendrá que ser a su vez :

  • Ligera, para que su ejecución sea rápida.
  • Y Pura, para no generar efectos secundarios fuera de la misma.

Funciones ya incluidas en el framework

No poder usar los atributos directamente para asignar boolean y number son los casos más comunes en los que esta limitación de los @Input se hará presente. Por ello el framework ya nos ofrece de entrada las funciones transformadoras para estos dos formatos.

export function booleanAttribute(value: unknown): boolean {
  return typeof value === 'boolean' ? value : (value != null && value !== 'false');
}
Enter fullscreen mode Exit fullscreen mode
export function numberAttribute(value: unknown, fallbackValue = NaN): number {
  const isNumberValue = !isNaN(parseFloat(value as any)) && !isNaN(Number(value));
  return isNumberValue ? Number(value) : fallbackValue;
}
Enter fullscreen mode Exit fullscreen mode

Ambas disponibles en el paquete @angular/core

Precaución al usarlas

Estas funciones se han creado de una manera genérica para cubrir el mayor número de casos posibles, por lo que es importante tener en cuenta la implementación de las mismas si vamos a usarlas tal cual.

Y es que si nos fijamos, vemos como ambas marcan su parámetro de entrada como del tipo unknown, lo que permitiría que el padre pasara cualquier tipo de valor por ese input.

//parent.component.html
<ako-child disabled="someRandomValue" /> // ✅

<ako-child [disabled]="{ some: 'Value' }" /> // ✅
<ako-child [disabled]="4405" /> // ✅

//child.component.html
@Component(...)
export class ChildComponent {
  @Input({ transform: booleanAttribute}) disabled: boolean = false;
}
Enter fullscreen mode Exit fullscreen mode

Raro será que alguien al usar un @Input supuestamente booleano intente pasar un número o un objeto, pero si queremos asegurar un uso correcto de nuestros inputs siempre podemos crear nuestras propias funciones transformadoras que sean más restrictivas.

//parent.component.html
<ako-child disabled="disabled" /> // ✅
<ako-child disabled="someRandomValue" /> // ❌

<ako-child [disabled]="true" /> // ✅
<ako-child [disabled]="false" /> // ✅
<ako-child [disabled]="{ some: 'Value' }" /> // ❌
<ako-child [disabled]="4405" /> // ❌

//child.component.html
function disabledAttribute(value: '' | 'disabled' | boolean) {
  return booleanAttribute(value);
}

@Component(...)
export class ChildComponent {
  @Input({ transform: disabledAttribute}) disabled: boolean = false;
}
Enter fullscreen mode Exit fullscreen mode

Otros usos

Salvar la limitación a la hora de usar atributos para inputs que no sean string es uno de los principales objetivos de esta nueva funcionalidad. Podemos usarlas para cualquier otro tipo de transformación. Por ejemplo:

Transformar un stringen un objeto Date

//parent.component.html
<ako-child date="2024-04-25" />

//child.component.html
function dateAttribute(value: string): Date {
  return new Date(value);
}

@Component(...)
export class ChildComponent {
  @Input({ transform: dateAttribute}) date?: Date;
}
Enter fullscreen mode Exit fullscreen mode

Forzar que el input sea siempre un Array

//parent.component.html
<ako-child aProperty="100" />
<ako-child [aProperty]="[100,200,300]" />

//child.component.html
function toArray(value: unknown | unknown[]): unknown[] {
  return Array.isArray(value) ? value : [value];
}

@Component(...)
export class ChildComponent {
  @Input({ transform: toArray }) aProperty?: unknown[];
}
Enter fullscreen mode Exit fullscreen mode

Y otros muchos más…


Conclusión

En definitiva esta nueva opción transform nos permite aplicar esas transformaciones a los valores recibidos por los @Input de una manera mucho más sencilla, limpia y reutilizable.


Si deseas apoyar la creación de más contenido de este tipo, puedes hacerlo a través nuestro Paypal


YouTube · X · GitHub

💖 💪 🙅 🚩
akotech
akotech

Posted on April 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related