Angular Router killed my links

5 Jul

I’ve just finished a 700h Angular 2/4 project on my own and I was bit by Angular more than once either because there were bugs, bad design, poor documentation, or assumptions that were made incorrectly.

In this post, I am talking about the routing system in Angular. We won’t hide from it; routing frameworks are complicated to do. Not only do you want to cover everybody’s scenario, but technology keeps changing at a rapid pace. When Angular 2 shipped, Angular Router was already misaligned with Angular Core as it released its version 3. After playing with it, it definitely felt like a beta version of how things were supposed to work. Documentation was scarce. To top it all off, they kept making breaking changes in patches. I had to lock package.json on a specific version so as to not be surprised on deployment.

Even after following all of their advice, I messed it up. Hopefully you can do something early enough in your project to prevent this mishap.

Before starting with Angular, let’s step back and see what the best HTML practice to create links should be.

The Basics

Choose your correct HTML element

This “click” event came a long way in JavaScript/HTML. Remember this onclick attribute, that was awesome! (not?). Then addEventListener('click') showed up, then jQuery chimed in, etc.

There are so many ways to make things clickable on the page, however, so many people get this wrong. Tutorials, Stackoverflow, even the Angular Documentation. My biggest recommendation is: Stop setting your click events on non-navigational elements.

You should restrict your events to <a> and <button>. THAT’S IT! How can I get my whole <div> to be clickable? Wrap an <a> tag around it. In HTML5, the <a> tag became a “transparent element” (http://w3c.github.io/html-reference/a.html#a-changes), you can wrap anything with it (almost anything… I’m still waiting on the <tr> element).

If you plan to handle a click event on something other than these two elements, you should handle all ARIA accessibility concerns and handle all keyboard combinations that the users expect. Good luck! Make sure to try it with screen readers too: you’ll be surprised.

DON’T:

 

DO

So, <a> elements or <button> elements?

With all Single Application Pages (SPA) popping up, you “technically” never navigate away. So then, you might think <button> is the way to go. Also, <button> gives you the ability out of the box to NOT show the status link in the bottom left. Advantage? Maybe. However, your button is not a link; if you hope search engines will catch that, you’re out of luck.

Use <a> tags when you navigate to another page.

Use <button> when you open a little popover or a dialog that is not reachable from the URL. I’m done talking about the buttons. Use it with parsimony when you do NOT navigate.

DON’T:

 

DO:

Where should I navigate to?

Unfortunately, the common practice I have seen is to set the href attribute to #, javascript:;, javascript:void(0) or a another variant, then handle the click event by code. I’m guilty: I have done the same thing too often. Your link should reflect the page URL it will navigate to. That way, your user will be able to CTRL+click on the link, or right click then “Open in a new tab”.

DON’T:

 

DO:

Let me bubble

If you inconveniently used the # technique, your code most likely uses preventDefault() on the link. If you use jQuery, they recommend to use return false. If you do so, you actually stop the click event from bubbling.

On a SPA page, all clicks should bubble properly since we are never refreshing the page. Another component on the page might be listening to the <body> to close a popover, or change its state.

So, the recommendation, don’t stop bubbling, and don’t use #.

DON’T:

 

DO:

Then Angular Router Way

The documentation they provide is getting quite beefy, written most likely by some Angular Experts, but they are not aware of these subtleties or are simply ignoring good practice to make the documentation slimmer.

The Angular Routing system is using the pushState technology which allows you to stay on the same page while you still navigate to different URL. It’s a little harder to setup, but a lot more useful when you start deep-linking your site to other web sites or e-mail messages.

Don’t use router.navigate()

Angular Router recommends putting your (click) attributes on <li> then using the router to navigate by code. Not only will this element not be tab-able, but you just lost the functionality to right click and open in a new tab. Use [routerLink] appropriately to create the correct link.

DON’T:

 

DO:

My links are all string based – I hate it

This is not new to Angular. It’s a problem I see in all the frameworks I have worked with. With the usage of a typed language, I find it sad that we still make important decisions about linking our pages with something that is not compiled and duplicated.

If you decide to change your link in the routing system, you’ll have to check the usage of that sad string everywhere.

Maybe your HTML is not compiled (that’s for another post), but at least start being DRY and use a compiled variable to point to your link.

DO:

One day, if you start compiling your HTML template, you will be thankful the link is actually defined in one spot.

Your code knows too much about how it should route

The coupling we created might scare you, but without it you are at the mercy of runtime errors. You will need to have a good series of integration testing to catch these issues.

One more problem with the code above is this logic in ['/, crisisCenterRoutingLinks.heroCenter, hero.id]. Your component is very much aware of how to create a link to reach the “heroCenter” in this case. That’s not very good. Let’s try it with a function.

In CrisisCenterRoutingLinks class, let’s add this method

Notice we made heroCenter private: we wouldn’t want people to reach it because it’s actually not a valid URL. You’ll need to update the routing page too to get the heroCenterRoute.

Our HTML would look like this:

If you need to pass more things to the function, you might want to accept another argument with a typed interface to pass in your currentRoute in order to handle the relativeTo functionality for example.

Complication with re-usable components

I might have a component that is re-used across the site, but still need to get some routing data handled. Let’s say, for example, a <tn-help> component that creates a help link with a cute little image. This time, we don’t want to be too permissive on this approach and accept any kind of string. We should restrict its usage and try to avoid using providers that are too global. (This help button does not need to know how to reach into the CrisisCenterRoutingLinks!)

In my previous project, I was using a EventEmitter to finally use this.router.navigate. You should avoid that based on the recommendations I have provided above.

I will let you manage your imports and file placements. You might want to separate the enum from the service.

Now all knowledge about how to create links is encapsulated in one place AND it is compiled.

Go even further?

I just scratched the surface on how you can make an abstraction layer on top of the Angular Router to handle links properly. You might even be able to do this with all your /images/ links?

It’s important to create abstractions as such for a big website so that if you need to change something everywhere you will not have to scavenge for the information. However, make sure you understand the functionality if you want to avoid any bad assumptions on how Angular works.

 

Jean-Sébastien Goupil (11 Posts)

Web Architect Consultant - Passionate about web projects! Expert in barcodes, automation, and JavaScript