Lessons learned while upgrading from AngularJS UI-Router to Angular Router
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
As Peter Rabbit learned his lesson in the tale about him by Beatrix Potter, so did I while migrating my app routing from AngularJS UI Router to new Angular Router. Hard lessons, that cost Peter his shoes and blue jacket and me long hours of work. But, we both came out a bit wiser and more prepared for future challenges.
Now after a month of work I’m finally done with the migration. So I want to share with you what I’ve learned. There are cases with the new Router that might seem obvious at first, but which will be as tricky as Mr. McGregor’s vegetable garden once you try to implement them.
Mind you, I’m not here to tell you how to setup the Router, its underlying ideas or API. There are a bunch of good resources around. Rather, I want to share a couple of stories that can happen to any developer migrating his/her app routing.
The App Itself: Let’s say I have a Rabbit Listing App where I can manage my rabbits: add newborn ones, remove those that ran away to the forest, edit their profiles when someone gets fatter or changes color as the seasons change. Keep the bunnies in mind and take a walk with me through the stories.
Story I. How to navigate back programmatically to the previous page?
There is a dashboard full of rabbits, when you click on a rabbit in the list illustrated above — you’re navigated to her profile. Then there is a little back button which, no wonder, when you click it navigates you back to the dashboard. But how can we implement this first trivial behavior? The trick is to leverage Observables and the RxJS pairwise operator, subscribe to router events, and filter only those that identify successful navigation (NavigationEnd). Pairwise gives you the current and previous value, and consequently you will have access to current and previous router events. The first one from the pair holds your previous URL. Check out the code:
The solution was inspired by this Stack Overflow answer.
Story II: About how to redirect to one of child routes by default when a user navigates to the parent route.
It’s still the same furry dashboard. But now, we’d like to see our long eared friends in two ways: a list and tiles. Here is how the configuration looks like:
When you navigate to the dashboard using URL /dashboard you want to go to /dashboard/list by default. Thus, somehow the router setup should be modified to redirect to the child list route when the parent dashboard route is navigated. The solution is to add one more child route setup that has merely a technical purpose. The way the Router works is that when a parent is matched that doesn’t have an associated component or redirection, it starts traversing its children. The first (technical) child will match because it has an empty path and pathMatch = ‘full’. But it also redirects to its sibling route. And that’s what we wanted.
Story III: How to configure the router to navigate to the same URL with or without a parameter?
When I press the Add Rabbit button on the dashboard I’m navigated to the page with url rabbitapp.fun/rabbit. After I press the Save Rabbit Info button, it will be available via rabbitapp.fun/rabbit/:newRabbitId. So I want some router config that will allow us to navigate to the same URL with or without route params (rabbit and rabbit/:rabbitId). This is a fairly common case that takes you to whichever cute creature you want to manage. So I’ve configured the route:
and expected the router to use it whether I navigate to /rabbit url (create new rabbit) or /rabbit/:rabbitId (edit rabbit info). But, while navigating to bare rabbit without id I was redirected to my default redirect URL. After I’ve commented out my default redirect URL I got a run-time error “Cannot match any routes” (which, of course, means the route didn’t match).
I was quite surprised to learn that I’ll have to duplicate my routes’ setup and create one route config with the rabbitId param and another one without. The inconvenient point here is the child routes setup. They will have to be duplicated too. And as we don’t particularly like code repetition and humongous configs I’d recommend extracting the child routes’ config to a separate object.
Small Tip ^^: Interestingly, there is also another way: you can have a single route config and pass some default value for the parameter and check it in your component to decide which flow to activate — with or without the parameter.
Story IV: How to get all URL parameters?
Suppose you want to learn about a baby rabbit’s game preferences. In the app you navigate to the following URL: rabbitapp.fun/rabbit/:parentId/babies/:babyId/leisure&game=HippityHop. The router config for the page showing this will be quite lengthy and will have lots of parameters:
Now, in your LeisureComponent you want to get these params — rabbitId, babyId as well as a game query parameter. So what’s the code to get ALL parameters from the URL? The naive solution I started with was to extract parameters from ActivatedRoute and its parents:
But, then it became annoying to figure out each time how deep those parameters are hidden. I wanted something neat, like:
So for convenience, I’ve implemented this functionality. You can use it and modify it to your needs.
In short, getRouteParams iterates over the passed array of parameters. And for each of them it recursively calls the getParam function that will search in the route params or queryParams. And, if it is not found — in the parent params and so on. It returns the object where param name is a key and value is what it is. ;)
The only downside I know is that you can’t have same the param names (which is good practice, by the way).
Add those methods to your component directly or to the dedicated service as I did. It’s up to you.
Story V: How to reload current route?
Once my rabbit turns 1 year old, his info has to be shown with a completely different GUI than that of a baby rabbit. So on a happy day I go to my birthday rabbit edit page rabbit/:birthdayBunnyId and change his age. After a request to the server is done I want to fully reload the page and show a different set of widgets that are more proper for the adult rabbit. But as the URL stays the same — nothing happens. If you have a background in AngularJS and the UI Router you will remember the $state reload parameter that will force component re-initialization.
We want the same behavior with the new Router. If you have Angular 5.1 or higher — you’re good . You can add the onSameUrlNavigation prop to the router configuration. I’ve found this article. Hopefully, it will be helpful. If not — you’re in a bit of trouble, just like me. The problem is that when you use router.navigate or router.navigateByUrl and go to the same URL you’re currently on, it will have no effect, meaning that the component associated with the route won’t be refreshed.
How should you reload the current route to the update page data until you migrate to the newer Angular? You can use this hack. Remember, I have RouteService in my application. I added the reloadCurrentPage method to it. The function signature is the same as for router.navigate.
Then you inject the service and call its method when you’d like to reload the current page and get fresh data. In case of the birthday rabbit it is something like this:
If you want to know more details and what inspired the solution , check out this discussion.
Story VI: How to detect that the user is navigating to a certain page?
You’re surfing the app, enjoying the pictures of baby rabbits. On the rabbit details page there is a CarrotNibbleWidget that will display a number of carrots that a bunny eats in one meal time. The same widget is shown on adult, teenage and baby pages. Teenage bunnies have a huge appetite and may fake that they are starving when they are not. So as not to overfeed them, I want to display a red warning message on a teenage page stating: “There is no mistake here, a rabbit can have only one carrot per meal. S/he is not faint or dizzy because of hunger, only trying to outwit you!!!”. So how can we detect that the user is navigating to a certain page, or that the widget is currently shown on the teenage page and not any other and this is a proper place to display a message? — Certainly, you can get the Router instance and current URL:
But how do you know whether it’s the teenage page or the adult page? Definitely, you can come up with some regexp that will check your URL against what is expected, but that’s a bit complicated and not worth it. The solution I prefer is to use route data. Along with the router config I’ve added an enumeration of the application pages:
And then I add page to route data:
Now I import APP_PAGES into CarrotNibbleWidget, get the data from activatedRoute.snapshot and compare it with the expected one from the enumeration.
And this is it.
Reached here?— I’m proud to be helpful. If you enjoyed my bunny stories or found some useful tips — use that clap button below 👏👏👏. They were inspired by The Tale of Peter Rabbit which I’ve mentioned in the beginning and recently discovered for myself not at a proper age but with the adult appreciation of the illustrations and the story itself. If you have questions, find mistakes or would like to propose other points to highlight — you’re welcome to leave comments. Hopefully, I saved you some time for chamomile tea and blueberries.