DEV Community

Cover image for Simplify Routing Parameters in Angular Components
Dany Paredes
Dany Paredes

Posted on

Simplify Routing Parameters in Angular Components

In my previous article on building a sample store, I explored the animation feature in Angular 17. At the same time, I also developed an extensive product page feature.

The product detail page needs to extract the product ID from the URL using the router and then use this ID to fetch product information from another API, similar to the following:

Animation
To achieve this, I need to configure the ActivatedRoute in conjunction with the RouterLink. Let's get started.

Configure Route and RouterLink

Create your component product-detail using the angular CLI:

ng g c pages/product-detail
Enter fullscreen mode Exit fullscreen mode

In my app.routes.ts, I added the new path using :id to represent the parameter and the loadComponent.

  {
    path:'products/:id',
    loadComponent: () =>
        import('./pages/product-detail/product-detail.component').then(c => c.ProductDetailComponent)
  }
Enter fullscreen mode Exit fullscreen mode

In my code, I added the routerLink directive to direct users to the product details page, incorporating both the URL and the parameter.

[routerLink]="['/products/', product.id]
Enter fullscreen mode Exit fullscreen mode

In my template, it appears as follows:

@for (product of products();track product.id) {
  <kendo-card width="360px" [routerLink]="['/products/', product.id]"
              style="cursor: pointer">
    <img [src]="product.image" kendoCardMedia alt="cover_img"/>
    <kendo-card-body>
      <h4>{{ product.title }}</h4>
    </kendo-card-body>
    <kendo-card-footer class="k-hstack">
      <span>Price {{ product.price | currency }}</span>
    </kendo-card-footer>
  </kendo-card>
} @empty {
  <h2> No products! 😌</h2>
}
Enter fullscreen mode Exit fullscreen mode

Read The Router Parameter

In the details page, we need to inject both ActivatedRoute and HttpClient. In my case, I make the request within the same component.

Using route.paraMap with combination of switchMap, obtain the parameter and make the request to return the product. To skip the subscription or use the pipe async, I use the toSignal, and convert the observable from httpClient to a signal, which can then be used in the template.

To keep this article concise, the request is made within the same component, although using an external service is recommended.


  route = inject(ActivatedRoute)
  http = inject(HttpClient);

  $product = toSignal<Product>(this.route.paramMap.pipe(
    switchMap(params => {
      const id = params.get('id');
      return this.http.get<Product>(`https://fakestoreapi.com/products/${id}`)
    })
  ));
Enter fullscreen mode Exit fullscreen mode

In the template, we read the signal by using () to gain access to the returned product.

@if ($product()) {
    <kendo-card width="360px" >
      <img [src]="$product()?.image" kendoCardMedia alt="cover_img"/>
      <kendo-card-body>
        <h4>{{ $product()?.title }}</h4>
        <hr kendoCardSeparator/>
        <p>
          {{ $product()?.description }}
        </p>
      </kendo-card-body>
      <kendo-card-footer class="k-hstack">
        <span>Price {{ $product()?.price | currency }}</span>
      </kendo-card-footer>
    </kendo-card>
  } @else {
  <h2> Sorry product not found 😌</h2>
}
Enter fullscreen mode Exit fullscreen mode

Save, and everything should work! But is there a better or more concise way to do it? Since Angular 16.1, we can use the bind Input property that matches with the parameter. Let's give it a try!

Using Input with Router Parameter.

First we must to provide withComponentInputBinding() function into the router config. it enable to bind the input that match with the route parameter, in our case is id .

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

In the product-details, add the id property with the input decorator, and then use the id to make the request. To ensure the id is set, print it in the constructor.

export class ProductDetailComponent {
  #http = inject(HttpClient);
  @Input() id!: string;
  $product = toSignal<Product>(this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`));

  constructor() {
    console.log(this.id);
  }
Enter fullscreen mode Exit fullscreen mode

The id is undefined, which causes the product to be requested with an undefined value, because the id has not yet been set in the component. To access it, let's try reading it in the OnInit lifecycle.

empty

Indeed, it received the id, but the toSignal() function needs to run within the injectionContext(). If you've read my previous article on Understanding InjectionContext, you'll recognize that we're facing a similar situation.

demo

To resolve this issue, we need to inject the Injector and impor trunInInjectionContext.

  #http = inject(HttpClient);
  #injector = inject(Injector);
Enter fullscreen mode Exit fullscreen mode

To make the code cleaner, move the request to a new function called getProductDetail.

private getProductDetail() {
    runInInjectionContext(this.#injector, () => {
      this.$product = toSignal(
        this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`),
      );
    });
  }
Enter fullscreen mode Exit fullscreen mode

The final code will look like this:

export class ProductDetailComponent implements OnInit {
  @Input() id!: string;
  $product!: Signal<Product | undefined>;
  #http = inject(HttpClient);
  #injector = inject(Injector);

  ngOnInit(): void {
    this.getProductDetail();
  }

  private getProductDetail() {
    runInInjectionContext(this.#injector, () => {
      this.$product = toSignal(
        this.#http.get<Product>(`https://fakestoreapi.com/products/${this.id}`),
      );
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Save the changes, and everything should work! Yay!!!

Recap

The feature withComponentInputBinding helps us avoid subscriptions and inject the route and get the values from route easy. But of course if the process the data is bit complex, I highly recommend move the logic to a service.

I recommend checkout the official documentation

Happy Coding 😁

Top comments (0)