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'
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
ydata
// 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.
export class ProductDetailsComponent {
@Input() productId;
}
En el caso de que tuviéramos un ruta del tipo
catalog?q=teclado
con un parámetro de consultaq
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 {}
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()),
],
};
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'
El orden de prioridad que sigue el Router
a la hora de vincular el @Input
es el siguiente:
- Resolvers
- Data
- Parámetro de Ruta
- 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
) {}
}
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) {}
}
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);
}
}
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.
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.
// 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.
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.
// 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:
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
.
import {..., Input as RouteParam } from '@angular/core'
export class ProductDetailsComponent {
@RouteParam() set id(newId) {
this.product$ = this.catalogService.getProduct(newId);
}
...
}
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
Top comments (0)