-
Notifications
You must be signed in to change notification settings - Fork 1
Routing & Navigation
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
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
-
<base href>
:- Most routing applications should add a
<base>
element to theindex.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="/">
- Most routing applications should add a
-
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';
- 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,
-
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 theAppModule'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 andcomponent
represents the action to be performed on reaching that path - The
appRoutes
array of routes describes how to navigate. Pass it to theRouterModule.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.
-
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>
-
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
-
Active Router Link
- The
RouterLinkActive
directive toggles css classes for activeRouterLink
bindings based on the currentRouterState
. - On each anchor tag, you see a property binding to the
RouterLinkActive
directive that look likerouterLinkActive="..."
. - 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 givenRouterLink
will only be active if its URL is an exact match to the current URL.
- The
-
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
-
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.
-
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.euser/<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.
-
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. nameconst 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 aparams
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 ID1
, nameAakash
, if we change the ID to3
, we see ID3
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 thesnapshot
, there is someparams
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 bysubscribing
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 hookngOnDestroy(): 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!
-
Using
routerLink
- If I have a
routerLink
configured on a relative path, on clicking this URL, I will get errorError: 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 belocalhost: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>
- If I have a
-
Using
Router
andActivatedRoute
- If I have an relative path setup and I am using Router service and
ActivatedRoute
, I will get errorError: 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:
- 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});
- Use absolute path:
this.router.navigate(['/servers']);
- Use
- If I have an relative path setup and I am using Router service and
-
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
, wherepage=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
, whereintroduction
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.
- 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 theid
is not provided. It will navigate toProductComponent
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.
-
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 viarouter.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 } }); }
-
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'});
-
Reading the Query parameters is similar to reading the Router Parameter. There are two ways by which you can retrieve the query parameters.
-
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. ThequeryParams
is accessible viaActivatedRoute
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 thequeryParams
of theActivatedRoute
, which returns the observable of typeParams
. 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 */ });
- The
-
Using Snapshot
-
You can also read the value of the query parameter from
queryParams
using the snapshot property of theActivatedRoute
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.
-
-
-
Reading Fragments is same as that of Query Parameters
-
Subscription
this.sub = this.route.fragment .subscribe(params => { this.pageNum = +params.get('pageNum')||0; /* this.pageNum = (params.pageNum) ? +params.pageNum : 0 */ });
-
Snapshot:
this.route.snapshot.fragment;
-
Subscription
-
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-
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>
-
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>
-
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>
-
- Child Routes or Nested routes are routes within other 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
andEditRoutingServerComponent
as the child of theRoutingServersComponent
, we need to add thechildren
key to theservers
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 pathservers
and instantiates theRoutingServersComponent
and displays it in the<router-outlet>
directive of its parent component ( which isAppComponent
) -
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 theRoutingServerComponent
and renders it in the<router-outlet>
directive present in theRoutingServersComponent
-
The components are always rendered in the
<router-outlet>
of the parent component. -
For
RoutingServerComponent
the parent component isRoutingServersComponent
and not theAppComponent
. Hence, we need to add<router-outlet></router-outlet>
in the servers.component.html
-
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).
-
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 staticdata
with the data property here. Thedata
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
, likequeryParams
, 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.
-
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, ... ], ...
-
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
- CanActivate
- CanActivateChild
- CanDeactivate
- Resolve
- CanLoad
-
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 acanActivate()
method in this class. -
The
canActivate()
method now will receive two arguments, theActivatedRouteSnapshot
and the state of the router, so theRouterStateSnapshot
. -
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. SocanActivate()
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 withcanActivate()
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 addingcanActivate
, 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 myAuthGuard
(auth-guard.service.ts
) and this will make sure that servers and all the child routes are only accessible if the auth-guardcanActivate()
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
-
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 useCanActivateChild
guard. -
Let's implement this interface too which is also provided by
@angular/router
and this interface requires you to provide aCanActivateChild()
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 haveCanActivateChild()
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.
-
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 achangesSaved
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 itCanComponentDeactivate
and this interface will require one thing from the component which implements it, this component should have acanDeactivate()
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 implementCanDeactivate
: 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 thecanDeactivate()
method. -
Now this class here, this guard will also need to have a
canDeactivate()
method. This is thecanDeactivate()
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 typeCanComponentDeactivate
. 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 executecanDeactivate()
in our service and can rely on the fact that the component we're currently on has thecanDeactivate()
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 likecanActivate
and here, we now will point to ourCanDeactivateGuard
. 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 callcanDeactivate
, our component. Well, for this to work on ouredit-server.component
, here we need to implement ourCanComponentDeactivate
interface. This interface now forces us to implement thecanDeactivate()
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.
- 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 useresolver
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
orcanDeactivate
which will allow us to run some code before a route is rendered. -
Now the difference to
canActivate
is that theresolver
will not decide whether this route should be rendered or not, whether the component should be loaded or not, theresolver
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 aresolver
. -
Lets create a service,
server-resolver
, this has to implement theresolve
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 theresolve()
method and thisresolve()
method takes two arguments, theActivatedRouteSnapshot
andRouterStateSnapshot
. 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 datangOnInit() { /* 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 theresolvers
we want to use ie. our resolver service. This will now map the data. This resolver gives us back some data withresolve()
method that was implemented, this method will be called by Angular when this router is loaded.
-
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.
-
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 theindex.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 theindex.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 theforRoot
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 totrue
, we have this hashtag in our URL i.e.- Ordinary URL :
http://localhost:4200/servers
- Hash mode URL:
http://localhost:4200/#/servers
- Ordinary URL :
-
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 of404
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.
Angular official Guide
techiediaries
smashingmagazine
codecraft
angularindepth
imp - tektutorialshub
stackoverflow - pathmact: prefix vs full