Routing & Navigation - aakash14goplani/FullStack GitHub Wiki

Topics Covered

Basics
Configuration
Pass Navigation Programmatically
Pass Parameters to Router
Fetching Route Parameters
Relative path v/s Absolute Path
Pass Query Parameters and Fragments
Retrieve Query Parameters and Fragments
Child Routes
Path Matching
Pass static data using data property
Outsourcing Route Configuration
Guards
canActivate
canActivateChild
canDeactivate
resolve
canLoad
Location Strategies
MISC: Routing Cycle and others


Basics

The Angular Router enables navigation from one view to the next as users perform application tasks. You can navigate imperatively when the user clicks a button, selects from a drop box, or in response to some other stimulus from any source. And the router logs activity in the browser's history journal so the back and forward buttons work as well. More insights

Configuration

  1. <base href>:

    • Most routing applications should add a <base> element to the index.html as the first child in the <head> tag to tell the router how to compose navigation URLs.
    • If the app folder is the application root, as it is for the sample application, set the href value exactly as <base href="/">
  2. Imports

    • The Angular Router is an optional service that presents a particular component view for a given URL. It is not part of the Angular core. It is in its own library package, @angular/router. Import what you need from it as you would from any other Angular package: import { RouterModule, Routes } from '@angular/router';
  3. RouterModule.forRoot()

    • A routed Angular application has one singleton instance of the Router service. When the browser's URL changes, that router looks for a corresponding Route from which it can determine the component to display.
    • A router has no routes until you configure it. The following example creates different route definitions (which are basically JS objects holding meta-data understood by angular), configures the router via the RouterModule.forRoot() method, and adds the result to the AppModule's imports array.
    const appRoutes: Routes = [
       { path: 'crisis-center', component: CrisisListComponent },
       { path: 'hero/:id',      component: HeroDetailComponent },
       { path: 'heroes',        component: HeroListComponent, data: { title: 'Heroes List' } },
       { path: '',              redirectTo: '/heroes', pathMatch: 'full'},
       { path: 'page-not-found', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} },
       { path: '**', redirectTo: '/page-not-found' }
    ];
    
    @NgModule({
      imports: [ RouterModule.forRoot(appRoutes)],
      ...
    })
    
    export class AppModule { }
    • Here path is the URL after your domain and component represents the action to be performed on reaching that path
    • The appRoutes array of routes describes how to navigate. Pass it to the RouterModule.forRoot() method in the module imports to configure the router.
    • Each Route maps a URL path to a component. There are no leading slashes in the path. The router parses and builds the final URL for you, allowing you to use both relative and absolute paths when navigating between application views.
    • The :id in the second route is a token for a route parameter. In a URL such as /hero/42, "42" is the value of the id parameter. The corresponding HeroDetailComponent will use that value to find and present the hero whose id is 42.
    • The data property in the third route is a place to store arbitrary data associated with this specific route. The data property is accessible within each activated route. Use it to store items such as page titles, breadcrumb text, and other read-only, static data.
    • The empty path in the fourth route represents the default path for the application, the place to go when the path in the URL is empty, as it typically is at the start. This default route redirects to the route for the /heroes URL and, therefore, will display the HeroesListComponent.
    • The ** path in the last route is a wildcard. The router will select this route if the requested URL doesn't match any paths for routes defined earlier in the configuration. This is useful for displaying a 404 - Not Found page or redirecting to another route.
    • The order of the routes in the configuration matters and this is by design. The router uses a first-match wins strategy when matching routes, so more specific routes should be placed above less specific routes. In the configuration above, routes with a static path are listed first, followed by an empty path route, that matches the default route. The wildcard route comes last because it matches every URL and should be selected only if no other routes are matched first.
  4. Router Outlet:

    • Now you have routes configured and a place to render them, but how do you navigate?
    • You have bound to a template expression that returned an array of route link parameters (the link parameters array). The router resolves that array into a complete URL.
    <div class="container">
     <div class="row">
         <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
             <ul class="nav nav-tabs">
                 <li role="presentation" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
                    <a [routerLink]="['/']">Home</a><!--property-binding with array-->
                 </li> 
                 <li role="presentation" routerLinkActive="active">
                    <a [routerLink]="'/servers'">Servers</a><!--property-binding with string-->
                 </li>
                 <li role="presentation" routerLinkActive="active">
                    <a routerLink="/users">Users</a><!--directive-->
                 </li>
             </ul>
         </div>
     </div>
     <div class="row">
         <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
             <router-outlet></router-outlet>
         </div>
     </div>
    </div>
  5. Router Link

    • Regular anchor tags have attribute "href" which holds the target action once the link is clicked. Default behaviors is that page is always re-load once you click that anchor tag. This is not the behavior we desire with Angular. In Angular, applications are designed to be Single Page and they should not reload entire HTML once again.
    • Technically it still reloads the page but keep in mind, it never sends a request so this reload icon will never spin but Angular simply determined that we are on this page, so no further action is needed.
    • To prevent this from happening we can use RouterLink directives on the anchor tags give the router control over those elements.
    • We can use RouterLink in three different ways as shown above in example
  6. Active Router Link

    • The RouterLinkActive directive toggles css classes for active RouterLink bindings based on the current RouterState.
    • On each anchor tag, you see a property binding to the RouterLinkActive directive that look like routerLinkActive="...".
    • The template expression to the right of the equals (=) contains a space-delimited string of CSS classes that the Router will add when this link is active (and remove when the link is inactive). You set the RouterLinkActive directive to a string of classes such as [routerLinkActive]="'active fluffy'" or bind it to a component property that returns such a string.
    • Active route links cascade down through each level of the route tree, so parent and child router links can be active at the same time. To override this behavior, you can bind to the [routerLinkActiveOptions] input binding with the { exact: true } expression. By using { exact: true }, a given RouterLink will only be active if its URL is an exact match to the current URL.
  7. ActivatedRouting

    • The route path and parameters are available through an injected router service called the ActivatedRoute. It provides access to information about a route associated with a component that is loaded in an outlet.
    • This comes in very handy when we want to pass navigation programmatically. Example Within a component clicking on a button should redirect to different component.
    • More Reading

^ Back to Top ^


Pass navigation programmatically

  • We can load a route programmatically i.e. we don't have a link the user can click but we've finished some operation or the user clicked some button and then we want to trigger the navigation from our TypeScript code.

  • I add a new button to it and on this button, I simply want to load the server component. So here, now we could try adding routerLink but let's say I want to have a click listener and execute some method because we do something else than just navigating there.

    <button class="btn btn-primary" (click)="onLoadServer()">Servers</button>
  • I simply want to navigate to the servers component. So we could use a routerLink but let's say here, we have some complex calculation or we reach out to our back-end, we store something on the server and once we are done, now we want to navigate away.

  • To do so, we somehow need to get access to our router, this Angular router because we need to tell it hey please navigate somewhere else. We can inject this router.

    constructor(private router: Router) { }
  • With this injected, we can use this router here, we get a couple of methods there, one of the most important ones being navigate.

  • navigate takes an argument which allows us to navigate to a new route and here, a route is defined as an array of the single or the different elements of this new path. So if let's say we want to go to /servers here, we could add /servers here.

    onLoadServer() {
       this.router.navigate(['/servers']);
    }
  • This is now programmatically routing to a different page, still it doesn't reload our page, it does the same as if we clicked a routerLink, but with this router navigate method, we are able to trigger this programmatically, so trigger this in our code.

  • Above functionality made use of absolute path. You could have a relative one but here, you have to control to what this should be relative to!

  • Now let's say we remove the slash at the beginning, so we turn this into a relative path, servers and we still are on the servers component,

  • To use relative path, we have to pass a second argument to the navigate method which is a JavaScript object and here we can configure this navigation action using relativeTo property.

  • Here we define relative to which route this link should be loaded and by default, this is always the root domain, here we have to give a route though, so we don't pass a string here, instead the route is something we can inject here too.

  • We can get the currently active route by injecting route which is of type ActivatedRoute

    constructor(private router: Router, private route: ActivatedRoute) { }
  • Now ActivatedRoute like the name implies simply injects the currently active routes, so for the component you loaded, this will be the route which loaded this component and the route simply is kind of a complex Javascript object which keeps a lot of meta information about the currently active route.

  • Now we can set this as a value, so this route, this injected route for the relativeTo property and now with that extra piece of information, Angular knows what our currently active route is.

    onLoadServer() {
       this.router.navigate(['servers'], {relativeTo: this.route});
    }
  • So here, we are telling it now our currently active routes is this ActivatedRoute, so relative to this route you should navigate and then it will simply resolve all the relative paths you might have here relative to this route.

^ Back to Top ^


Pass parameters to Router

  • Let's say that we want to be able to load a single user from a list of users. For that, we could pass the ID of the user we want to load in that route path.

  • One approach would be to set up a route with user/1 i.e user/<id> where user is component that has user details and <id> is the parameter that needs to be passed via URL

  • We can add parameters to our routes, dynamic segments in our paths. We do this by adding a colon and then any name you like, like for example ID, you will later be able to retrieve the parameter inside of the loaded component by that name you specify here

    const appRoute: Routes = [
       {path: 'user/:id', component: UserComponent }
    ];
    ...
    imports: [
       RouterModule.forRoot(appRoute)
    ],
  • So by ID in this case and the colon simply tells Angular that this is a dynamic part of the path.

  • Without colon, only routes which are users/id and with ID, I literally mean the word ID, would lead to this component. With a colon user i.e. users/:id, slash anything else would load this component and anything else would be interpreted as the ID, e.g. user/1, user/A etc.

^ Back to Top ^


Fetching Route Parameters

  • In the last section, we created our route with a dynamic path segment, now we update our appRoute path to add one more parameter i.e. name

    const appRoute: Routes = [
       {path: 'user/:id/:name', component: UserComponent }
    ];
  • Now we want to get access to the data the user sent us or which is encoded in the URL. The first step is to inject ActivatedRoute to get access to the currently loaded route.

    constructor(private route: ActivatedRoute) { }
  • This currently loaded route is a JavaScript object with a lot of metadata about this currently loaded route, one of this important pieces of information is the currently active user.

  • In this user component, I have defined a user object which is undefined for now, it should have the following structure and it's not used right now but we could load our user by simply getting access or retrieving this parameter from our URL.

    user: {id: number, name: string};
  • In ngOnInit() when our component gets initialized, we want to get our user.

  • Now we assign user object to a JavaScript object because that is the type of it, a JavaScript object with an ID and with a name. Now the value for the ID can be fetched from our route and there, we have a snapshot property and on this snapshot of our currently active route, we have a params JavaScript object and here we can get our ID and now you will only have the access to properties here which you defined in your route parameters.

    ngOnInit(): void {
       this.user = {
          id: this.route.snapshot.params.id,
          name: this.route.snapshot.params.name
       };
    }
    <p>User with ID {{user.id}} loaded.</p>
    <p>User name is {{user.name}}</p>
  • Now this should work, so if we save this and we target user/1/Aakash, we have both the ID and the name and we hit enter, we correctly see ID 1, name Aakash, if we change the ID to 3, we see ID 3 here.

  • Using the snapshot is, as the name suggests, a one-time event. A typical use case is to get the parameter when the component loads. Read the code explicitly; when I load the component I will get the URL parameter. This strategy will not work if the parameter changes within the same component {read advance section below}

Advance Section

  • Remember, the router populates the snapshot, when the component loads for the first time. Hence you will read only the initial value of the query parameter with the snapshot property. You will not be able to retrieve any subsequent changes to the query parameter.

  • We saw how we can retrieve our route parameters and this is working fine but there are ways to break this, there are cases where this approach will not work!

  • On our users component, we saw that we have the user ID and name we passed on our URL. Now in here, let me add a routerLink, I want to load /users/8/Aakash

    <a [routerLink]="'/user/8/Aakash'">Aakash (8)</a>
  • We get a link on our currently loaded page and if I click this, you'll see that the URL was updated. Now it's /users/8/Aakash but the text here (i.e. View) wasn't updated and this is not a bug, this is the default behavior!

  • We load our data by using this snapshot object on the route. Now if we load a new route, what happens? Angular has a look at our app module, finds the fitting route, loads the component, initializes the component and gives us the data by accessing the snapshot.

  • Now that only happens if we haven't been on this component before but if we click this link, which is on the user component, well then the URL still changes but we already are on the component which should get loaded.

  • Angular cleverly doesn't really instantiate this component, that would only cost us performance, why would it re-render a component we already are on? Now you might say because the data we want to load changed but Angular doesn't know and it's good that by default, it won't recreate the whole component and destroy the old one if we already are on that component.

  • Still of course you want to get access to the updated data and you can. It's fine to use this snapshot for the first initialization but to be able to react to subsequent changes, we need a different approach.

  • In ngOnInit(), after we assign this initial set up, we can use our route object and instead of using the snapshot, there is some params property on this route object itself.

  • Now we didn't use that before, we had the snapshot in between, what's the difference? - Params here is an observable. Basically, observables are a feature added by third-party package, not by Angular but heavily used by Angular which allow you to easily work with asynchronous tasks and this is an asynchronous task because the parameters of your currently loaded route might change at some point in the future if the user clicks this link but you don't know when. So therefore, you can't block your code and wait for this to happen here because it might never happen.

  • So an observable is an easy way to subscribe to some event which might happen in the future, to then execute some code when it happens without having to wait for it now and that is what params is. It is such an observable and as the name implies, we can observe it and we do so by subscribing to it.

    import { Subscription } from 'rxjs/internal/Subscription';
    ...
    paramSubscribtion: Subscription;
    ...
    ngOnInit(): void {
      this.user = {
        id: this.route.snapshot.params.id,
        name: this.route.snapshot.params.name
      };
      this.paramSubscribtion = this.route.params.subscribe(
        (param: Params) => {
          this.user.id = param.id;
          this.user.name = param.name;
        }
      );
    }
  • The function will be fired whenever new data is sent through that observable, i.e. whenever the parameters change in this use case and it will update our user object whenever the parameter change. Here params will always be an object just like here on the snapshot which holds the parameters you defined in the route as properties.

  • Now if I click this link, it correctly updates the View because our observable fires and we then retrieve the updated parameters and assign them to our user object and this therefore actually is the approach you should take to be really safe against changes not being reflected in your template.

  • Now if you know that the component you're on may never be reloaded from within that component as we're doing it here, then you might not need this addition, you might simply use the snapshot. In all other cases, make sure to use this approach to get informed about any changes in your route parameters.

Behind the Scenes

  • The fact that you don't have to add anything else to this component here simply is because Angular does something for you in the background, it cleans up the subscription you set up here whenever this component is destroyed because if it wouldn't do this, it would lead to memory overflow problems.

  • You're subscribing to parameter changes and let's say you then leave this component and later you come back. Well once you left, this component will be destroyed and when you come back, a new one will be created but this subscription here will always live on in memory because it's not closely tied to your component, so if the component is destroyed, the subscription won't.

  • Now it will be here because Angular handles this destroying of the subscription for you but theoretically, you might want to implement the onDestroy() lifecycle hook

    ngOnDestroy(): void {
      this.paramSubscribtion.unsubscribe();
    }
  • Again because it's important, you don't have to do this, you can leave it as it was before because Angular will do this for you regarding these route observables but if you add your own observables, you have to unsubscribe on your own!

^ Back to Top ^


Relative path v/s Absolute Path

  1. Using routerLink

    • If I have a routerLink configured on a relative path, on clicking this URL, I will get error

      Error: Uncaught (in promise): Error: Cannot match any routes.

    • E.g. If I am on localhost:4200/servers and I have link configured as <a routerLink="servers">Reload</a> on clicking this link the resulting path would be localhost:4200/servers/servers which is not defined within router (in AppModule) so we get error.
    • To rectify this we need to use absolute path with routerLink i.e. <a routerLink="/servers">Reload</a>
  2. Using Router and ActivatedRoute

    • If I have an relative path setup and I am using Router service and ActivatedRoute, I will get error

      Error: Uncaught (in promise): Error: Cannot match any routes.

    • E.g.
      constructor(private router: Router, private route: ActivatedRoute) { }
      this.router.navigate(['servers']);
    • To resolve this we can:
      1. Use relativeTo property to specify 'to what this path is relative to'. The router then calculates the target URL based on the active route's location., e.g. this.router.navigate(['servers'], {relativeTo: this.route});
      2. Use absolute path: this.router.navigate(['/servers']);

^ Back to Top ^


Pass Query Parameters and Fragments

  • Query parameters are optional parameters that you pass to a route. The query parameters are added to the end of the URL Separated by Question Mark (?).

  • For Example, /product?page=2, where page=2 is the query parameter. The given URL is an example of paginated product list, where URL indicates that second page of the Product list is to be loaded.

  • Fragments are optional parameters that you pass to a route. The fragments are added to the end of the URL Separated by Hash (#).

  • For Example, /product#introduction, where introduction is the fragment. The given URL is an example of paginated product list, where URL indicates that introduction section of the Product list is to be loaded.

Difference between Query parameter and Route parameter

  • The route parameters are required and is used by Angular Router to determine the route. They are part of the route definition. For Example, when we define the route as shown below, the id is the route parameter.
    { path: 'product', component: ProductComponent }
    { path: 'product/:id', component: ProductDetailComponent }
  • The above route matches the following URL The angular maps the values 1 & 2 to the id field
URL Pattern
/product matches => path: 'product'
/product/1 matches => path: 'product/:id'
/product/2 matches => path: 'product/:id'
  • The Router will not navigate to the ProductDetailComponent route, if the id is not provided. It will navigate to ProductComponent instead. If the product route is not defined, then it will result in a error.

  • However, the query parameters are optional. The missing parameter does not stop angular from navigating to the route. The query parameters are added to the end of the URL Separated by Question Mark

  • Route Parameters or Query Parameters?

    • Use route parameter when the value is required
    • Use query parameter, when the value is optional.

Passing Query Parameters

  • The Query parameters are not part of the route. Hence you do not define them in the routes array like route parameters. You can add them using the routerlink directive or via router.navigate method.

  • <a [routerLink]="['product']" [queryParams]="{ page:2 }">Page 2</a> -> The router will construct the URL as /product?pageNum=2

  • You can pass more than one Query Parameter as: <a [routerLink]="['product']" [queryParams]="{ val1:2 , val2:10}">Whatever</a> -> The router will construct the URL as /product?val1=2&val2=10

  • You can also navigate programmatically using the navigate method of the Router service as shown below

    constructor(private route: ActivatedRoute) { }
    goToPage(pageNum) {     
     this.router.navigate(['/product'], { queryParams: { page: pageNum } }); 
    }

Passing Fragments

  • These are passed same as that of query parameters, e.g.

    <a [routerLink]="['/product']" [fragment]="'introduction'">{{ server.name }}</a>
    this.router.navigate(['/product'], { fragment: 'introduction' }); 
  • You can also pass query parameters and fragments simultaneously

    <a [routerLink]="['/servers', server.id]" [queryParams]="{allowEdit: '1'}" [fragment]="'loading'" *ngFor="let server of servers">{{ server.name }}</a>
    this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading'});
  • More Reading

^ Back to Top ^


Retrieve Query Parameters and Fragments

  • Reading the Query parameters is similar to reading the Router Parameter. There are two ways by which you can retrieve the query parameters.

    1. Using queryParams observable

      • The queryParams is a Observable that contains query parameters available to the current route. We can use this to retrieve values from the query parameter. The queryParams is accessible via ActivatedRoute that we need to inject in the constructor of the component or service, where we want to read the query parameter
      • You can subscribe to the queryParams of the ActivatedRoute, which returns the observable of type Params. We can then use the get method to read the query parameter as shown below.
      this.sub = this.route.queryParams
        .subscribe(params => {
           this.pageNum = +params.get('pageNum')||0; 
           /* this.pageNum = (params.pageNum) ? +params.pageNum : 0 */
      });
    2. Using Snapshot

      • You can also read the value of the query parameter from queryParams using the snapshot property of the ActivatedRoute as: this.route.snapshot.queryParams;

      • Remember, the router populates the snapshot, when the component loads for the first time. Hence you will read only the initial value of the query parameter with the snapshot property. You will not be able to retrieve any subsequent changes to the query parameter.

  • More Reading

  • Reading Fragments is same as that of Query Parameters

    1. Subscription
      this.sub = this.route.fragment
        .subscribe(params => {
           this.pageNum = +params.get('pageNum')||0;
           /* this.pageNum = (params.pageNum) ? +params.pageNum : 0 */
      });
    2. Snapshot: this.route.snapshot.fragment;

queryParamsHandling

  • The query parameter is lost when the user navigates to another route.

  • For Example, if user navigates to the server page with route /servers/2?allowEdit=2 then he navigates to the server page, the angular removes the query parameter from the url. This is the default behavior

  • You can change this behavior by configuring the queryParamsHandling strategy. This Configuration strategy determines how the angular router handles query parameters, when user navigates away from the current route. It has three options

    1. queryParamsHandling : null

      • This is default option. The angular removes the query parameter from the URL, when navigating to the next..
      this.router.navigate(['edit'], { queryParams: { allowEdit: '2' }, queryParamsHandling :null}   );
      <a [routerLink]="['edit']" [queryParams]="{ allowEdit:2 }" queryParamsHandling=null>Server 2</a>
    2. queryParamsHandling : preserve

      • The Angular preserves or carry forwards the query parameter of the current route to next navigation. Any query parameters of the next route are discarded
      this.router.navigate(['edit'], { queryParams: { allowEdit: '2' }, queryParamsHandling :"preserve"}   );
      <a [routerLink]="['edit']" [queryParams]="{ allowEdit:2 }" queryParamsHandling="preserve">Server 2</a>
    3. queryParamsHandling : merge

      • The Angular merges the query parameters from the current route with that of next route before navigating to the next route.
      this.router.navigate(['edit'], { queryParams: { allowEdit: '2' }, queryParamsHandling :"merge"}   );
      <a [routerLink]="['edit']" [queryParams]="{ allowEdit:2 }" queryParamsHandling="merge">Server 2</a>

^ Back to Top ^


Child Routes

  • Child Routes or Nested routes are routes within other routes.

How to Create Child Routes / Nested Routes

  • Lets consider our routes to be of following structure:

    { path: 'servers', component: RoutingServersComponent },   
    { path: 'servers/:id', component: RoutingServerComponent },
    { path: 'servers/:id/edit', component: EditRoutingServerComponent }
  • To make RoutingServerComponent and EditRoutingServerComponent as the child of the RoutingServersComponent, we need to add the children key to the servers route, which is an array of all child routes as shown below

    { path: 'servers', component: RoutingServersComponent, children: [
       { path: ':id', component: RoutingServerComponent },
       { path: ':id/edit', component: EditRoutingServerComponent }
     ] },
  • The child route definition is similar to the parent route definition. It has a path and component that gets invoked when the user navigates to the child route.

  • In the above example, the parent route path is servers and one of the child route is :id. This is will match the URL path /servers/id.

  • When the user navigates to the /servers/id, the router will start to look for a match in the routes array

  • It starts off the first URL segment that is servers and finds the match in the path servers and instantiates the RoutingServersComponent and displays it in the <router-outlet> directive of its parent component ( which is AppComponent)

  • The router then takes the remainder of the URL segment /id and continues to search for the child routes of Servers route. It will match it with the path :id and instantiates the RoutingServerComponent and renders it in the <router-outlet> directive present in the RoutingServersComponent

Display the component using <router-outlet>

  • The components are always rendered in the <router-outlet> of the parent component.

  • For RoutingServerComponent the parent component is RoutingServersComponent and not the AppComponent. Hence, we need to add <router-outlet></router-outlet> in the servers.component.html

  • More Reading

^ Back to Top ^


Path Matching

  • By default, Angular matches paths by prefix. That means, that the following route will match both /recipes and just /: { path: '', redirectTo: '/somewhere-else' }

  • Actually, Angular will give you an error here, because that's a common gotcha: This route will now ALWAYS redirect you! Why? - Since the default matching strategy is prefix, Angular checks if the path you entered in the URL does start with the path specified in the route. Of course every path starts with '' (Important: That's no whitespace, it's simply "nothing").

  • To fix this behavior, you need to change the matching strategy to full : { path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }

  • Now, you only get redirected, if the full path is '' (so only if you got NO other content in your path in this example).

^ Back to Top ^


Pass static data using data property

  • In our page-not-found component here, now I don't want to output page not found or anything like this, instead let's say we have some error message which I want to output via string interpolation i.e. I want to access static data.

  • So let's add it here, let's add error message to this component.

    statusMessage: string = '';
  • Now for routing, there only is one proper use case you want to target right now and that is that a route is not found.

  • So in our app-routing.module, we know that if we have the not found route we will always display the same error message and we can pass such static data with the data property here. The data property allows us to pass an object and in this object, we can define any key-value pairs.

    { path: 'page-not-found', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} },
    { path: '**', redirectTo: '/page-not-found' }
  • So with this, we now want to retrieve that whenever we load our error page component and for this, like params, like queryParams, we follow the same approach:

    ngOnInit() {
      this.statusMessage = this.route.snapshot.data.message;
      this.route.data.subscribe(
        (data: Data) => {
          this.statusMessage = data.message;
        }
      );
    }
  • So with this, when we encounter some invalid route, we correctly see page not found, the static error message we passed with the data property and this is a typical use case whenever you have some static data you want to pass to a route.

^ Back to Top ^


Outsourcing Route Configuration

  • We must have our routes configured in a separate file. Follow these steps for configuration:

  • First step is to create array of routes:

    const appRoute: Routes = [
       { path: '', component: HomeComponent },
       { path: 'users', component: UsersComponent, children: [
         { path: ':id/:name', component: UserComponent }
       ] },
      { path: 'page-not-found', component: PageNotFoundComponent, data: {message: 'Please double check the URL entered'} },
      { path: '**', redirectTo: '/page-not-found' }
    ];
  • Second step is to configure NgModule() and export your module

    @NgModule({
       imports: [
         // RouterModule.forRoot(appRoute, { useHash: true })
         RouterModule.forRoot(appRoute)
       ],
       exports: [
         RouterModule
       ]
    })
  • Final step is to import your module (that was exported in last step) in app.module.ts file

    ...
    imports: [
      ...,
      AppRoutingModule,
      ...
    ],
    ...

^ Back to Top ^


Guards

  • We use the Angular Guards to control, whether the user can navigate to or away from the current route.

  • Uses of Angular Route Guards

    • To Confirm the navigational operation
    • Asking whether to save before moving away from a view
    • Allow access to certain parts of the application to specific users
    • Validating the route parameters before navigating to the route
    • Fetching some data before you display the component.
  • Types of Route Guards

    1. CanActivate
    2. CanActivateChild
    3. CanDeactivate
    4. Resolve
    5. CanLoad

CanActivate

  • This guard decides if a route can be activated (or component gets used). This guard is useful in the circumstance where the user is not authorized to navigate to the target component. Or the user might not be logged into the system

  • Creating fake AuthService to demonstrate working of CanActivate

  • auth-guard.service.ts - it guards certain actions like navigating to, around or away from it.

  • auth.service.ts - enables to be able to login or out.

  • Now here, I will implement the canActivate interface which is provided by the @angular/router package, and it forces you to have a canActivate() method in this class.

  • The canActivate() method now will receive two arguments, the ActivatedRouteSnapshot and the state of the router, so the RouterStateSnapshot.

  • Where are we getting these arguments from? - Angular should execute this code before a route is loaded, so it will give us this data and we simply need to be able to handle the data.

  • canActivate() returns either returns an observable, a promise, and a boolean. So canActivate() can run both asynchronously, returning an observable or a promise or synchronously because you might have some guards which execute some code which runs completely on the client, therefore it runs synchronously or you might have some code which takes a couple of seconds to finish because you use a timeout in there or you reach out to a server, so it runs asynchronously and both is possible with canActivate()

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
  • In auth.service we will track the state of user i.e. loggedIn or loggedOut and I will have a method which allows us to check the authenticated state

    loggedIn: boolean = false;
     login() {
         this.loggedIn = true;
     }
     logout() {
         this.loggedIn = false;
     }
     isAuthenticated() {
         const promise = new Promise(
             (resolve, reject) => {
                 setTimeout(() => { resolve(this.loggedIn); }, 300);
             }
         );
         return promise;
     }
  • So with this auth.service added, I now want to use it in my auth-guard so I will add a constructor to my auth-guard.

    constructor(private authService: AuthService, private router: Router) {}
  • I simply want to check whether the user is logged in or not. So here, I can reach out to my auth.service, to the isAuthenticated() method which returns a promise. I then want to check if this is true, in which case I want to return true and otherwise, I want to navigate away because I don't want to allow the user access to the route you wanted to go to originally, I will navigate away to force the user to go somewhere else.

    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
         return this.authService.isAuthenticated().then(
             (authentication: boolean) => {
                 if (authentication) {
                     return true;
                 } else {
                     this.router.navigate(['/']);
                     // return false;
                 }
             }
         );
     }
  • This now allows us to control access to whatever is controlled by this canActivate guard here.

  • We're still not using this guard, to use it, I'll go to my app-routing.module and now we want to define which route or routes should be protected by this guard and we do so by going to that route, it's the servers route and all its child routes and adding canActivate, this property to it.

  • canActivate takes an array of all the code basically, all the guards you want to apply to this route and it will automatically get applied to all the child routes. So here, canActivate will use my AuthGuard (auth-guard.service.ts) and this will make sure that servers and all the child routes are only accessible if the auth-guard canActivate() method returns true in the end which will only happen if in the auth.service, loggedIn is set to true.

    { path: 'servers', canActivate: [AuthGuard], component: RoutingServersComponent, children: [
       { path: ':id', component: RoutingServerComponent },
       { path: ':id/edit', component: EditRoutingServerComponent }
     ] },
  • So our guard is working, however on our whole servers tab. Now I want to be able to see the list of servers and only protected child routes! - we can do this using CanActivateChild guard

  • Example online

^ Back to Top ^

CanActivateChild

  • This guard determines whether a child route can be activated. This guard is very similar to CanActivateGuard. We apply this guard to the parent route. The Angular invokes this guard whenever the user tries to navigate to any of its child route. This allows us to check some condition and decide whether to proceed with the navigation or cancel it.

  • In the last section, we added the canActivate guard and it was working fine but it was working for our whole servers path here, now we could grab it from here and add it to our child to make sure that only the child are protected.

  • The children and not our root path but that is not the easiest way because if we add more child items, we have to add canActivate to each of them. Here we can use CanActivateChild guard.

  • Let's implement this interface too which is also provided by @angular/router and this interface requires you to provide a CanActivateChild() method in this class which basically takes the same form as the canActivate method, so it has the route and state and it returns an observable, promise or boolean.

    canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
         return this.canActivate(route, state);
     }
  • Since this is exactly the same form and we want to run the same logic, we can simply return canActivate()

  • CanActivateChild also takes an array of services which act as guards which implement the right interfaces and here we can still add the auth-guard because the auth-guard now is able to do both, protect a single route since we have canActivate() implemented or all child routes since we have CanActivateChild() implemented too.

    { path: 'servers', canActivateChild: [AuthGuard], component: RoutingServersComponent, children: [
       { path: ':id', component: RoutingServerComponent },
       { path: ':id/edit', component: EditRoutingServerComponent }
     ] },
  • So now this is the fine-grained control you can implement to protect a whole route and all its child routes or just the child routes, depending on which behavior you need in your app.

  • Example Online

^ Back to Top ^

CanDeactivate

  • This Guard decides if the user can leave the component (navigate away from the current route). This route is useful in where the user might have some pending changes, which was not saved. The CanDeactivate route allows us to ask user confirmation before leaving the component. You might ask the user if it’s OK to discard pending changes rather than save them.

  • In the last sections, we discussed how to use canActivate to control access to a route, now I want to focus on the control of whether you are allowed to leave a route or not.

  • We might want to control this if we are logged in once we do edit a server and actually changed something, I want to ask the user if he accidentally clicks back or somewhere else, if you really want to leave or if you maybe forgot to click update server first.

  • In edit-server.component I'll add a changesSaved property which is false by default and which I want to change whenever we click on update server. After the changes were saved, I want to navigate away to go up one level to the last loaded server.

    changesSaved: boolean = false;
    ...
    onUpdateServer() {
     ...
     this.changesSaved = true;
     this.router.navigate(['../'], {relativeTo: this.route});
    }
  • We're changing this changesSaved property here. Now let's make sure that whenever the user tries to accidentally navigate away, that we prevent them from doing so or at least ask if he really wants to leave.

  • Now we somehow need to execute this code in this component here because we will need access to this changesSaved property which informs us on whether this update button was clicked or not.

  • I'll create a guard/service can-deactivate-guard. I first of all now want to export an interface, let name it CanComponentDeactivate and this interface will require one thing from the component which implements it, this component should have a canDeactivate() method. So this method should take no arguments but in the end, it should return an observable or a promise or just a boolean.

    export interface CanComponentDeactivate {
      canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
    }
  • You might recognize this pattern here from the can-activate-guard, these guards share the same structure.

  • Our service/Guard CanDeactivateGuard will implement CanDeactivate: an interface provided by the Angular router. This actually is a generic type and it will wrap our own interface, so it will wrap an interface which forces some component or some class to implement the canDeactivate() method.

  • Now this class here, this guard will also need to have a canDeactivate() method. This is the canDeactivate() method which will be called by the Angular router once we try to leave a route. Therefore this will have the component on which we're currently on as an argument and this component needs to be of type CanComponentDeactivate. We also will receive the current route, the current state and the next state as an argument, this will now also return an observable, a promise or a boolean.

    export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
       canDeactivate(component: CanComponentDeactivate, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
         return component.canDeactivate();
       }
    }
  • Here I will call canDeactivate() on the component we're currently on and this is why I need to implement this interface in this component, why I created this interface in the first place because now, the Angular router can execute canDeactivate() in our service and can rely on the fact that the component we're currently on has the canDeactivate() method too because this is what we will actually implement the logic checking whether we are allowed to leave or not because we need this connection between our guard and the component.

  • We can add canDeactivate as a property to this route config, it takes an array just like canActivate and here, we now will point to our CanDeactivateGuard. Angular will run this guard whenever we try to leave this path here.

    { path: 'servers', canActivateChild: [AuthGuard], component: RoutingServersComponent, children: [
       { path: ':id', component: RoutingServerComponent },
       { path: ':id/edit', component: EditRoutingServerComponent, canDeactivate: [CanDeactivateGuard] }
     ] },
  • Remember that the CanDeactivateGuard will in the end call canDeactivate, our component. Well, for this to work on our edit-server.component, here we need to implement our CanComponentDeactivate interface. This interface now forces us to implement the canDeactivate() method in our component.

    export class EditRoutingServerComponent implements OnInit, CanComponentDeactivate {
    ...
    canDeactivate(): boolean | Observable<boolean> | Promise<boolean> {
     if (!this.changesSaved) {
       return confirm('Do you want to discard changes?');
     } else {
       return true;
      }
    }
  • This logic will be run whenever the CanDeactivateGuard is checked by the Angular router.

  • Example Online

^ Back to Top ^

Resolve

  • This guard delays the activation of the route until some tasks are complete. You can use the guard to pre-fetch the data from the backend API, before activating the route

Passing dynamic data with Resolver

  • To pass static data we use data property and to pass dynamic data we use resolver guard.

  • For example here on the servers, let's say the servers already have been loaded but once we click a server, I want to load the individual server from some back-end, So how could this work? If we have such a use case, we need a resolver.

  • This also is a service, just like canActivate or canDeactivate which will allow us to run some code before a route is rendered.

  • Now the difference to canActivate is that the resolver will not decide whether this route should be rendered or not, whether the component should be loaded or not, the resolver will always render the component in the end but it will do some pre-loading, it will fetch some data the component will then need later on.

  • Of course the alternative is to render the component or the target page instantly and in the onInit() method of this page, you could then fetch the data and display some spinner whilst you are doing so. So that is an alternative but if you want to load it before actually displaying the route, this is how you would add such a resolver.

  • Lets create a service, server-resolver, this has to implement the resolve interface provided by @angular/router. Resolve is a generic type and it should wrap whichever item or data field you will get here, will fetch here in the end, in our case this will be Server.

  • Now the resolve interface requires us to implement the resolve() method and this resolve() method takes two arguments, the ActivatedRouteSnapshot and RouterStateSnapshot. These are the two information pieces the resolve method gets by Angular and in the end, this then also has to return either an observable or a promise or just a generic type i.e. just such a server.

    interface Server {
      id: number;
      name: string;
      status: string;
    }
    @Injectable()
    export class ServerResolver implements Resolve<Server> {
      constructor(private serverService: ServersRoutingService) {}
      resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Server> | Promise<Server> | Server {
         return this.serverService.getServer(+route.params.id);
      }
    }
  • In the server.component, we have to update onInit() to fetch Server data

    ngOnInit() {
      /* normal/standard approach
      const id = +this.route.snapshot.params.id;
      this.server = this.serversRoutingService.getServer(id);
      this.route.params.subscribe(
        (param: Params) => {
          this.server = this.serversRoutingService.getServer(+param.id);
        }
      ); */
      // approach using resolver guard
      this.route.data.subscribe(
        (data: Data) => {
          this.server = data.server;
        }
      );
    }
  • Now we want to add it to our routing module.

    { path: 'servers', canActivateChild: [AuthGuard], component: RoutingServersComponent, children: [
       { path: ':id', component: RoutingServerComponent, resolve: {server: ServerResolver} },
       { path: ':id/edit', component: EditRoutingServerComponent, canDeactivate: [CanDeactivateGuard] }
     ] },
  • This is different to the other guards, there we use arrays but for resolve, we have key-value pairs of the resolvers we want to use ie. our resolver service. This will now map the data. This resolver gives us back some data with resolve() method that was implemented, this method will be called by Angular when this router is loaded.

  • Example Online

^ Back to Top ^

CanLoad

  • The CanLoad Guard prevents the loading of the Lazy Loaded Module. We generally use this guard when we do not want to unauthorized user to be able to even see the source code of the module.

  • This guard works similar to CanActivate guard with one difference. The CanActivate guard prevents a particular route being accessed. The CanLoad prevents entire lazy loaded module from being downloaded, Hence protecting all the routes within that module.

  • More Reading

^ Back to Top ^


Location Strategies

  • Now if you have a look at our application, we get a couple of routes in there, /users, /servers and much more. It works fine here on our local setup but actually, but there are chances it might not work when you host your application on a real server because routes is always parsed by the server first.

  • Now here on the local environment in our development environment, we're also using a development server but this server has one special configuration your real life server also has to have.

  • The server hosting your Angular single page application has to be configured such that in a case of a 404 error, it returns the index.html file - the file starting and containing your Angular app. Why? - Because all your URLs are parsed by the server first, not by Angular, by the server.

  • Now if you have /servers here, it will look for a /servers route on your server, on the real server hosting your web app, there are chances you don't have that route here because you only have one file there, index.html containing your Angular app and you want Angular to take over and to parse this route but it will never get a chance if your server decides no, I don't know the route, here's your 404 error page. Therefore you need to make sure that in such a case, your web server returns the index.html file.

  • If for some reason, you can't get this to work or you need to support very old browsers then you can fallback to our older technique which was used a couple of years ago, using a (#) hash sign in your routes.

  • You can enable it in your app-routing.module where you register your routes with the forRoot method. You can pass a second argument:

    @NgModule({
      imports: [
         RouterModule.forRoot(appRoute, { useHash: true })
      ],
      exports: [
         RouterModule
      ]
    })
  • The default is false which is why we didn't have to pass it. By setting it to true, we have this hashtag in our URL i.e.

    • Ordinary URL : http://localhost:4200/servers
    • Hash mode URL: http://localhost:4200/#/servers
  • What this hashtag will do is, it informs your web server, hey only care about the part in this URL before this hashtag, so all the parts thereafter will be ignored by your web server. Therefore this will run even on servers which don't return the index.html file in case of 404 errors because they will only care about the part in front of the hashtag. By default and the part after the hashtag can now be parsed by your client i.e. Angular.

^ Back to Top ^


Miscellaneous

Article

^ Back to Top ^


Sample application using Routing

Angular official Guide
techiediaries
smashingmagazine
codecraft
angularindepth
imp - tektutorialshub
stackoverflow - pathmact: prefix vs full

^ Back to Top ^

⚠️ **GitHub.com Fallback** ⚠️