Note for course Angular 2: Services & DI & Routing

Anh-Thi Dinh

TIPS

Get a copy, not access directly,
1// for example
2export class ... {
3	private recipes: Recipe[] = [];
4
5	getRecipes() {
6		// get a copy from outside, not access the original recipes
7		return this.recipes.slice();
8	}
9}
👉 Spread operator. (...var)
1// ES6's feature (spread operator): tranform "[a, b, c]" to "a, b, c"
2// because we cannot .push([]), but we can .push(a,b,c)
3this.ingredients.push(...ingredients);
1<!-- if we just bind a string -->
2<div abcXyz="abc">
3<div [abcXyz]="'abc'">
4<div [abcXyz]="['abc']">
5
6<!-- if we bind objects -->
7<div abcXyz="{}">
1// convert from string to number
2const id = +this.abc['id']; // just add "+" before it!
1// an optional param in a method
2abc(required_1, required_2, optional?) { // with "?"
3	...
4}
1// When creating a new service (example.service.ts), you wanna add it in
2// app.module.ts in the section "providers"
3
4// You can make a shortcut right in the file example.service.ts
5// and no need to give it name in app.module.ts
6@Injectable({providedIn: 'root'})
7export class .... { }
1// inject a service in a component
2export class ... {
3	constructor(private serviceName: ServiceName) { }
4	// then you can use it here!
5}

Services & Dependency Injection

Service?

  • Don't duplicate tasks.
  • Access data and used in somewhere.
  • Just another class which help centralize your codes.
Why we need services? If we don't use, just use what we have (binding, emit,...) → more components and they need to communicate to each other → too complicated!

DI & Logging service

  • Naming: logging.service.ts
  • There is NO DECORATOR like @Service() → just a normal typescript class!
DI injects class (of a service) into our component automatically. ← we need to inform angular that we need to add this instant → add a constructor
1// new-account.component.ts
2import { LoggingService } from '../logging.service';
3
4@Component({
5	providers: [LoggingService] // 2) angular know how to gives us this instan
6})
7export class ... {
8
9	// 1) tell angular: we need an instance of LoggingService class
10	constructor(private loggingService: LoggingService) {}
11									//  ^ custom name
12
13	// use it
14	onCreateAccount() {
15		...
16		this.loggingService.logStatusChange(...); // angular created this for us auto
17//  ^ reuse this line multiple times in multiple components
18	}
19}

Data Service

Service: store and manage our data → exchange property & event binding → get event to app component.
1// reference pointing
2this.accounts = this.accountsService.accounts;
3// they are actually the same object (ie. this.accountsService.accounts)
Without account services, we have to emit & output our data and event (add acc for example). However, with services, we don't need them anymore, just inject the service and put the account into it!

Hierarchical injector

Inject a service to father → all its child component get the same instance of the service! ⇒ only go down in the tree components → if there is a duplicate in child, it will overwrite the father's.
[video] If don't want create A NEW INSTANCE IN CHILD (which will overwrite the one coming from father) → just remove the service in providers! → it will use the service of the father.

Inject Services into Services

  • Normally, if service A doesn't contain (inject) any service → no need @Injectable()
  • If we wanna inject service B into service A → we need to add @Injectable() into A (not B!!!!)
    • 1// service B
      2
      3// service A
      4@Injectable()
      5export class ServiceA {
      6	constructor(private serviceB: ServiceB) {}
      7	// something using serviceB
      8}
GOOD PRACTICE: ALWAYS ADD @Injectable() for all services!

Services with cross-components

With services, we don't have to build complex inputs, outputs chanes where you pass events and properties to get data from component A to B,... → much cleaner!
1// accounts.service.ts
2@Injectable()
3export class AccountsService {
4	statusUpdated = new EventEmitter<string>();
5}
6
7// account.component.ts
8@Component({})
9export class AccountComponent {
10  @Input() account: {name: string, status: string};
11  constructor(private accountsService: AccountsService) {}
12					 // ^ a shorthand to create a property with the same name as
13					 //   "accountService" <- we can "this.accountService".
14  onSetTo(status: string) {
15    ...
16    this.accountsService.statusUpdated.emit(status); // emit an event
17  }
18}
19
20// new-account.component.ts
21@Component({})
22export class NewAccountComponent {
23	constructor(private accountsService: AccountsService) {
24    this.accountsService.statusUpdated.subscribe( // event is observable!
25      (status: string) => alert('New Status: ' + status) // capture that event!
26    );
27  }
28}
👉 Example: exchange active / inactive users.
👉
Project with recipes and shopping-list. (change from using EventEmitter to service) — videos

Service - pushing data from A-B

When we use .slice() to copy a list (to work on this), there may be some event cannot access the original one (eg. when using addIngredients) → we need to emit an event containing the original list.
1// shopping-list.service.ts
2export class ... {
3	ingredientsChanged = new EventEmitter<Ingredient[]>();
4	private ingredients: Ingredient[] = [...];
5	...
6	addIngredient(ingredient: Ingredient) {
7    this.ingredients.push(ingredient);
8    this.ingredientsChanged.emit(this.ingredients.slice());
9  }
10}
1// shopping-list.component.ts
2export class ... implements OnInit {
3	ingredients: Ingredient[];
4	constructor(private slService: ShoppingListService) { }
5	ngOnInit() {
6	  ...
7	  this.slService.ingredientsChanged
8	    .subscribe(
9	      (ingredients: Ingredient[]) => {
10	        this.ingredients = ingredients;
11	      }
12	    );
13	}
14}

Routing → change pages

Adding Routes

Angular ships its own router which allows to change URLs of our application.
Where? ⇒ Because the router controls URLs of all things in our apps, the place we can put it is in app.module.ts
1// app-routing.module.ts
2//----------------------
3import { Routes, RouterModule } from '@angular/router';
4
5const appRoutes: Routes = [
6	{ path: '', component: HomeComponent },
7	{ path: 'users', component: UsersComponent }, // something: http//localhost:4200/users
8				//^ without "/"
9	{ path: 'servers', component: ServersComponent }
10]
11
12@NgModule({
13	import: [
14		RouterModule.forRoot(appRoutes)
15							// ^ register our routes to the RouterModule of our app
16	]
17})
1// where to display component after click?
2// app.component.html
3//----------------------
4<router-outlet></router-outlet> // <- a directive shipped by angular!!!
Add some links to app (video)
1<!-- if we add links to a normal <a> => it will reload the app! -->
2<!-- => USE A SPECIAL DIRECTIVE "rounterLink" -->
3<!-- app.component.html -->
4<a routerLink="/">
5<a routerLink="/servers">
6<a [routerLink]="['/users']">
7			<!-- ^ we can use "'/users'" <- has to have '' because without it, -->
8			<!-- |   angular will look for a property "/users" instead of a string -->
9			<!-- ^ we use [] to add more complicated path here -->
10			<!--   for example, ['/users', 'something'] <- /users/something -->
11
12<!-- routerLink capture the click event + prevent the default behavior (which reloads -->
13<!-- entire our app) -->

Understand paths

(video) Why we need "/" before "/servers"? → if we on home page, it't normal, but if we in subpage (eg. /servers), if there is another link to "servers" (eg. <a routerLink='servers'>), it will be "/servers/servers" ← error!!!
We can relative / absolute paths inside routerLink.
1routerLink="/abc" // abs path: localhost:4200/abc
2routerLink="abc" // if you are in localhost:4200/xyz -> localhost:4200/xyz/abc
3routerLink="./abc" // relative path: current position + /abc
4routerLink="../abc" // relative path: father path + /abc

routerLinkActive

We use <a class="active"> for current tab. ← how to use angular to add/remove this class auto?
1// don't forget to add "routerLinkActive" to ALL "li"
2<li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
3									//  ^css class                      //  ^only active if it's exactly the FULL path
4	<a routerLink="/servers">Servers</a>
5</li>
6
7// we add "routerLinkAcitve" inside <a> if we want
(video) routerLinkActive will check if the current path contains the path given in routerLink ← the empty path, ie. "/" is in all paths!!! → this may lead to a problem in which "Home" tab is always "active" ⇒ we need routerLinkActiveOptions
Perform navigation after users click on some button, for example.
1// home.component.html
2<button (click)="onLoadServers()">Load servers</button>
3
4// home.component.ts
5export class ... {
6	constructor(private router: Router) { }
7	onLoadServers() {
8		// complex calculations
9		this.router.navigate(['/servers']);
10	}
11}
If we want .navigate() knows where we are,
1// servers.component.html
2<button (click)="onReload()">Reload page</button>
3
4// servers.component.html
5export class ... {
6	constructor(private router: Router,
7							private route: ActivatedRoute) {
8													// ^simply inject current route which load the component
9													// a route => simply a javascript object
10}
11
12	onReload() {
13		this.router.navigate(['/servers']);
14						// ^with this method, we don't get the problem of
15						// /servers/servers like in routerLink if we use ['servers']
16						// REASON: .navigate() doesn't know where we currently are, it just
17						//   know the path of template -> '/servers' is the same as 'servers'
18
19		// If we wanna use relative path with .navigate()?
20		this.router.navigate(['/servers'], {relativeTo: this.route});
21											//  ^             ^with this, angular knows what is currenty
22											//  |                active route
23											//  | we can use ['../'] (like a relative path)
24	}
25}

Add params to Route

(Videos: Passing params to Routes + Fetching Route params + fetch reactively) For example we wanna navigate to users, each user a path → change with ids. ← id will be the param of the Route.
1// app-routing.module.ts
2const appRoutes: Routes = [
3	{ path: 'users/:id/:name', component: UserComponent }
4					//     ^without this, localhost/users/something will get error!
5]
Get the id from the router,
1// user.component.ts
2import { ActivatedRoute, Params } from '@angular/router';
3import { Subscription } from 'rxjs/Subscription';
4
5export class ... {
6	user: {id: number, name: string};
7	paramsSubscription: Subscription;
8
9	constructor(private route: ActivatedRoute) { }
10					//  ^ gives us access to the id passed in the URL -> selected user
11
12	ngOninit() {
13		// OPTION 1: INITIAL ONLY
14		this.user = {
15			id: +this.route.snapshot.params['id'], // <- from 'users/:id'
16								//   ^it's just a snapshot of the 1st initialization
17			name: this.route.snapshot.params['name'] // <- from 'users/:id/:name'
18									//     if we change url dynamically, it won't work!
19		};
20
21		// OPTION 2: subscribe to the change of url (whenever we click)
22		this.paramsSubscription
23			= this.route.params
24							 // ^it's an observable -> help you work with async tasks
25																											 //   ^in the future, users perform some tasks
26																											 //   -> you don't know where, when, how long,...
27	      .subscribe(
28	        (params: Params) => {
29	          this.user.id = +params['id']; // "+" to convert to number
30	          this.user.name = params['name'];
31	        }
32	      );
33	}
34
35	ngOnDestroy() {
36    this.paramsSubscription.unsubscribe(); // end of subscription!
37		// in fact, for this router, you don't have to do this because
38		//   angular will destroy the subscription for you
39		//   but in general case for OBSERVABLE THAT YOU CREATED,
40		//   you should .unsubcribe() it!
41  }
42}
1// user.component.html
2<p>User with ID {{ user.id }}</p>
3<p>User with name {{ user.name  }}</p>
GOOD PRACTICE: Always .unsubscribe() the observable has been created!

Query params (?...)

1// inside an a tag in html
2<a
3  [routerLink]="['/servers', server.id]" // locahost/servers/3
4  [queryParams]="{allowEdit: server.id === 3 ? '1' : '0'}" // .../3?allowEdit=1
5  fragment="loading" // ...?allowEdit=1#loading
6  >
7  {{ server.name }}
8</a>
1// navigate from a button?
2// servers.component.html
3<button (click)="onReload()">Reload page</button>
1// servers.component.html
2export class ... {
3	constructor(private router: Router) { }
4
5	onReload(id: number) {
6		// localhost/servers/1/edit?allowEdit=1#loading
7		this.router.navigate(['/servers', id, 'edit'],
8			{
9				queryParams: {allowEdit: '1'},
10				fragment: 'loading'
11			}
12		);
13	}
14}
How to retrieve the informatoin from the URLs? ⇒ check video.
1// edit-server.component.ts
2constructor(private route: ActivatedRoute) { }
3								 // ^simply inject current route which load the component
4
5// 1st approach -> only get the ones created on init
6ngOnInit() {
7	console.log(this.route.snapshot.queryParams);
8	console.log(this.route.snapshot.fragment);
9}
10
11// 2nd approach -> allow you to react to the change of query params
12ngOnInit() {
13	this.route.queryParams.subscribe();
14	this.route.fragement.subscribe();
15}

Nested / Child router

(video + codes) No need to change to a new page for each child component, just display them on a sidebar (a part of the view) when we click on the navigator.
1// app-routing.module.ts
2const appRoutes: Routes = [
3	{ path: 'servers', component: ServersComponent, children: [
4		{path: ':id', component: ServerComponent},
5		{path: ':id/edit', component: EditServerComponent }
6	] }
7]
1// servers.component.html
2<router-outlet></router-outlet> // replace "old" <app-server> and <app-edit-server>
3//^ all the child routes inside this component will be shipped by angular
4//  -> that's why we have several "the same" <router-outlet> in our app

Preserve query params between paths

Before enter to edit-server, there is ?allowEdit=1, however, after navigate to edit-server, this info is gone. How to preserve it?
1// server.component.ts
2onEdit() {
3	this.router.navigate(
4		['edit'],
5		{
6			relativeTo: this.route,
7			queryParamsHandling: 'preserve'
8											//   ^keep the old + overwrite to the new one
9											//   ^'merge' if you wanna merge old + new
10		}
11	);
12}

Redirect & wildcat routes

For example: building 404 page.
1const appRoutes: Routes = [
2	{ path: 'not-found', component: PageNoteFoundComponent },
3	// a specific path
4	{ path: 'something', redirectTo: '/not-found'  }, // redirect /something to /not-found
5	// all the paths (performed after THE ABOVE <- order makes sense!!!!)
6	{ path: '**', redirectTo: '/not-found' }
7]
Error of path: '' (nothing) ← default behavior to check in angular is "prefix" (check from beginning) → every urls contains this "nothing" ('')
1// errors
2{ path: '', redirectTo: '/somewhere-else' }
3
4// fix
5{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }

app-routing.module.ts

Store all routing tasks in a single file. → no need to stored in app.module.ts.
1// app-routing.module.ts
2// no need to re-declare components like already done in app.module.ts
3const appRoutes: Routes = [...];
4
5@NgModule({
6	imports: [
7		RouterModule.forRoot(appRoutes)
8	],
9	exports: [RouterModule]
10})
11export class AppRoutingModule { }
1// app.module.ts
2...
3@NgModule({
4	...
5	imports: [
6		AppRoutingModule
7	]
8	...
9})

Login & Guards & Authentication

(video) Create auth-guard.service.ts (file) containing the service to control the authentication. → use CanActivateangular executes this before router loaded!
1// auth-guard.service.ts
2export class AuthGuard ....{
3	canActivate(
4	  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
5
6	// Obersvable / Promise -> some tasks need auth from server/databse <- async
7	// boolean -> some tasks completely on client <- sync
8}
Example: auth.service.ts → a fake service for the testing. In real app, we use this file to get the info / confirmation from the server about the authentication!
Apply to routes?
1// app-routing.module.ts
2{path: '...', canActivate: [AuthGuard], component: ..., children: ...} // apply also for children
3//            ^add this to the path you wanna apply auth
(video) Show the father route list, only protect child routes? → use CanActivateChild
1// auth-guard.service.ts
2export class AuthGuard ....{
3	canActivate(
4	  state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
5
6	canActivateChild(...){...}
7}
8
9// app-routing.module.ts
10{path: '...', canActivateChild: [AuthGuard], component: ..., children: ...}
11
(video + file) Control whether you are allowed to leave a route or not ← Confirm to leave the input/changes!!! solve the problem of user accidentially navigating away!!!
Idea: angular router can execute canDeactivate() in a service (can-deactivate-guard.service.ts) > component we are currently on has canDeactivate() ← how guard communicates with our components.
1// can-deactivate-guard.service.ts
2export interface CanComponentDeactivate {
3	canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
4}
5
6export class CanDeactivateGuard implements canDeactivate<CanComponentDeactivate> {
7	canDeactivate(component: CanComponentDeactivate, ...): ... {
8		return component.canDeactivate();
9	}
10}
1// app-routing.module.ts
2{path: ..., ..., canDeactivate: [CanDeactivateGuard]}
3
4// app.module.ts
5providers: [CanDeactivate]
6
1// edit-server.component.ts
2export class ... implements CanComponentDeactivate {
3	canDeactivate():... {
4		// check if there are some changes and return true/false
5	}
6}

Parsing static data to route

(video + code) Navigate to an error page with custom message from route.
1// error-page.component.html
2<h4>{{ errorMessage }}</h4>
1// error-page.component.ts
2
3export class ErrorPageComponent implements OnInit {
4	errorMessage: string;
5
6	constructor(private route: ActivatedRoute) { }
7									//  ^simply inject current route which load the component
8
9	ngOnInit() {
10		this.errorMessage = this.route.snapshot.data['message'];
11																				//  ^ there is "data" in app-routing.module.ts
12		// or subscribe to the changes
13		// (including the init snapshot)
14		this.route.data.subscribe(
15      (data: Data) => {
16        this.errorMessage = data['message'];
17      }
18    );
19	}
20}
1// app-routing.module.ts
2{ path: 'not-found', component: ErrorPageComponent, data: {message: 'Page not found!'} }

Parsing dynamic data (server) to route

(video + codes) Display the component after fetch from the server (async data) → should watch the videos + below notes:
1// server-resolver.service.ts
2resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Server> | Promise<Server> | Server {
3  return this.serversService.getServer(+route.params['id']);
4													// ^this service here will rerendered whenever we
5													// rerender the route
6}
7// unlike the component itself, this is executed each time, so no need to set
8// up an observable or something like that
1// app-routing.module.ts
2{ path:..., component:..., resolver: {server: ServerResolver} }
3																	//  ^choose a name you like
4// but make sure "server" is set the same in
5// server.component.ts_____________________
6ngOnInit() {                         //    |
7	this.route.data                  //      |
8		.subscribe(                  //        |
9			(data: Data) => { this.server = data['server'] }
10		);
11}

Location strategies

(video) When deployment, all URLs are parsed by the server (which hosts your app) first → then angular → route of angular (eg. nagivate something strange page to not found) may not work like on localhost. → need to make sure your web server return html file you want!
Hash mode routing → informs your web server on care the part of URL before "#"! → all things behind will be ignored by webserver.
1localhost:/users -> localhost:/#/users
2													//   ^from this to end: ignored by webserver
1// app-routing.module.ts
2@NgModule({
3	imports: [
4		RouterModule.forRoot(appRoutes, {useHash: true})
5	]
6})
7export ...