Obtener parámetros de la URL como @Input (Angular 16+)
Tags: angular, webdev, programming
Published: 2024-04-16 18:56:08

A partir de `Angular 16` podemos obtener desde un componente la información de una ruta simplemente utilizando `@Input`. En este artículo veremos el funcionamiento de este nuevo procedimiento, así como algunas consideraciones que tendremos que tener en cuenta a la hora de utilizarlo para evitar problemas potenciales. 

> Si lo prefieres, el contenido de este artículo también lo tienes en formato video [aquí](https://youtu.be/Ozm4VOgYIQE).

{% embed https://youtu.be/Ozm4VOgYIQE %}

---

## Tipos principales de información en las Rutas

A la hora de desarrollar una aplicación, es muy común tener que lidiar con rutas que definen cierta información de forma dinámica.

En Angular podemos destacar tres tipos principales de este tipo de información:

- Los **parámetros de ruta**.
- Los **parámetros de consulta**.
- Y una serie de datos adicionales que puede tener asociada una ruta, tanto de forma manual en su propiedad **data** como a través de **resolvers.**

```tsx
const routes = [
  { 
    path: 'products/:productId',  // :productId  es un parámetro de ruta
    component: ProductDetailsComponent,
    data: {
      experimental: true
    },
    resolve: {
      product: productResolver
    }
  },
  ...
]

// Los parámetros de búsqueda se encuentran después del '?' en la URL
// en este caso serían q y maxPrice
'myshop.com/catalog?q=teclado&maxPrice=100'  
```

---

## Procedimiento Tradicional

El procedimiento más habitual para obtener esta información desde un componente consiste en:

- Inyectar la ruta actual (`ActivatedRoute` / `ActivatedRouteSnapshot`)
- Y extraer la información a través de sus propiedades `params`, `queryParams` y `data`

```tsx
// Información en forma de Observables
export class ProductDetailsComponent {
  route = inject(ActivatedRoute);
  
  productId$ = this.route.params.pipe(map(params => params['productId']));
}
```

---

## Nuevo Procedimiento

Bien pues a partir de `Angular 16`, esta misma información también la podremos obtener simplemente añadiendo un `@Input` en el componente **cuyo nombre coincida con el del parámetro o data** que queramos obtener.

```tsx
export class ProductDetailsComponent {
  @Input() productId;  
}
```

> En el caso de que tuviéramos un ruta del tipo `catalog?q=teclado` con un parámetro de consulta `q` y quisiéramos vincularlo con una propiedad más descriptiva, podríamos conseguirlo indicando el nombre real del parámetro en el alias del `@Input`.
> 
> 
> ```tsx
> @Input('q') query;
> ```
> 

---

## ¿Cómo activarlo?

Esta nueva característica no viene activada por defecto, por lo que tendremos que activarla manualmente en la configuración del `Router`.

Si estamos usando la **configuración modular** esto lo podremos conseguir asignando a `true` la opción `bindToComponentInputs` en el método `forRoot`.

```tsx
@NgModule({
  imports: [
    RouterModule.forRoot(
      routes, 
      { bindToComponentInputs: true } // <---
    )
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}
```

Y en el caso de estar usando la **configuración standalone**, la podremos activar usando la función `withComponentInputBinding()` a la hora de proveer el Router.

```tsx
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
  ],
};
```

Una vez activada la característica el Router vinculará automáticamente todos los parámetros y data de las rutas con los inputs del componente cuyo nombre coincida con dichos parámetros.

---

## Orden de prioridad

En el caso de tener múltiples parámetros o data con un mismo nombre.

```tsx
// sameName para todos ellos 
{ 
  path: 'somepath/:sameName',
  data: {
    sameName: true
  },
  resolve: {
    sameName: someResolver
  }
}

'somepath?sameName=aValue'
```

El orden de prioridad que sigue el `Router` a la hora de vincular el `@Input` es el siguiente:

1. Resolvers
2. Data
3. Parámetro de Ruta
4. Parámetro de Consulta

---

## Consideraciones a tener en cuenta

### Los valores de los `@Input` no están asegurados hasta `ngOnInit`

Partiendo del siguiente ejemplo con la configuración tradicional:

```tsx
export class ProductDetailsComponent {
  id$ = this.route.params.pipe(
    map((params) => params['id'])
  );
  product$ = this.id$.pipe(
    switchMap((id) => this.catalogService.getProduct(id))
  );
  
  constructor(
    private catalogService: CatalogService,
    private route: ActivatedRoute
  ) {}
}
```

Podríamos estar tentados de refactorizarlo con el nuevo procedimiento de la siguiente manera: 

```tsx
export class ProductDetailsComponent {
  @Input() id; // <- extraemos el id usando el input
  product$ = this.catalogService.getProduct(this.id); //<- utilizamos directamente el id
  
  constructor(private catalogService: CatalogService) {}
}
```

Pero los valores de los `@Input` en Angular no están garantizados hasta que el componente haya sido inicializado, o lo que es lo mismo hasta que se ejecute el *lifecycle hook* de `ngOnInit`. 

Por tanto en este caso tendríamos que mover la inicialización de la propiedad `product$` a dicho hook.

```tsx
export class ProductDetailsComponent implements OnInit{
  @Input() id;
  product$: Observable<Product | null>;
  
  constructor(private catalogService: CatalogService) {}
  
  ngOnInit() {
    this.product$ = this.catalogService.getProduct(this.id);
  }
}
```

---

### Podemos perder la reactividad en rutas reutilizables

La opción anterior de usar el `ngOnInit` para la inicialización de las propiedades dependientes de `@Input` funcionaría sin problemas en la mayoría de los casos.

Pero en los casos en los que el Router reutilice el componente para la siguiente navegación (ej.- si navegamos directamente de los detalles de un producto a los detalles de otro), como el `ngOnInit` no se ejecutaría para esa segunda navegación, la propiedad del producto no se reasignaría.

En casos simples como este podríamos solucionarlo simplemente sustituyendo la propiedad `id`por un setter y mover ahí la asignación del producto.

```tsx
export class ProductDetailsComponent {
  @Input() set id(newId) {
    this.product$ = this.catalogService.getProduct(newId);
  }
  product$: Observable<Product | null>;
  
  constructor(private catalogService: CatalogService) {}
}
```

---

### Para casos complejos mejor usar inputs como Señales (v17.1+)

Para casos más complejos en los que tengamos múltiples pasos dependientes de los inputs, podríamos también usar los setters, pero el riesgo de convertir la lógica en ilegible es alto, sobre todo si la complejidad de la lógica es alta.

```tsx
// routes.ts
{
  path: 'catalog',
  component: CatalogComponent,
  resolve: {
    products: allProductsResolver,
  },
},

// URL example: myshop.com/catalog?f=teclado&orderBy=priceASC
export class CatalogComponent {
  @Input() products!: Products; // <- extraido del resolver
  
  //f y orderBy extraidos de los parámetros de consulta
  @Input('f') set nameFilter(val: string) {
    this.filteredProducts = filterByProductName([this.products, val ?? null]);
    this.orderBy = this._order;
  }

  _order: string = '';
  @Input() set orderBy(val: string) {
    this._order = val;
    this.orderedProducts = sortByPrice([this.filteredProducts, val ?? null]);
  }

  filteredProducts: Products = [];
  orderedProducts: Products = [];

  constructor(private route: ActivatedRoute) {}

}
```

Para estos casos lo más recomendable es usar una versión declarativa. Teniendo como opciones: los observables y las señales.

En el caso de los observables, la mejor opción bajo mi punto de vista, es olvidarnos de los Inputs y utilizar el procedimiento anterior, extrayendo la información desde `ActivatedRoute` . Podríamos usar un versión mixta utilizando `BehaviorSubjects` y asignándolos desde los setters de los Inputs. 

```tsx
export class CatalogComponent {
  @Input() products!: Products;

  _nameFilter$ = new BehaviorSubject('');
  @Input('f') set nameFilter(val: string) {
    this._nameFilter$.next(val);
  }

  _order$ = new BehaviorSubject('');
  @Input() set orderBy(val: string) {
    this._order$.next(val);
  }

  filteredProducts$?: Observable<Products>;
  orderedProducts$?: Observable<Products>;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    this.filteredProducts$ = combineLatest([
      of(this.products),
      this._nameFilter$,
    ]).pipe(map(filterByProductName));

    this.orderedProducts$ = combineLatest([
      this.filteredProducts$,
      this._order$,
    ]).pipe(map(sortByPrice));
  }
}
```

Pero lo único que conseguiríamos es complicar innecesariamente el código sin ningún beneficio. Por lo que bajo mi punto de vista no merece la pena.

La opción que si merece la pena y tenemos disponible desde la `v17.1+` es la de usar los **inputs como señales**. El funcionamiento de estos es igual que el de los `@Input` tradicionales, pero exponen los valores a través de una señal.

```tsx
// usando el decorador
@Input() id: string;

// usando la función input (con i minúscula)
id = input<string>(); // id sería del tipo InputSignal<string>
```

Usando estos inputs en conjunto con las señales computadas el ejemplo quedaría de la siguiente manera:

```tsx
export class CatalogComponent {
  products = input<Products>();
  nameFilter = input<string>('', { alias: 'f' }); //<-- también podemos usar alias
  orderBy = input<string>('');

  filteredProducts = computed(() =>
    filterByProductName([this.products(), this.nameFilter()])
  );
  orderedProducts = computed(() =>
    sortByPrice([this.filteredProducts(), this.orderBy()])
  );
}

```

Siendo esta, bajo mi punto de vista, una de las opciones más simples y elegantes que hay disponibles ahora mismo.

---

### A simple vista no sabemos de donde procede el valor del Input

Uno de los ‘problemas’ a la hora de usar este nuevo procedimiento, es que a primera vista no sabemos si los valores de los inputs definidos en un componente vienen de un supuesto padre o si son extraídos de alguna parte de la ruta.

Técnicamente la procedencia del valor es irrelevante para el correcto funcionamiento del componente. Pero si queremos ser más claros a la hora de definir nuestros inputs, tenemos la opción de usar alias a la hora de importar las dependencias `@Input` e `input` desde `@angular/core` .

```tsx
import {..., Input as RouteParam } from '@angular/core'

export class ProductDetailsComponent {
  @RouteParam() set id(newId) {
    this.product$ = this.catalogService.getProduct(newId);
  }
  
  ...
}
```

```tsx
import { 
  ...,
  input as resolvedData,
  input as queryParam,
} from '@angular/core';

export class CatalogComponent {
  products = resolvedData<Products>();
  nameFilter = queryParam<string>('', { alias: 'f' }); //<-- también podemos usar alias
  orderBy = queryParam<string>('');
  
  ...
}
```

---

## Conclusión

Este nuevo procedimiento nos puede ser muy útil a la hora de simplificar la extracción de la  información de las rutas, pero deberemos tener cuidado a la hora de elegir la implementación para evitar posibles errores potenciales.

---

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