Thi Notes
AboutNotesBlogTopicsToolsReading
About|Sketches |Cooking |Cafe icon Support Thi

Note for course Angular 2: Services & DI & Routing

Note for course Angular 2: Services & DI & Routing

Anh-Thi Dinh
Angular
MOOC
mooc-angular

TIPS

Get a copy, not access directly,
πŸ‘‰ Spread operator. (...var)

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

  • Course video.
  • 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
πŸ‘‰ Codes for this section.

Data Service

Service: store and manage our data β†’ exchange property & event binding β†’ get event to app component.
πŸ‘‰ Codes for this section.
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!!!!)
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!
πŸ‘‰ Codes for this section.
πŸ‘‰ 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.
πŸ‘‰ Codes for this section

Routing β†’ change pages

πŸ‘‰ Codes for this section
πŸ‘‰
Example of final project using routing.

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

routerLink

Add some links to app (video)

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.

routerLinkActive

We use <a class="active"> for current tab. ← how to use angular to add/remove this class auto?
(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

Navigating programmatically

Perform navigation after users click on some button, for example.
πŸ‘‰ Codes for this section
If we want .navigate() knows where we are,

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.
πŸ‘‰ Codes for this section.
Get the id from the router,
GOOD PRACTICE: Always .unsubscribe() the observable has been created!

Query params (?...)

πŸ‘‰ Video for this section.
How to retrieve the informatoin from the URLs? β‡’ check video.

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.

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?

Redirect & wildcat routes

For example: building 404 page.
Error of path: '' (nothing) ← default behavior to check in angular is "prefix" (check from beginning) β†’ every urls contains this "nothing" ('')

app-routing.module.ts

Store all routing tasks in a single file. β†’ no need to stored in app.module.ts.
πŸ‘‰ Example file.

Login & Guards & Authentication

(video) Create auth-guard.service.ts (file) containing the service to control the authentication. β†’ use CanActivate ← angular executes this before router loaded!
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?
(video) Show the father route list, only protect child routes? β†’ use CanActivateChild
(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.

Parsing static data to route

(video + code) Navigate to an error page with custom message from route.

Parsing dynamic data (server) to route

(video + codes) Display the component after fetch from the server (async data) β†’ should watch the videos + below notes:

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.
In this post
β—†TIPSβ—†Services & Dependency Injectionβ—‹Service?β—‹DI & Logging serviceβ—‹Data Serviceβ—‹Hierarchical injectorβ—‹Inject Services into Servicesβ—‹Services with cross-componentsβ—‹Service - pushing data from A-Bβ—†Routing β†’ change pagesβ—‹Adding Routesβ—‹routerLinkβ—‹Understand pathsβ—‹routerLinkActiveβ—‹Navigating programmaticallyβ—‹Add params to Routeβ—‹Query params (?...)β—‹Nested / Child routerβ—‹Preserve query params between pathsβ—‹Redirect & wildcat routesβ—‹app-routing.module.tsβ—‹Login & Guards & Authenticationβ—‹Parsing static data to routeβ—‹Parsing dynamic data (server) to routeβ—‹Location strategies
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}
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}
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}
1// reference pointing
2this.accounts = this.accountsService.accounts;
3// they are actually the same object (ie. this.accountsService.accounts)
1// service B
2
3// service A
4@Injectable()
5export class ServiceA {
6	constructor(private serviceB: ServiceB) {}
7	// something using serviceB
8}
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}
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}
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!!!
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) -->
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
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
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}
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}
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]
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>
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}
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}
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
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}
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]
1// errors
2{ path: '', redirectTo: '/somewhere-else' }
3
4// fix
5{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }
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})
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}
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
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
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}
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!'} }
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}
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 ...