@Input ({ transform })
Tags: angular, webdev, spanish
Published: 2024-04-26 08:31:39

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. 

```tsx
@Input({ transform: (value: any) => SomeType) }) aProperty: SomeType
```

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í.](https://youtu.be/8czEFI4EMqg)_
> 
> 
> {% embed https://youtu.be/8czEFI4EMqg %}
> 

---

## `@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`

```tsx
@Input() disabled: boolean;
```

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

```tsx
//❌ 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" /> // ✅
```

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.

```tsx
@Input() disabled: '' | 'disabled' | boolean;
```

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.

```tsx
_disabled: boolean = false;

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

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

```tsx
@Input({ transform: (value:any) => any })
```

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

```tsx
export function booleanAttribute(value: unknown): boolean {
  return typeof value === 'boolean' ? value : (value != null && value !== 'false');
}
```

```tsx
export function numberAttribute(value: unknown, fallbackValue = NaN): number {
  const isNumberValue = !isNaN(parseFloat(value as any)) && !isNaN(Number(value));
  return isNumberValue ? Number(value) : fallbackValue;
}
```

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

```tsx
//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;
}
```

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. 

```tsx
//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;
}
```

---

## 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 `string`en un objeto `Date`**

```tsx
//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;
}
```

**Forzar que el input sea siempre un Array**

```tsx
//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[];
}
```

***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](https://www.paypal.com/donate/?hosted_button_id=8HJNHL8VTB8ZG)*

---

[YouTube](https://youtube.com/akotech) · [X](https://twitter.com/_akotech) · [GitHub](https://github.com/ako-tech)