Angular 2 - Services & DI & Routing

Last modified 9 months ago / Edit on Github

This is my note for the course "Angular - The Complete Guide (2021 Edition)" which is taught by Maximilian SchwarzmΓΌller on Udemy. This is an incredible course for you to start to learn Angular from basic. The official Angular docs is good but it's not for the beginner.

πŸ„ PART 1 β€” Angular 1 - Basics & Components & Databinding & Directives
πŸ„ PART 2 β€” Angular 2 - Services & Dependency Injection & Routing
πŸ„ PART 3 β€” Angular 3 - Observable
πŸ„ PART 4 β€” Angular 4 - Forms
πŸ‘‰ Github repo
πŸ‘‰ Other note (taken before this course): Angular 101

This note contains only the important things which can be look back later, it cannot replace either the course nor the official document!

TIPS

Get a copy, not access directly,

// for example
export class ... {
private recipes: Recipe[] = [];

getRecipes() {
// get a copy from outside, not access the original recipes
return this.recipes.slice();
}
}

πŸ‘‰ Spread operator. (...var)

// ES6's feature (spread operator): tranform "[a, b, c]" to "a, b, c"
// because we cannot .push([]), but we can .push(a,b,c)
this.ingredients.push(...ingredients);
<!-- if we just bind a string -->
<div abcXyz="abc">
<div [abcXyz]="'abc'">
<div [abcXyz]="['abc']">

<!-- if we bind objects -->
<div abcXyz="{}">
// convert from string to number
const id = +this.abc['id']; // just add "+" before it!
// an optional param in a method
abc(required_1, required_2, optional?) { // with "?"
...
}
// When creating a new service (example.service.ts), you wanna add it in
// app.module.ts in the section "providers"

// You can make a shortcut right in the file example.service.ts
// and no need to give it name in app.module.ts
@Injectable({providedIn: 'root'})
export class .... { }
// inject a service in a component
export class ... {
constructor(private serviceName: ServiceName) { }
// then you can use it here!
}

Services & Dependency Injection

Service?

  • Don't duplicate tasks.
  • Access data and used in somewhere.
  • Just another class which help centralize your codes.

Angular_2_-Services&Dependency_Injection&_Rout_fb1390f6e4ee48849ae39feb665bdf1b/Untitled.png

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.

// new-account.component.ts
import { LoggingService } from '../logging.service';

@Component({
providers: [LoggingService] // 2) angular know how to gives us this instan
})
export class ... {

// 1) tell angular: we need an instance of LoggingService class
constructor(private loggingService: LoggingService) {}
// ^ custom name

// use it
onCreateAccount() {
...
this.loggingService.logStatusChange(...); // angular created this for us auto
// ^ reuse this line multiple times in multiple components
}
}

Data Service

Service: store and manage our data β†’ exchange property & event binding β†’ get event to app component.

πŸ‘‰ Codes for this section.

// reference pointing
this.accounts = this.accountsService.accounts;
// 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.

Angular_2_-Services&Dependency_Injection&_Rout_fb1390f6e4ee48849ae39feb665bdf1b/Untitled_1.png

[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!!!!)

    // service B

    // service A
    @Injectable()
    export class ServiceA {
    constructor(private serviceB: ServiceB) {}
    // something using serviceB
    }

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.

// accounts.service.ts
@Injectable()
export class AccountsService {
statusUpdated = new EventEmitter<string>();
}

// account.component.ts
@Component(
{})
export class AccountComponent
{
@Input() account: {name: string, status: string};
constructor(private accountsService: AccountsService) {}
// ^ a shorthand to create a property with the same name as
// "accountService" <- we can "this.accountService".
onSetTo(status: string) {
...
this.accountsService.statusUpdated.emit(status); // emit an event
}
}

// new-account.component.ts
@Component(
{})
export class NewAccountComponent
{
constructor(private accountsService: AccountsService) {
this.accountsService.statusUpdated.subscribe( // event is observable!
(status: string) => alert('New Status: ' + status) // capture that event!
);
}
}

πŸ‘‰ 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

// shopping-list.service.ts
export class ... {
ingredientsChanged = new EventEmitter<Ingredient[]>();
private ingredients: Ingredient[] = [...];
...
addIngredient(ingredient: Ingredient) {
this.ingredients.push(ingredient);
this.ingredientsChanged.emit(this.ingredients.slice());
}
}
// shopping-list.component.ts
export class ... implements OnInit {
ingredients: Ingredient[];
constructor(private slService: ShoppingListService) { }
ngOnInit() {
...
this.slService.ingredientsChanged
.subscribe(
(ingredients: Ingredient[]) => {
this.ingredients = ingredients;
}
);
}
}

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

// app-routing.module.ts
//----------------------
import { Routes, RouterModule } from '@angular/router';

const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent }, // something: http//localhost:4200/users
//^ without "/"
{ path: 'servers', component: ServersComponent }
]

@NgModule({
import: [
RouterModule.forRoot(appRoutes)
// ^ register our routes to the RouterModule of our app
]
})
// where to display component after click?
// app.component.html
//----------------------
<router-outlet></router-outlet> // <- a directive shipped by angular!!!

Add some links to app (video)

<!-- if we add links to a normal <a> => it will reload the app! -->
<!-- => USE A SPECIAL DIRECTIVE "rounterLink" -->
<!-- app.component.html -->
<a routerLink="/">
<a routerLink="/servers">
<a [routerLink]="['/users']">
<!-- ^ we can use "'/users'" <- has to have '' because without it, -->
<!-- | angular will look for a property "/users" instead of a string -->
<!-- ^ we use [] to add more complicated path here -->
<!-- for example, ['/users', 'something'] <- /users/something -->

<!-- routerLink capture the click event + prevent the default behavior (which reloads -->
<!-- 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.

routerLink="/abc" // abs path: localhost:4200/abc
routerLink="abc" // if you are in localhost:4200/xyz -> localhost:4200/xyz/abc
routerLink="./abc" // relative path: current position + /abc
routerLink="../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?

// don't forget to add "routerLinkActive" to ALL "li"
<li routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
// ^css class // ^only active if it's exactly the FULL path
<a routerLink="/servers">Servers</a>
</li>

// 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.

πŸ‘‰ Codes for this section

// home.component.html
<button (click)="onLoadServers()">Load servers</button>

// home.component.ts
export class ... {
constructor(private router: Router) { }
onLoadServers() {
// complex calculations
this.router.navigate(['/servers']);
}
}

If we want .navigate() knows where we are,

// servers.component.html
<button (click)="onReload()">Reload page</button>

// servers.component.html
export class ... {
constructor(private router: Router,
private route: ActivatedRoute
) {
// ^simply inject current route which load the component
// a route => simply a javascript object
}

onReload() {
this.router.navigate(['/servers']);
// ^with this method, we don't get the problem of
// /servers/servers like in routerLink if we use ['servers']
// REASON: .navigate() doesn't know where we currently are, it just
// know the path of template -> '/servers' is the same as 'servers'

// If we wanna use relative path with .navigate()?
this.router.navigate(['/servers'], {relativeTo: this.route});
// ^ ^with this, angular knows what is currenty
// | active route
// | we can use ['../'] (like a relative path)
}
}

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.

// app-routing.module.ts
const appRoutes: Routes = [
{ path: 'users/:id/:name', component: UserComponent }
// ^without this, localhost/users/something will get error!
]

Get the id from the router,

// user.component.ts
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';

export class ... {
user: {id: number, name: string};
paramsSubscription: Subscription;

constructor(private route: ActivatedRoute) { }
// ^ gives us access to the id passed in the URL -> selected user

ngOninit() {
// OPTION 1: INITIAL ONLY
this.user = {
id: +this.route.snapshot.params['id'], // <- from 'users/:id'
// ^it's just a snapshot of the 1st initialization
name: this.route.snapshot.params['name'] // <- from 'users/:id/:name'
// if we change url dynamically, it won't work!
};

// OPTION 2: subscribe to the change of url (whenever we click)
this.paramsSubscription
= this.route.params
// ^it's an observable -> help you work with async tasks
// ^in the future, users perform some tasks
// -> you don't know where, when, how long,...
.subscribe(
(params: Params) => {
this.user.id = +params['id']; // "+" to convert to number
this.user.name = params['name'];
}
);
}

ngOnDestroy() {
this.paramsSubscription.unsubscribe(); // end of subscription!
// in fact, for this router, you don't have to do this because
// angular will destroy the subscription for you
// but in general case for OBSERVABLE THAT YOU CREATED,
// you should .unsubcribe() it!
}
}
// user.component.html
<p>User with ID </p>
<p>User with name </p>

GOOD PRACTICE: Always .unsubscribe() the observable has been created!

Query params (?...)

πŸ‘‰ Video for this section.

// inside an a tag in html
<a
[routerLink]="['/servers', server.id]" // locahost/servers/3
[queryParams]="{allowEdit: server.id === 3 ? '1' : '0'}" // .../3?allowEdit=1
fragment="loading" // ...?allowEdit=1#loading
>

</a>
// navigate from a button?
// servers.component.html
<button (click)="onReload()">Reload page</button>
// servers.component.html
export class ... {
constructor(private router: Router) { }

onReload(id: number) {
// localhost/servers/1/edit?allowEdit=1#loading
this.router.navigate(['/servers', id, 'edit'],
{
queryParams: {allowEdit: '1'},
fragment: 'loading'
}
);
}
}

How to retrieve the informatoin from the URLs? β‡’ check video.

// edit-server.component.ts
constructor(private route: ActivatedRoute) { }
// ^simply inject current route which load the component

// 1st approach -> only get the ones created on init
ngOnInit() {
console.log(this.route.snapshot.queryParams);
console.log(this.route.snapshot.fragment);
}

// 2nd approach -> allow you to react to the change of query params
ngOnInit() {
this.route.queryParams.subscribe();
this.route.fragement.subscribe();
}

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.

// app-routing.module.ts
const appRoutes: Routes = [
{ path: 'servers', component: ServersComponent, children: [
{path: ':id', component: ServerComponent},
{path: ':id/edit', component: EditServerComponent }
] }
]
// servers.component.html
<router-outlet></router-outlet> // replace "old" <app-server> and <app-edit-server>
//^ all the child routes inside this component will be shipped by angular
// -> 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?

// server.component.ts
onEdit() {
this.router.navigate(
['edit'],
{
relativeTo: this.route,
queryParamsHandling: 'preserve'
// ^keep the old + overwrite to the new one
// ^'merge' if you wanna merge old + new
}
);
}

Redirect & wildcat routes

For example: building 404 page.

const appRoutes: Routes = [
{ path: 'not-found', component: PageNoteFoundComponent },
// a specific path
{ path: 'something', redirectTo: '/not-found' }, // redirect /something to /not-found
// all the paths (performed after THE ABOVE <- order makes sense!!!!)
{ path: '**', redirectTo: '/not-found' }
]

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

// errors
{ path: '', redirectTo: '/somewhere-else' }

// fix
{ 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.

πŸ‘‰ Example file.

// app-routing.module.ts
// no need to re-declare components like already done in app.module.ts
const appRoutes: Routes = [...];

@NgModule({
imports: [
RouterModule.forRoot(appRoutes)
],
exports: [RouterModule]
})
export class AppRoutingModule { }
// app.module.ts
...
@NgModule({
...
imports: [
AppRoutingModule
]
...
})

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!

// auth-guard.service.ts
export class AuthGuard ....{
canActivate(
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

// Obersvable / Promise -> some tasks need auth from server/databse <- async
// boolean -> some tasks completely on client <- sync
}

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?

// app-routing.module.ts
{path: '...', canActivate: [AuthGuard], component: ..., children: ...} // apply also for children
// ^add this to the path you wanna apply auth

(video) Show the father route list, only protect child routes? β†’ use CanActivateChild

// auth-guard.service.ts
export class AuthGuard ....{
canActivate(
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {

canActivateChild(...){...}
}

// app-routing.module.ts
{path: '...', canActivateChild: [AuthGuard], component: ..., children: ...}

(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.

// can-deactivate-guard.service.ts
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

export class CanDeactivateGuard implements canDeactivate
<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate, ...): ... {
return component.canDeactivate();
}
}
// app-routing.module.ts
{path: ..., ..., canDeactivate: [CanDeactivateGuard]}

// app.module.ts
providers: [CanDeactivate]
// edit-server.component.ts
export class ... implements CanComponentDeactivate {
canDeactivate():... {
// check if there are some changes and return true/false
}
}

Parsing static data to route

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

// error-page.component.html
<h4></h4>
// error-page.component.ts

export class ErrorPageComponent implements OnInit {
errorMessage: string;

constructor(private route: ActivatedRoute) { }
// ^simply inject current route which load the component

ngOnInit() {
this.errorMessage = this.route.snapshot.data['message'];
// ^ there is "data" in app-routing.module.ts
// or subscribe to the changes
// (including the init snapshot)
this.route.data.subscribe(
(data: Data) => {
this.errorMessage = data['message'];
}
);
}
}
// app-routing.module.ts
{ 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:

// server-resolver.service.ts
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Server> | Promise<Server> | Server {
return this.serversService.getServer(+route.params['id']);
// ^this service here will rerendered whenever we
// rerender the route
}
// unlike the component itself, this is executed each time, so no need to set
// up an observable or something like that
// app-routing.module.ts
{ path:..., component:..., resolver: {server: ServerResolver} }
// ^choose a name you like
// but make sure "server" is set the same in
// server.component.ts_____________________
ngOnInit() { // |
this.route.data // |
.subscribe( // |
(data: Data) => { this.server = data['server'] }
);
}

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.

localhost:/users -> localhost:/#/users
// ^from this to end: ignored by webserver
// app-routing.module.ts
@NgModule({
imports: [
RouterModule.forRoot(appRoutes, {useHash: true})
]
})
export ...