What is routing?
- In Angular, routing is used to navigate between different views or components in a single-page application (SPA).
Setting up Routes
- Configure the routes in app.module.ts as an array of json objects representing the different routes for our app
- import RouterModule in app.module.ts and pass our array to the .forRoot() method
- place router-outlet html element in the app.component.html where we want the content to load for each of the routes
Configuring app routes
1
2
3
4
5
6
7
8
9
10
| const appRoutes: Routes = [
// localhost/home
{path: 'home', component: HomeComponent},
// localhost/users
{path: 'users', component: UsersComponent},
// localhost/servers
{path: 'servers', component: ServersComponent},
// wildcard route
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
];
|
Importing RouterModule
1
2
3
4
5
| imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot(appRoutes) // make sure to import and place the array variable within the ()
],
|
Placing the router-outlet html element
1
2
3
4
5
| <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>
|
Why we can just use href=”routename” for redirection
- when we use href for an anchor tag, the browser will redirect you to that page instead of dynamically loading that spa
Using routerLinkActive
- use routerLinkActive when you want to apply some classes to a certain element only if it is on the route specified
- you can specify any class you want
Below example
- we are specifying 3 routes [home, servers, users]
- we want to apply the active class when it is on the respective route
- example if we are on the /servers route
- it will get dyanmically class=”active”
1
2
3
4
5
| <ul class="nav nav-tabs">
<li role="presentation" routerLinkActive="active" routerLink="home"><a>Home</a></li>
<li role="presentation" routerLinkActive="active" routerLink="servers"><a>Servers</a></li>
<li role="presentation" routerLinkActive="active" routerLink="users"><a>Users</a></li>
</ul>
|
Navigate programmatically
- steps
- inject Router into your code
- use .navigate() to navigate to the url
Inject the Router into your code by constructor injection
1
2
3
| constructor(private router: Router) {
}
|
Navigate to different url
1
2
3
4
5
6
| onLoadServers(){
// complex calculation ...
// navigate to /servers
this.router.navigate(['/servers'])
}
|
Path Parameters to route
- Steps
- Setup in the route configuration
- Inject ActivatedRoute into the constructor of the component
- ActivatedRoute.snapshop.params[‘paramName’] to access the parameter name you are looking for
Setup in route configuration with :id
1
2
3
4
5
| const appRoutes: Routes = [
// .. other routes
// ..
{path: 'users/:id', component: UserComponent}
]
|
Inject ActivatedRoute and access the path parameter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| export class UserComponent implements OnInit {
// our user
user: {id: number};
// injecting the ActivatedRoute
constructor(
private route: ActivatedRoute
){}
// NgOnInit
ngOnInit() {
// accessing the path parameters
this.user = {
id: this.route.snapshot.params['id']
}
console.log(this.user);
}
}
|
Angular not updating component when navigating to it twice
- in the previous example we created a route that looked like localhost/users/id
- if we were to have a button on the user html file that navigated to localhost/users/differentID
- if would navigate to that route but not update the content of that component
- why is this??
- this is because angular is smart enough to know that we are already on the component so no need to refresh if we are just going back to it even though it won’t update any of the data on it
ActivatedRoute.snapshot or without??
- what is the difference between
this.route.snapshot.params['id']
and this this.route.params.subscribe(()=> {})
- without snapshot is just an observable
- meaning we can subscribe so that way when a change is detected we can run some code to update our user
Now once a change is detected to the route parameters the code in the subscribe code block will be executed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| ngOnInit() {
// initial set
this.user = {
id: this.route.snapshot.params['id']
}
// subscribing
this.route.params
.subscribe(
(params: Params) => {
this.user.id = params['id']
}
)
}
|
Query parameters & fragement
- Setting in HTML template code
[queryParams]="{paramName: '1'}"
- setting query parameters in html template
fragment="fragmentName"
- setting the fragment name
- Settting up in Typescript component code
this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading' })
- this will route to /servers/id/edit with query parameter and fragement in url
- Retrieving Query parameters
Using Query parameters in the html temlpate code
1
2
3
4
5
6
7
8
9
10
11
12
13
| <!-- list of servers -->
<div class="col-xs-12 col-sm-4">
<div class="list-group">
<a
[routerLink]="[server.id, 'edit']"
[queryParams]="{allowEdit: '1'}"
fragment="something"
class="list-group-item"
*ngFor="let server of servers">
</a>
</div>
</div>
|
Using Typescript code to go to route with path parameters, query parameters and fragment
1
2
| let id = 10;
this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading' })
|
Retrieving Query parameters in ts code
1
2
3
4
5
6
7
8
9
| ngOnInit() {
// gets the server
this.server = this.serversService.getServer(this.route.snapshot.params['id']);
// get the id of the server
this.paramsSubscription = this.route.params.subscribe(
(params) => this.currentId = params.id
);
}
|
Nesting Routes
- Steps
- Place the child routes in children array
- Place router-outlet html element tag in the parent component template where you want the children component to be loaded
Our typescript routes configruation goes from this to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const appRoutes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'users', component: UsersComponent},
{path: 'users/:id', component: UserComponent},
{path: 'servers', component: ServersComponent},
{path: 'servers/:id/edit', component: EditServerComponent},
{path: 'servers/:id', component: ServerComponent},
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
];
|
goes to this with nested routes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| const appRoutes: Routes = [
// localhost/home
{path: 'home', component: HomeComponent},
// localhost/users
{path: 'users', component: UsersComponent, children: [
{path: ':id', component: UserComponent},
]},
// localhost/servers
{path: 'servers', component: ServersComponent, children: [
{path: ':id', component: ServerComponent},
{path: ':id/edit', component: EditServerComponent},
]},
// wildcard route
{ path: '**', redirectTo: '/home', pathMatch: 'full' },
];
|
1
2
3
| <div class="col-xs-12 col-sm-4">
<router-outlet></router-outlet>
</div>
|
Route Guards
- Are a way of protecting routes in angular
- there are multiple route guard types
Route Guard | Description |
---|
canActivate | Protects route and all of its child routes |
canActivateChild | Protects all the child routes |
canDeactivate | Controll if the user can navigate away from a route |
canActivate example
We are creating the function that holds our logic GuardCheck()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class UserService {
isAllowed: boolean = false;
GuardCheck(){
this.isAllowed = false;
return of(this.isAllowed).pipe(tap( (v) => console.log(v)));
}
}
|
We are exporting our guard function that takes in ActivatedRouteSnapshot
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| import { inject } from "@angular/core";
import { ActivatedRouteSnapshot, createUrlTreeFromSnapshot } from "@angular/router";
import { UserService } from "./user.service";
import { map } from "rxjs/operators";
export const authGuard = (next: ActivatedRouteSnapshot) => {
return inject(UserService)
.GuardCheck()
.pipe(
map( (isAllowed) =>
isAllowed ? true : createUrlTreeFromSnapshot(next, ['/', 'notFound'])
)
)
}
|
We are adding the exported function created above and putting it in our canActivate array
1
2
3
4
5
6
| const appRoutes: Routes = [
// localhost/home
{path: 'home', component: HomeComponent, canActivate: [authGuard]},
// ... other routes defined here
]
|
canDeactivate example
- used when you want execute some action or logic when a user leaves a route
- the example we are going to do is check if a user has saved changes before leaving a route
- There was a pretty major change where angular went from class based to function based route guards
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| @Component({
// ..
})
export class EditServerComponent implements OnInit{
// ..
public changesSaved: boolean = false;
// constructor
constructor(
private serversService: ServersService,
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit() {
// ..
}
canDeactivate(){
if(this.changesSaved === false){
return confirm('Are you sure you want to exit without saving changes');
}
}
}
|
1
2
3
4
5
| // localhost/servers
{path: 'servers', component: ServersComponent, children: [
{path: ':id', component: ServerComponent},
{path: ':id/edit', component: EditServerComponent, canDeactivate: [ (component) => component.canDeactivate() ]},
]},
|
Passing data to a route
Passing Static data to a route
- Steps
- setup route configuration with the data property assigned with a json object
- read the contents of the data by injecting ActivatedRoute into the component and using
route.snapshot.data['key']
or using route.data.subscribe( (data: Data) => {this.message = data['key']})
1
| {path: 'notFound', component: ErrorPageComponent, data: {message: 'Page not found!'}},
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| export class ErrorPageComponent implements OnInit{
message: string;
constructor(private route: ActivatedRoute){
}
ngOnInit(): void {
// option 1
this.message = this.route.snapshot.data['message'];
// option 2
this.route.data.subscribe(
(data: Data) => this.message = data['message']
)
}
}
|
Passing Dynamic data to route
- depreciated Resolve to ResolveFn
- the only way to achieve this before angular 15 was to use a Resolver
- Class-based resolvers have been already deprecated in the Angular v15 release, but now with version 16, when the functional approach is even more common
- Steps
- Create the ResolverFn function the thing that is is going to be returning should be passed as the generic type in diamond brackets
Resolver function created here and exported
1
2
3
4
| export const ServerResolver: ResolveFn<Server> =
(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
return inject(ServersService).getServer(+route.params['id']);
}
|
The resolve key is used here
- it takes a js object as the value
- key in the js object can be whatever you want
- the value is the name of the resolver function
1
| {path: ':id', component: ServerComponent, resolve: {server: ServerResolver} },
|
Now in this case of the ServerComponent now before ngOnInit is called it will have the data passed by the step above in the route.data object
1
2
3
4
5
6
7
8
9
10
11
12
13
| constructor(
private serversService: ServersService,
private route: ActivatedRoute
) { }
ngOnInit() {
// gets the server to render from the data that is published by the server resolver
this.route.data.subscribe(
(data: Data) => {
this.server = data['server'];
}
);
}
|
Location Strategy
- There are two main types of location stragety for angular
- what is location strategy
- Path Location stragety (default)
- normal default localhost/user/1
- hash location strategy
- localhost/#/user/1
- anything before the # is the host
- anything after the # is the endpoint for the host
Hash Stategy
- easy to implement
- useHash in app.module.ts
1
2
3
4
5
6
7
8
| @NgModule({
imports: [
RouterModule.forRoot(appRoutes, {useHash: true})
],
// ..
})
export class AppRoutingModule {
}
|
Observables
- can be thought of as a data source
- follows the observable timeline
- timeline
- mulitple events emitted by the observable
- the observer does something when that happens. (subscribe function)
- 3 types of data you can receive
- handle data
- handle errors
- handle completion
- angular observables are unsubscribed and handled by angular
- observalbe will stop when an error is pushed or it is complete
Installation
npm install --save rxjs@6
npm install --save rxjs-compat
Observer data methods
method name | Description |
---|
.next() | next action for the observer |
.error() | send an error |
.complete() | tell the observer that it is complete |
Creating our own observable
- use the Observable() constructor to create a new Observable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| import {Observable} from 'rxjs';
// ..
// creating the observable
const observable = new Observable(
(observer) => {
let count = 0;
setInterval(() => {
observer.next(count++);
if(count == 10){
// observer.error('there was a fucking error');
observer.complete();
}
}, 1000)
}
)
observable.subscribe(
(data) => {
console.log(`data = ${data}`);
},
(error) => {
console.log(`there was an error dickwad`);
console.log(error);
},
() => {
console.log(`complete`);
}
)
|
Operators
- operators sit between the observable data and the subscription maniuplating data in between
- so that means data gets transformed before reaching the observer
- there are many different operators
Example uses map() to transform data before the observer receives the data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // transform data before subscribing
observable.pipe(map( (data: number) => {
return 'Round ' + (data + 1)
}))
.subscribe(
(data) => {
console.log(`data = ${data}`);
},
(error) => {
console.log(`there was an error`);
console.log(error);
},
() => {
console.log(`observable complete`);
}
)
|
Example using filter and map to
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| observable.pipe(
// filter results
filter( (data: number) => {
return data > 0;
}),
// map or transform results
map( (data: number) => {
return 'Round ' + (data + 1)
})
)
.subscribe(
(data) => {
console.log(`data = ${data}`);
},
(error) => {
console.log(`there was an error`);
console.log(error);
},
() => {
console.log(`observable complete`);
}
)
|
Subjects
- subjects are not passive like observables, meaning that they can be triggered with code in the observer
- you use .next with a subject to emit an event or
- preferred way of communication
1
2
3
4
5
6
| @Injectable({
providedIn: 'root'
})
export class UserService {
activatedSubject = new Subject<boolean>()
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| export class AppComponent implements OnInit, OnDestroy {
activated = false;
activatedSubscription: Subscription;
constructor(private us: UserService) {}
ngOnInit() {
// subscribe to the subject
this.activatedSubscription = this.us.activatedSubject
.subscribe(
(data) => {
this.activated = data;
})
}
ngOnDestroy(): void {
this.activatedSubscription.unsubscribe();
}
}
|
1
2
3
| onActivate(){
this.us.activatedEventEmitter.next(true);
}
|