DEV Community

Cover image for Obtener parámetros de la URL como @Input (Angular 16+)
akotech
akotech

Posted on

Obtener parámetros de la URL como @Input (Angular 16+)

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


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.
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'  
Enter fullscreen mode Exit fullscreen mode

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
// Información en forma de Observables
export class ProductDetailsComponent {
  route = inject(ActivatedRoute);

  productId$ = this.route.params.pipe(map(params => params['productId']));
}
Enter fullscreen mode Exit fullscreen mode

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.

export class ProductDetailsComponent {
  @Input() productId;  
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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.

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes, withComponentInputBinding()),
  ],
};
Enter fullscreen mode Exit fullscreen mode

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.

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

'somepath?sameName=aValue'
Enter fullscreen mode Exit fullscreen mode

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:

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
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

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

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) {}
}
Enter fullscreen mode Exit fullscreen mode

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.

export class ProductDetailsComponent implements OnInit{
  @Input() id;
  product$: Observable<Product | null>;

  constructor(private catalogService: CatalogService) {}

  ngOnInit() {
    this.product$ = this.catalogService.getProduct(this.id);
  }
}
Enter fullscreen mode Exit fullscreen mode

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 idpor un setter y mover ahí la asignación del producto.

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

  constructor(private catalogService: CatalogService) {}
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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) {}

}
Enter fullscreen mode Exit fullscreen mode

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.

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));
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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>
Enter fullscreen mode Exit fullscreen mode

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

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()])
  );
}

Enter fullscreen mode Exit fullscreen mode

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 .

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

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

  ...
}
Enter fullscreen mode Exit fullscreen mode
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>('');

  ...
}
Enter fullscreen mode Exit fullscreen mode

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


YouTube · X · GitHub

Top comments (0)