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}
- 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!
- 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}
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!
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.- 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!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
👉 Project with recipes and shopping-list. (change from using EventEmitter to service) — videos
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}
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) -->
(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
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}
(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!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}
(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
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}
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' }
Store all routing tasks in a single file. → no need to stored in
app.module.ts
.👉 Example file.
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})
(video) Create
auth-guard.service.ts
(file) containing the service to control the authentication. → use CanActivate
← angular 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}
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!'} }
(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}
(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 ...