• Home
  • About
    • WalkingRiver.com photo

      WalkingRiver.com

      The best software is both elegant and beautiful, and seldom what you expect.

    • Learn More
    • Twitter
    • Facebook
    • LinkedIn
    • Github
    • Youtube
  • Posts
    • All Posts
    • All Tags
  • Projects
  • Courses
  • Speaking
  • Contact

Angular ActivatedRoute to Page Route

01 Feb 2021

Reading time ~4 minutes

How to tie changes to an HTML dropdown (select) to your page’s URL and reactively respond to those changes, without forcing a page reload. Angular’s ActivatedRoute and some basic RxJS make it easy to do.

Angular

Background

On my page I have a dropdown control representing a set of “stores” in the application. When the user selects a new store from the dropdown, information about the newly-selected store is retrieved from a remote service and displayed on the screen.

<select (change)="storeChanged($event)">
  <option *ngFor="let store in stores" [value]="store.id">
    
  </option>
</select>

Wiring that up to the dropdown was as simple as implementing the appropriate event handler.

storeChanged(event) {
  const storeId = event.detail.value;
  this.storeInfo = this.storeService.getStore(storeId);
}

Page Refresh

As simple as this approach is, it does not lend itself to page refresh or bookmarking in the browser. The page URL never changes, so if you refresh the page, you have to select your store again. I admit that this is a minor annoyance, but what if there were a straightforward way to make that happen?

ActivateRoute and Router

If your first thought was to change the page’s route to include the store ID, give yourself a pat on the back. This approach makes a minor change to the dropdown’s change event handler, and then sets up a subscription on the ActivatedRoute parameters. And as you will see shortly, the component will also navigate to a new route using the Angular Router.

Before making any further changes to the code, there are two classes that need to be imported at the top of the component file.

import { ActivatedRoute, Router } from "@angular/router";

Both then need to be injected into the component’s constructor.

constructor(
  private route: ActivatedRoute,
  private router: Router,
  . . . // Other services injected
  ) {

Change the Route

The next thing to do is to change the page’s route to include the storeId. This will end up looking something like this in your routing module.

// Before
  path: 'stores',
  component: StoreComponent
},

// After
{
  path: 'stores/:storeId',
  component: StoreComponent
},
{
  path: 'stores',
  component: StoreComponent
},

Place the original route without the storeID as the second route so that a route like /stores/circuit-city/ is matched before /stores.

Update the Change Event Handler

Next, update the dropdown component’s change event handler to navigate to a new URL when the dropdown changes. It should end up looking something like this.

storeChanged(event) {
  const storeId = event.detail.value;
  if (storeId) {
    return this.router.navigateByUrl(`stores/${storeId}`);
  }

  return this.router.navigateByUrl('stores');
}

The event handler first checks to see whether the selection has a value. If it does, it calls the Angular Router to navigate to the URL that includes the storeId. Otherwise, it navigates to the default route without the storeId.

The code that used to be there to load the selected store’s details will be moved, as you will see next.

Subscribe to the ActivatedRoute

To get everything working together, it is necessary to detect and respond to changes to the page’s route. Angular will not force a page reload. You could do that, of course, but it would require bootstrapping of your entire application, and is absolutely the wrong thing to do here.

Instead, take advantage of the fact that ActivatedRoute provides a number of Observables to which you can subscribe. In this case, the only thing we care about are the route parameters, which are part of ActivatedRoute.params.

We can set up a subscription inside of ngOnInit.

ngOnInit(): {
  this.selectedStoreSubscription = this.route.params.subscribe(params => {
    const storeId = params.storeId || '';

    if (storeId) {
      this.storeInfo = this.storeService.getStore(storeId);
    } else {
      // Clear existing results
      this.storeInfo = null;
    }
  });
}

Now, any time the route changes, the subscription will receive a new set of params. If the params contain a storeId, it will load the new store. Otherwise, it will clear the existing storeInfo.

Unsubscribe from the subscription

Because I am subscribing to an Observable with no specific completion, it is necessary to unsubscribe when the component is destroyed. For that, I can simply unsubscribe in the ngOnDestroy function.

ngOnDestroy() {
  if (this.selectedStoreSubscription) {
    this.selectedStoreSubscription.unsubscribe();
  }
}

Conclusion

After writing this code and being satisfied that it works as expected, I wondered whether or not I could have gotten similar results with an Angular async pipe, and completely dispensed with the subscription. I decided that even though it might be possible, it was probably not desirable.

Almost every detail of the storeInfo would be data-bound on HTML view. I probably could have made it work, but this code is simple enough that I am happy with it as-is. It is easy to understand and easy to test, and there is no reason to change that.

Angular Advocate

Angular Advocate Book

If you are interested in more content like this, please consider my recently-released book, Angular Advocate: How to Awaken the Champion Within and Become the Go-to Expert at Work, available in a Kindle Edition at Amazon or DRM-free on Gumroad.


Do you have any comments, questions, or just want to see more? Please follow me on Twitter and let me know.

Did I make any mistakes in this post? Feel free to suggest an edit.




AngularRoutingRxJS Share Tweet +1