import { FormsModule } from '@angular/forms';
...
// FormsModule to the imports[] array in your app.module.ts
Two way binding directive
<input type="text" placeholder="name" [(ngModel)]="name" />
Interfaces
interfaces are like contracts you need to fulfill the contract/implement the interface
interface User {
username: string;
password: string;
confirmPassword?: string; // ? === optional prop
}
// Interfaces for functions
interface CanDrive {
accelerate(speed: number): void;
}
// whichever item implements the CanDrive interface
// must have accelerate fn!
const myCar:CanDrive = {
accelerate: function(speed: number) {
...
}
}
Generics
Types which can hold/use several types
let numberArray: Array<number>;
Using bootstrap
npm i --save bootstrap
// angular.json
....
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css", // pretty sure that there is a cleaner way to do this
"src/styles.css"
],
....
Creating component using the cli
ng g c <name of component>
DataBinding === Communication
- String interpolation
{{getStatus()}}
- Property binding
<button [disabled]="!allowNewServer" type="button" class="btn btn-primary">
Add New Server
</button>
export class ServerComponent {
private serverId: number = 11;
private serverStatus: string = 'online';
public allowNewServer: boolean = false;
constructor() {
setTimeout(() => (this.allowNewServer = true), 2000);
}
getServerId(): number {
return this.serverId;
}
getServerStatus(): string {
return this.serverStatus;
}
}
// another example
<p [innerText]="allowNewServer"></p>
- Event binding
<button
(click)="onAddNewServer()"
[disabled]="!allowNewServer"
type="button"
class="btn btn-primary"
>
Add New Server
</button>
...
export class ServerComponent {
private serverId: number = 11;
private serverStatus: string = 'online';
public allowNewServer: boolean = false;
public allowNewServerStatus: string = 'not allowed to add new servers';
constructor() {
setTimeout(() => (this.allowNewServer = true), 2000);
}
getServerId(): number {
return this.serverId;
}
getServerStatus(): string {
return this.serverStatus;
}
onAddNewServer(): void {
this.allowNewServerStatus = 'you can add new servers now';
}
}
Access to event data
<input
type="text"
name="serverName"
id="serverName"
class="form-control"
value=""
required="required"
pattern=""
title=""
(input)="onUpdateServerName($event)"
/>
...
onUpdateServerName(event: Event) {
this.serverName = (<HTMLInputElement>event.target).value;
}
// explicit typeCasting for TS
- Two-Way-Binding
you need to enable ngModel directive you need to add the FormsModule to the imports[] array in the AppModule
<input
type="text"
name="serverName"
id="serverName"
class="form-control"
[(ngModel)]="serverName"
/>
....
// class member
public serverName: string = 'Test Server';
https://www.npmjs.com/package/esm
Directives
Instructions in the DOM
Structural Directives
<div *ngIf="isUsernameSet()">
<div *ngIf="isUsernameSet(); else noUser">
<strong>User: {{ userName }}</strong>
<hr />
<button
(click)="onResetUsername()"
[disabled]="!isUsernameSet()"
type="button"
class="btn btn-primary"
>
Reset name
</button>
</div>
<ng-template #noUser>
<p>No user!</p>
</ng-template>
Attribute Directives
<strong [ngStyle]="{ backgroundColor: checkIfUserMatches() }"
<strong
[ngClass]="{ match: userName === 'ivan' }" // match here is the css class
[ngStyle]="{ backgroundColor: checkIfUserMatches() }"
>User: {{ userName }}</strong
>
<app-server *ngFor="let server of servers"></app-server>
...
export class ServersComponent implements OnInit {
servers: Array<string> = ['s1', 's2', 's3'];
constructor() {}
ngOnInit() {}
}
Geting the index - *ngFor
<p [ngClass]="{ aboveFour: i >= 4 }" *ngFor="let item of clicks; let i = index">
logged at: {{ item }}
</p>
model === data
Create a recipe model
// recipe.model.ts
export class Recipe {
public name: string;
public description: string;
public imagePath: string;
constructor(name: string, desc: string, imagePath: string) {
this.name = name;
this.description = desc;
this.imagePath = imagePath;
}
}
// use the model
import { Component, OnInit } from '@angular/core';
import { Recipe } from '../recipe.model';
@Component({
selector: 'app-recipe-list',
templateUrl: './recipe-list.component.html',
styleUrls: ['./recipe-list.component.css']
})
export class RecipeListComponent implements OnInit {
recipes: Recipe[] = [
new Recipe(
'A test',
'This is a simple test',
'https://www.maxpixel.net/static/photo/1x/Burgers-Burger-Chickpeas-Recipes-Food-Vegetables-2920072.jpg'
)
];
constructor() {}
ngOnInit() {}
}
// use the recipes
<a *ngFor="let recipe of recipes" href="#" class="list-group-item clearfix">
<div class="pull-left">
<h4 class="list-group-item-heading">{{ recipe.name }}</h4>
<p class="list-group-item-text">{{ recipe.description }}</p>
</div>
<span class="pull-right">
<img
src="{{ recipe.imagePath }}"
alt=""
class="img-responsive"
style="max-height: 50px;"
/>
</span>
</a>
Create the ingredient model using TS accessors
export class Ingredient {
constructor(public name: string, public amount: number) {}
}
// TS will set the "this props accordingly"
create and nest a component
ng g c recipes/recipe-list
// recipe-list will be nested inside recipes
Debugging
F12 -> Sources -> webpack -> . -> ....
Data binding - custom props binding
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit {
// expose this prop to parent components
@Input() element: { type: string; name: string; content: string };
constructor() {}
ngOnInit() {}
}
// app.component.html
<div class="container">
<app-cockpit></app-cockpit>
<hr />
<div class="row">
<div class="col-xs-12">
<app-server-element
*ngFor="let serverElement of serverElements"
[element]="serverElement"
></app-server-element>
</div>
</div>
</div>
Using an alias for component's class member
...
export class ServerElementComponent implements OnInit {
// expose this prop to parent components
@Input('srvElement') element: { type: string; name: string; content: string };
constructor() {}
ngOnInit() {}
}
<app-server-element
*ngFor="let serverElement of serverElements"
[srvElement]="serverElement"
></app-server-element>
Data binding - custom events binding
import { Component, OnInit, EventEmitter, Output } from "@angular/core";
@Component({
selector: "app-cockpit",
templateUrl: "./cockpit.component.html",
styleUrls: ["./cockpit.component.css"]
})
export class CockpitComponent implements OnInit {
// events that can be emitted from this component
// @Output() - parent components can listen for
// those custom events
@Output() serverCreated = new EventEmitter<{
serverName: string;
serverContent: string;
}>();
// <{ serverName: string; serverContent: string }>
// generic type indicating what data will be emitted
@Output() blueprintCreated = new EventEmitter<{
serverName: string;
serverContent: string;
}>();
newServerName = "";
newServerContent = "";
constructor() {}
ngOnInit() {}
onAddServer() {
this.serverCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
});
}
onAddBlueprint() {
this.blueprintCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
});
}
}
// cockpit html
...
<button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
<button class="btn btn-primary" (click)="onAddBlueprint()">
// parent component html
<app-cockpit
(serverCreated)="onServerAdded($event)"
(blueprintCreated)="onBlueprintAdded($event)"
></app-cockpit>
// parent component
export class AppComponent {
serverElements = [
{ type: 'server', name: 'testServer', content: 'test content' }
];
onServerAdded(serverData: { serverName: string; serverContent: string }) {
this.serverElements.push({
type: 'server',
name: serverData.serverName,
content: serverData.serverContent
});
}
onBlueprintAdded(blueprintData: {
serverName: string;
serverContent: string;
}) {
this.serverElements.push({
type: 'blueprint',
name: blueprintData.serverName,
content: blueprintData.serverContent
});
}
}
Using alias for custom event names
@Output("bpCreated") blueprintCreated = new EventEmitter<{
serverName: string;
serverContent: string;
}>();
Summary - data binding/communication using custom props and events
@Input()
expose component's prop to parent components
@Output
component emits custom events so parent components can listen to those events
20.03
Local references in templates
...
<input type="text" class="form-control" #serverNameInput />
// serverNameInput holds the whole html element
// in order to use its value
serverNameInput.value
Local references used in a component
// template
<input type="text" class="form-control" #serverNameInput />
// component
import { Component, OnInit, EventEmitter, Output, ViewChild, ElementRef } from "@angular/core";
...
@ViewChild('serverNameInput') serverNameInput: ElementRef;
....
// accessing its value
this.serverNameInput.nativeElement.value;
Projecting content into components - ng-content
<app-server-element
*ngFor="let serverElement of serverElements"
[srvElement]="serverElement"
>
<p>This will be projected!</p>
</app-server-element>
<div class="panel panel-default">
<div class="panel-heading">{{ element.name }}</div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>
Lifecycle hooks
- constructor
- ngOnChanges
// make sure that OnChanges(or any other LF hook interfaces) is imported
// and the class is implementing is
// it could work without those steps, though
import { Component, OnInit, Input, OnChanges } from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css']
})
export class ServerElementComponent implements OnInit, OnChanges {
// expose this prop to parent components
@Input('srvElement') element: { type: string; name: string; content: string };
constructor() {
console.log('constructor called');
}
ngOnChanges(changes: SimpleChanges): void {
//Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here.
//Add '${implements OnChanges}' to the class.
console.log('ngOnChanges called');
console.log(changes);
}
ngOnInit() {
console.log('ngOnInit called');
}
}
- ngDoCheck // called many times
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy
@ContentChild decorator
// used inside component
// to get the element which is passed
// to <ng-content></ng-content>
// make sure to import
// ContentChild!
// and then use the decorator
@ContentChild('someRef') someRefName: ElementRef;
Creating custom attribute directive
simple example
import { Directive, OnInit, ElementRef } from '@angular/core';
@Directive({
selector: '[appBasicHighlight]'
})
export class BasicHighlightDirective implements OnInit {
constructor(private elementRef: ElementRef) {}
ngOnInit() {
this.elementRef.nativeElement.style.backgroundColor = 'green';
}
}
you need to inform Angular for it
// app.module.ts
@NgModule({
declarations: [
BasicHighlightDirective
how to use it
<p appBasicHighlight>Testing directives</p>
better example
import { Directive, Renderer2, ElementRef, OnInit } from '@angular/core';
@Directive({
selector: '[appBetterHighlight]'
})
export class BetterHighlightDirective implements OnInit {
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
this.renderer.setStyle(
this.elRef.nativeElement,
'background-color',
'blue'
);
}
}
another enhancement
export class BetterHighlightDirective implements OnInit {
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
// this.renderer.setStyle(
// this.elRef.nativeElement,
// 'background-color',
// 'blue'
// );
}
@HostListener('mouseenter') mouseover(eventData: Event) {
this.renderer.setStyle(
this.elRef.nativeElement,
'background-color',
'blue'
);
}
@HostListener('mouseleave') mouseleave(eventData: Event) {
this.renderer.setStyle(
this.elRef.nativeElement,
'background-color',
'transparent'
);
}
}
yet another enhancement
export class BetterHighlightDirective implements OnInit {
@Input() defaultColor: string = 'transparent';
@Input() highlightColor: string = 'blue';
@HostBinding('style.backgroundColor') backgroundColor: string = this
.defaultColor;
constructor(private elRef: ElementRef, private renderer: Renderer2) {}
ngOnInit() {
this.backgroundColor = this.defaultColor;
}
@HostListener('mouseenter') mouseover(eventData: Event) {
this.backgroundColor = this.highlightColor;
}
@HostListener('mouseleave') mouseleave(eventData: Event) {
this.backgroundColor = this.defaultColor;
}
}
how to use it
<p appBetterHighlight [defaultColor]="'pink'" [highlightColor]="'lime'">
Testing directives
</p>
Creating custom structural directive
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appUnless]'
})
export class UnlessDirective {
@Input() set appUnless(condition: boolean) {
if (!condition) {
this.vcRef.createEmbeddedView(this.templateRef);
} else {
this.vcRef.clear();
}
}
constructor(
private templateRef: TemplateRef<any>,
private vcRef: ViewContainerRef
) {}
}
how to use
<p *appUnless="'mike' === 'smith'">
default user
</p>
ngSwitch
<div [ngSwitch]="valueToCheck">
<p *ngSwitchCase="true">
The value is true
</p>
<p *ngSwitchCase="false">
The value is false
</p>
<p *ngSwitchDefault>
The value is default
</p>
</div>
export class AppComponent {
loadedFeature: string = 'recipe';
valueToCheck: boolean;
onFeatureSelected(feature: string) {
this.loadedFeature = feature;
}
}
// it will default in this case
Toggle class on an element using custom directive
import { Directive, HostListener, HostBinding } from '@angular/core';
@Directive({
selector: '[appDropdown]'
})
export class DropdownDirective {
@HostBinding('class.open') isOpen = false;
@HostListener('click') toggleOpen() {
this.isOpen = !this.isOpen;
}
}
Service
- It's a class which acts as a centralized business unit
- It eases the communication between components as well
Creation of a logging service
// logging.service.ts
export class LoggingService {
logStatusChange(status: string) {
console.log(`Status changed, new status: ${status}`);
}
}
how to use it in a component:
import { Component, EventEmitter, Output } from "@angular/core";
import { LoggingService } from "../shared/services/logging.service"; // !
@Component({
selector: "app-new-account",
templateUrl: "./new-account.component.html",
styleUrls: ["./new-account.component.css"],
providers: [LoggingService] // !
})
export class NewAccountComponent {
@Output() accountAdded = new EventEmitter<{ name: string; status: string }>();
constructor(private loggingService: LoggingService) {} // !
onCreateAccount(accountName: string, accountStatus: string) {
this.accountAdded.emit({
name: accountName,
status: accountStatus
});
this.loggingService.logStatusChange(accountStatus);
}
}
Creating the AccountsService
export class AccountsService {
accounts = [
{
name: 'Master Account',
status: 'active'
},
{
name: 'Testaccount',
status: 'inactive'
},
{
name: 'Hidden Account',
status: 'unknown'
}
];
addAccount(name: string, status: string) {
this.accounts.push({ name, status });
}
updateStatus(id: number, status: string) {
this.accounts[id].status = status;
}
}
How to use the services properly - the injector
It depends where this service is provided:
- AppModule: the same instance of the service is available in the whole app
- an alternative for when using Angular 6+:
@Injectable({providedIn: 'root'}) export class MyService { ... }
- AppComponent: the same instance of the service in all components below the AppComponent(not for the other Services)
- other components: the same instance is available for the component and all its child components
You can overwrite the service instance depending on where it is provided! If you need to have one instance you need to provided on the highest level possible. On ther other hand you can have as many instances based on your intent.
Single instance scenario:
- Do not include the service in the providers array, only in the constructor!
Injecting services into services
Scenario:
Use the loggingService in the accountsService
// app.module.ts
.......
@NgModule({
declarations: [AppComponent, AccountComponent, NewAccountComponent],
imports: [BrowserModule, FormsModule],
providers: [AccountsService, LoggingService],
bootstrap: [AppComponent]
})
.....
now you need to inject the service into the other service
// service in which you are injecting another service
import { LoggingService } from './services/logging.service';
import { Injectable } from '@angular/core';
@Injectable()
export class AccountsService {
accounts = [
{
name: 'Master Account',
status: 'active'
},
{
name: 'Testaccount',
status: 'inactive'
},
{
name: 'Hidden Account',
status: 'unknown'
}
];
constructor(private loggingService: LoggingService) {}
addAccount(name: string, status: string) {
this.accounts.push({ name, status });
this.loggingService.logStatusChange(status);
}
updateStatus(id: number, status: string) {
this.accounts[id].status = status;
this.loggingService.logStatusChange(status);
}
}
Emmitting events from a service and subscribe
export class AccountsService {
accounts = [
{
name: 'Master Account',
status: 'active'
},
{
name: 'Testaccount',
status: 'inactive'
},
{
name: 'Hidden Account',
status: 'unknown'
}
];
statusUpdated = new EventEmitter<string>(); // !
constructor(private loggingService: LoggingService) {}
addAccount(name: string, status: string) {
this.accounts.push({ name, status });
this.loggingService.logStatusChange(status);
}
updateStatus(id: number, status: string) {
this.accounts[id].status = status;
this.loggingService.logStatusChange(status);
}
}
export class AccountComponent {
@Input() account: { name: string; status: string };
@Input() id: number;
constructor(private accoutsService: AccountsService) {}
onSetTo(status: string) {
this.accoutsService.updateStatus(this.id, status);
this.accoutsService.statusUpdated.emit(status); // !
}
}
export class NewAccountComponent {
constructor(private accountsService: AccountsService) {
this.accountsService.statusUpdated.subscribe((status: string) =>
alert('new status: ' + status) // !
);
}
onCreateAccount(accountName: string, accountStatus: string) {
this.accountsService.addAccount(accountName, accountStatus);
}
}
Emmitting events from a service and subscribe - An Example
// create the emitter, also emit custom events
export class ShoppingListService {
ingredientsChanged = new EventEmitter<Ingredient[]>();
private ingredients: Ingredient[] = [
new Ingredient('Apples', 11),
new Ingredient('Tomatoes', 4)
];
getIngredients() {
return [...this.ingredients];
}
addIngredient(ingredient: Ingredient) {
this.ingredients.push(ingredient);
this.ingredientsChanged.emit([...this.ingredients]);
}
addIngredients(ingredients: Ingredient[]) {
this.ingredients.push(...ingredients);
this.ingredientsChanged.emit([...this.ingredients]);
}
}
// subscribe
export class ShoppingListComponent implements OnInit {
ingredients: Ingredient[] = [];
constructor(private shoppingListService: ShoppingListService) {}
ngOnInit() {
this.ingredients = this.shoppingListService.getIngredients();
this.shoppingListService.ingredientsChanged.subscribe(
(ingredients: Ingredient[]) => {
this.ingredients = ingredients;
}
);
}
}
Routing
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { ServersComponent } from './servers/servers.component';
import { UserComponent } from './users/user/user.component';
import { EditServerComponent } from './servers/edit-server/edit-server.component';
import { ServerComponent } from './servers/server/server.component';
import { ServersService } from './servers/servers.service';
import { Routes, RouterModule } from '@angular/router'; // !
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UserComponent },
{ path: 'servers', component: ServerComponent }
]; // !
@NgModule({
declarations: [
AppComponent,
HomeComponent,
UsersComponent,
ServersComponent,
UserComponent,
EditServerComponent,
ServerComponent
],
imports: [BrowserModule, FormsModule, RouterModule.forRoot(appRoutes)], // !
providers: [ServersService],
bootstrap: [AppComponent]
})
export class AppModule {}
// app.component.html
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a href="#">Home</a></li>
<li role="presentation"><a href="#">Servers</a></li>
<li role="presentation"><a href="#">Users</a></li>
</ul>
</div>
</div>
<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>
</div>
Navigation with router links
you can pass string or an array
<ul class="nav nav-tabs">
<li role="presentation" class="active"><a routerLink="/">Home</a></li>
<li role="presentation"><a routerLink="/servers">Servers</a></li>
<li role="presentation"><a [routerLink]="['/users']">Users</a></li>
</ul>
Adding active class on current page
<ul class="nav nav-tabs">
<li
role="presentation"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
<a routerLink="/">Home</a>
</li>
<li role="presentation" routerLinkActive="active">
<a routerLink="/servers">Servers</a>
</li>
<li role="presentation" routerLinkActive="active">
<a [routerLink]="['/users']">Users</a>
</li>
</ul>
Navigate programmatically
<button type="button" (click)="onLoadServers()" class="btn btn-primary">
Load Servers
</button>
onLoadServers() {
// some calculations
this.router.navigate(['/servers']);
}
Navigate programmatically - Using relative paths
import { Component, OnInit } from '@angular/core';
import { ServersService } from './servers.service';
import { Router, ActivatedRoute } from '@angular/router'; // !
@Component({
selector: 'app-servers',
templateUrl: './servers.component.html',
styleUrls: ['./servers.component.css']
})
export class ServersComponent implements OnInit {
private servers: { id: number; name: string; status: string }[] = [];
constructor(
private serversService: ServersService,
private router: Router, // !
private route: ActivatedRoute // !
) {}
ngOnInit() {
this.servers = this.serversService.getServers();
}
onReload() {
// !
this.router.navigate(['servers'], { relativeTo: this.route });
// it will try to navigate to /servers/servers
}
}
Passing params to routes
// app.module.ts
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id', component: UserComponent },
{ path: 'servers', component: ServersComponent }
];
Fetching route params
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id/:name', component: UserComponent },
{ path: 'servers', component: ServersComponent }
];
<button
type="button"
[routerLink]="['/users', 10, 'Anna']"
class="btn btn-primary"
>
Load Anna
</button>
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
user: { id: number; name: string };
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.user = {
id: this.route.snapshot.params['id'],
name: this.route.snapshot.params['name']
};
// observable
// this is useful if
// we are on the component itself and
// try to use the new params
this.route.params.subscribe((params: Params) => {
this.user.id = params['id'];
this.user.name = params['name'];
});
}
}
Passing query params and fragments
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id/:name', component: UserComponent },
{ path: 'servers', component: ServersComponent },
{ path: 'servers/:id/edit', component: EditServerComponent }
];
// /servers/3/edit?allowEdit=1#loading
<a
[routerLink]="['/servers', 3, 'edit']"
[queryParams]="{ allowEdit: 1 }"
fragment="loading"
class="list-group-item"
*ngFor="let server of servers"
>
{{ server.name }}
</a>
the same idea when inside a component
<button type="button" (click)="onLoadServer(1)" class="btn btn-primary">
Load Server 1
</button>
export class HomeComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {}
onLoadServer(id: number) {
// some calculations
this.router.navigate(['/servers', id, 'edit'], {
queryParams: { allowEdit: 1 },
fragment: 'loading'
});
}
}
Retrieving query params and fragments
export class EditServerComponent implements OnInit {
server: { id: number; name: string; status: string };
serverName = '';
serverStatus = '';
constructor(
private serversService: ServersService,
private route: ActivatedRoute
) {}
ngOnInit() {
console.log(this.route.snapshot.queryParams);
console.log(this.route.snapshot.fragment);
// in order to get the updated values
// if on the same page
// you need to use the reactive way
this.route.queryParams.subscribe(params => console.log(params));
this.route.fragment.subscribe(params => console.log(params));
this.server = this.serversService.getServer(1);
this.serverName = this.server.name;
this.serverStatus = this.server.status;
}
onUpdateServer() {
this.serversService.updateServer(this.server.id, {
name: this.serverName,
status: this.serverStatus
});
}
}
setup - nested routes
// before
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'users', component: UsersComponent },
{ path: 'users/:id/:name', component: UserComponent },
{ path: 'servers', component: ServersComponent },
{ path: 'servers/:id', component: EditServerComponent },
{ path: 'servers/:id/edit', component: EditServerComponent }
];
// after
const appRoutes: Routes = [
{ path: "", component: HomeComponent },
{
path: "users",
component: UsersComponent,
children: [{ path: ":id/:name", component: UserComponent }]
},
{
path: "servers",
component: ServersComponent,
children: [
{ path: ":id", component: ServerComponent },
{ path: ":id/edit", component: EditServerComponent }
]
}
];
you need to insert
<router-outlet></router-outlet>
like here:
<div class="row">
<div class="col-xs-12 col-sm-4">
<div class="list-group">
<a
[routerLink]="['/users', user.id, user.name]"
class="list-group-item"
*ngFor="let user of users"
>
{{ user.name }}
</a>
</div>
</div>
<div class="col-xs-12 col-sm-4">
<!-- <app-user></app-user> -->
<router-outlet></router-outlet>
</div>
</div>
Remember to subscribe if you are on the same page and want to retrive the updated params!
ngOnInit() {
const serverId = +this.route.snapshot.params["id"];
this.server = this.serversService.getServer(serverId);
// !
this.route.params.subscribe(params => {
this.server = this.serversService.getServer(+params["id"]);
});
}
Navigate to relative path
scenario:
you are on a certain page /servers/:id and want to navigate to /servers/:id/edit
export class ServerComponent implements OnInit {
server: { id: number; name: string; status: string };
constructor(
private serversService: ServersService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
const serverId = +this.route.snapshot.params["id"];
this.server = this.serversService.getServer(serverId);
this.route.params.subscribe(params => {
this.server = this.serversService.getServer(+params["id"]);
});
}
// !
onEditServer() {
this.router.navigate(["edit"], { relativeTo: this.route });
}
}
Navigate to relative path and keep the query params
export class ServerComponent implements OnInit {
server: { id: number; name: string; status: string };
constructor(
private serversService: ServersService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
const serverId = +this.route.snapshot.params["id"];
this.server = this.serversService.getServer(serverId);
this.route.params.subscribe(params => {
this.server = this.serversService.getServer(+params["id"]);
});
}
onEditServer() {
this.router.navigate(["edit"], {
relativeTo: this.route,
queryParamsHandling: "preserve"
});
}
}
Redirect when user navigates to unknown page
const appRoutes: Routes = [
{ path: "", component: HomeComponent },
{
path: "users",
component: UsersComponent,
children: [{ path: ":id/:name", component: UserComponent }]
},
{
path: "servers",
component: ServersComponent,
children: [
{ path: ":id", component: ServerComponent },
{ path: ":id/edit", component: EditServerComponent }
]
},
{ path: "not-found", component: PageNotFoundComponent },
{ path: "**", redirectTo: "/not-found" }
];
Redirection Path Matching
you only get redirected, if the full path is '' (so only if you got NO other content in your path in this example)
{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' }
Creating new module to hold the routing logic
// app-routing.module.ts
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { UsersComponent } from "./users/users.component";
import { UserComponent } from "./users/user/user.component";
import { ServersComponent } from "./servers/servers.component";
import { ServerComponent } from "./servers/server/server.component";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
const appRoutes: Routes = [
{ path: "", component: HomeComponent },
{
path: "users",
component: UsersComponent,
children: [{ path: ":id/:name", component: UserComponent }]
},
{
path: "servers",
component: ServersComponent,
children: [
{ path: ":id", component: ServerComponent },
{ path: ":id/edit", component: EditServerComponent }
]
},
{ path: "not-found", component: PageNotFoundComponent },
{ path: "**", redirectTo: "/not-found" }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
in order to use it in your main module:
...
import { AppRoutingModule } from "./app-routing.module";
@NgModule({
declarations: [
AppComponent,
HomeComponent,
UsersComponent,
ServersComponent,
UserComponent,
EditServerComponent,
ServerComponent,
PageNotFoundComponent
],
imports: [BrowserModule, FormsModule, AppRoutingModule],
providers: [ServersService],
bootstrap: [AppComponent]
})
...
Protecting routes using Guards
// auth-guard.service.ts
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from '@angular/router';
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.isAuthenticated().then((authenticated: boolean) => {
if (authenticated) {
return true;
} else {
this.router.navigate(['/']);
}
});
}
}
// auth.service.ts
export class AuthService {
loggedIn = false;
isAuthenticated() {
const promise = new Promise(resolve => {
setTimeout(() => {
resolve(this.loggedIn);
}, 500);
});
return promise;
}
login() {
this.loggedIn = true;
}
logout() {
this.loggedIn = false;
}
}
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { UsersComponent } from './users/users.component';
import { UserComponent } from './users/user/user.component';
import { ServersComponent } from './servers/servers.component';
import { ServerComponent } from './servers/server/server.component';
import { EditServerComponent } from './servers/edit-server/edit-server.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AuthGuard } from './auth-guard.service';
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UsersComponent,
children: [{ path: ':id/:name', component: UserComponent }]
},
{
path: 'servers',
component: ServersComponent,
canActivate: [AuthGuard], // ! it will guard the whole server path with child paths too
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent }
]
},
{ path: 'not-found', component: PageNotFoundComponent },
{ path: '**', redirectTo: '/not-found' }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
Protecting child routes
// app-routing.module.ts
...
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UsersComponent,
children: [{ path: ':id/:name', component: UserComponent }]
},
{
path: 'servers',
component: ServersComponent,
canActivateChild: [AuthGuard],
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent }
]
},
{ path: 'not-found', component: PageNotFoundComponent },
{ path: '**', redirectTo: '/not-found' }
];
...
// auth-guard.service.ts
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.authService.isAuthenticated().then((authenticated: boolean) => {
if (authenticated) {
return true;
} else {
this.router.navigate(['/']);
}
});
}
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.canActivate(route, state);
}
}
Navigate one level up
this.router.navigate(['../'], { relativeTo: this.route });
Creating custom interface
import { Observable } from 'rxjs';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
// simply put
// the class which implements this interface must
// have a canDeactivate method which returns one of the following:
// - Observable<boolean>
// - Promise<boolean>
// - boolean
Controlling navigation with canDeactivate
// can-deactivate-guard.service.ts
import { Observable } from 'rxjs';
import {
CanDeactivate,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
export class CanDeactivateGuard
implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(
component: CanComponentDeactivate,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return component.canDeactivate();
}
}
// app-routing.module.ts
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UsersComponent,
children: [{ path: ':id/:name', component: UserComponent }]
},
{
path: 'servers',
component: ServersComponent,
canActivateChild: [AuthGuard],
children: [
{ path: ':id', component: ServerComponent },
{
path: ':id/edit',
component: EditServerComponent,
canDeactivate: [CanDeactivateGuard] // !
}
]
},
{ path: 'not-found', component: PageNotFoundComponent },
{ path: '**', redirectTo: '/not-found' }
];
// app.module.ts
@NgModule({
declarations: [
AppComponent,
HomeComponent,
UsersComponent,
ServersComponent,
UserComponent,
EditServerComponent,
ServerComponent,
PageNotFoundComponent
],
imports: [BrowserModule, FormsModule, AppRoutingModule],
providers: [ServersService, AuthService, AuthGuard, CanDeactivateGuard], // !
bootstrap: [AppComponent]
})
// edit-server.component.ts
import { Component, OnInit } from '@angular/core';
import { ServersService } from '../servers.service';
import { ActivatedRoute, Params, Router } from '@angular/router';
import { CanComponentDeactivate } from './can-deactivate-guard.service';
import { Observable } from 'rxjs';
@Component({
selector: 'app-edit-server',
templateUrl: './edit-server.component.html',
styleUrls: ['./edit-server.component.css']
})
export class EditServerComponent implements OnInit, CanComponentDeactivate {
server: { id: number; name: string; status: string };
serverName = '';
serverStatus = '';
allowEdit = false;
changesSaved = false;
constructor(
private serversService: ServersService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
// console.log(this.route.snapshot.queryParams);
// console.log(this.route.snapshot.fragment);
// in order to get the updated values
// if on the same page
// you need to use the reactive way
this.route.queryParams.subscribe((params: Params) => {
this.allowEdit = params['allowEdit'] === '1' ? true : false;
});
// this.route.fragment.subscribe(params => console.log(params));
this.server = this.serversService.getServer(1);
this.serverName = this.server.name;
this.serverStatus = this.server.status;
}
onUpdateServer() {
this.serversService.updateServer(this.server.id, {
name: this.serverName,
status: this.serverStatus
});
this.changesSaved = true;
this.router.navigate(['../'], { relativeTo: this.route });
}
// !
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
if (!this.allowEdit) {
return true;
}
if (
(this.serverName !== this.server.name ||
this.serverStatus !== this.server.status) &&
!this.changesSaved
) {
return confirm('Do you want to discard the changes?');
} else {
return true;
}
}
}
Passing static data to a route
// component which will receive the static data
export class ErrorPageComponent implements OnInit {
errorMessage: string;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.errorMessage = this.route.snapshot.data['message'];
// you can subscribe as well
}
}
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UsersComponent,
children: [{ path: ':id/:name', component: UserComponent }]
},
{
path: 'servers',
component: ServersComponent,
canActivateChild: [AuthGuard],
children: [
{ path: ':id', component: ServerComponent },
{
path: ':id/edit',
component: EditServerComponent,
canDeactivate: [CanDeactivateGuard]
}
]
},
// { path: 'not-found', component: PageNotFoundComponent },
{
path: 'not-found', // !
component: ErrorPageComponent,
data: { message: 'Page not found v2' }
},
{ path: '**', redirectTo: '/not-found' }
];
Observable
interface Server {
id: number;
name: string;
status: string;
}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<Server>;
The Observable must return an object which implements the Server interface
Passing dynamic data to routes
// server-resolver.service.ts
import {
Resolve,
ActivatedRouteSnapshot,
RouterStateSnapshot
} from '@angular/router';
import { Observable } from 'rxjs';
import { ServersService } from '../servers.service';
import { Injectable } from '@angular/core';
// create an interface
interface Server {
id: number;
name: string;
status: string;
}
// it will return data in advance
@Injectable()
export class ServerResolver implements Resolve<Server> {
constructor(private serverService: ServersService) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<Server> | Promise<Server> | Server {
return this.serverService.getServer(+route.params['id']);
}
}
// app.module.ts
@NgModule({
declarations: [
AppComponent,
HomeComponent,
UsersComponent,
ServersComponent,
UserComponent,
EditServerComponent,
ServerComponent,
PageNotFoundComponent,
ErrorPageComponent
],
imports: [BrowserModule, FormsModule, AppRoutingModule],
providers: [
ServersService,
AuthService,
AuthGuard,
CanDeactivateGuard,
ServerResolver // !
],
bootstrap: [AppComponent]
})
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'users',
component: UsersComponent,
children: [{ path: ':id/:name', component: UserComponent }]
},
{
path: 'servers',
component: ServersComponent,
canActivateChild: [AuthGuard],
children: [
{
path: ':id',
component: ServerComponent,
resolve: { server: ServerResolver } // !
},
{
path: ':id/edit',
component: EditServerComponent,
canDeactivate: [CanDeactivateGuard]
}
]
},
// { path: 'not-found', component: PageNotFoundComponent },
{
path: 'not-found',
component: ErrorPageComponent,
data: { message: 'Page not found v2' }
},
{ path: '**', redirectTo: '/not-found' }
];
export class ServerComponent implements OnInit {
server: { id: number; name: string; status: string };
constructor(
private serversService: ServersService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit() {
// the resolver way
this.route.data.subscribe((data: Data) => {
this.server = data['server']; // !
});
// the param way
// const serverId = +this.route.snapshot.params['id'];
// this.server = this.serversService.getServer(serverId);
// this.route.params.subscribe(params => {
// this.server = this.serversService.getServer(+params['id']);
// });
}
onEditServer() {
this.router.navigate(['edit'], {
relativeTo: this.route,
queryParamsHandling: 'preserve'
});
}
}
Observables
Observables - various data sources:
- events
- http requests
In Angular, the Observable is an object imported from rxjs
Observable emits Observers subscribes
The Observable wraps some data and emits data
Creating an observable which emits new ascending number each second
ngOnInit() {
const myNumbers = Observable.interval(1000);
myNumbers.subscribe((number: number) => {
console.log(number);
});
}
Creating an observable which sends two packages and errors out
const myObs = Observable.create((observer: Observable<string>) => {
setTimeout(() => {
observer.next('first package');
}, 2000);
setTimeout(() => {
observer.next('second package');
}, 4000);
setTimeout(() => {
observer.error('not working');
}, 5000);
});
myObs.subscribe(
// handle data
(data: string) => {
console.log(data);
},
// handle error
(error: string) => {
console.log(error);
},
// handle complete
// this will not fire at all
() => {
console.log('completed');
}
);
// same thing may happen if you have complete method and
// then try to next some data
Unsubscribe on ngOnDestroy
export class HomeComponent implements OnInit, OnDestroy {
numbersObsSubscription: Subscription;
constructor() {}
ngOnInit() {
const myNumbers = Observable.interval(1000);
this.numbersObsSubscription = myNumbers.subscribe((number: number) => {
console.log(number);
});
}
ngOnDestroy() {
this.numbersObsSubscription.unsubscribe();
}
}
support older imports for rxjs
npm i --save rxjs-compat
Subject
Facilitate the communication between components
// users.service.ts
import { Subject } from 'rxjs/Subject';
export class UsersService {
userActivated = new Subject();
}
// acts as an observer and observable
// user.component.ts
export class UserComponent implements OnInit {
id: number;
constructor(private route: ActivatedRoute, private usersService: UsersService) { }
ngOnInit() {
this.route.params
.subscribe(
(params: Params) => {
this.id = +params['id'];
}
);
}
onActivate() {
this.usersService.userActivated.next(this.id); // !
}
}
export class AppComponent implements OnInit {
user1Activated = false;
user2Activated = false;
constructor(private usersService: UsersService) {}
ngOnInit() { // !
this.usersService.userActivated.subscribe(
(id: number) => {
if (id === 1) {
this.user1Activated = true;
} else if (id === 2) {
this.user2Activated = true;
}
}
);
}
}
<a routerLink="/">Home</a>
<a [routerLink]="['user', 1]">User 1 {{ user1Activated ? '(activated)' : '' }}</a>
<a [routerLink]="['user', 2]">User 2 {{ user2Activated ? '(activated)' : '' }}</a>
Operators
Used for performing additional methods on the data before returning it
const myNumbers = Observable.interval(1000)
.map( // !
(data: number) => {
return data * 2;
}
);
Update - Operators
const myNumbers = interval(1000)
.pipe(map(
(data: number) => {
return data * 2;
}
));
Forms
Two approaches:
- template driven
- reactive approach
Template driven
<form (ngSubmit)="onSubmit(f)" #f="ngForm">
<div id="user-data">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
class="form-control"
ngModel
/>
</div>
<button class="btn btn-default" type="button">
Suggest an Username
</button>
<div class="form-group">
<label for="email">Mail</label>
<input
ngModel
type="email"
name="email"
id="email"
class="form-control"
/>
</div>
</div>
<div class="form-group">
<label for="secret">Secret Questions</label>
<select ngModel name="secret" id="secret" class="form-control">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
</div>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
export class AppComponent {
suggestUserName() {
const suggestedName = "Superuser";
}
onSubmit(form: NgForm) {
console.log(form);
}
}
An alternative way of accessing the form using ViewChild
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
suggestUserName() {
const suggestedName = "Superuser";
}
// onSubmit(form: NgForm) {
// console.log(form);
// }
onSubmit() {
console.log(this.signupForm);
}
}
<form (ngSubmit)="onSubmit()" #f="ngForm">
TD: Validation
Built-in Validators & Using HTML5 Validation Which Validators do ship with Angular?
Check out the Validators class: https://angular.io/api/forms/Validators - these are all built-in validators, though that are the methods which actually get executed (and which you later can add when using the reactive approach).
For the template-driven approach, you need the directives. You can find out their names, by searching for "validator" in the official docs: https://angular.io/api?type=directive - everything marked with "D" is a directive and can be added to your template.
Additionally, you might also want to enable HTML5 validation (by default, Angular disables it). You can do so by adding the ngNativeValidate to a control in your template.
<button [disabled]="!f.valid" class="btn btn-primary" type="submit">
Submit
</button>
input.ng-invalid.ng-touched {
border: 1px solid red;
}
<input
type="email"
id="email"
class="form-control"
ngModel
name="email"
required
email
#email="ngModel">
<span class="help-block" *ngIf="!email.valid && email.touched">Please enter a valid email!</span>
TD: default values
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
defaultQuestion = "pet";
<select
id="secret"
class="form-control"
[ngModel]="defaultQuestion"
name="secret"
>
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
TD: Using ngModel with Two-Way-Binding
<div class="form-group">
<textarea
name="questionAnswer"
rows="3"
class="form-control"
[(ngModel)]="answer"></textarea>
</div>
<p>Your reply: {{ answer }}</p>
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
defaultQuestion = "pet";
answer = "";
TD: Grouping Form Controls
// username and email will be grouped into userData property
<div id="user-data" ngModelGroup="userData"
#userData="ngModelGroup">
<div
class="form-group"
>
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
class="form-control"
ngModel
required
/>
</div>
<button class="btn btn-default" type="button">
Suggest an Username
</button>
<div class="form-group">
<label for="email">Mail</label>
<input
ngModel
type="email"
name="email"
id="email"
class="form-control"
required
email
#email="ngModel"
/>
<span class="help-block" *ngIf="!email.valid && email.touched"
>Please enter a valid email!</span
>
</div>
</div>
<p *ngIf="!userData.valid && userData.touched">User Data is invalid!</p>
TD: Handling Radio Buttons
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
defaultQuestion = "pet";
answer = "";
genders = ["male", "female"];
<div class="radio" *ngFor="let gender of genders">
<label>
<input
type="radio"
name="gender"
ngModel
[value]="gender"
required
/>
{{ gender }}
</label>
</div>
TD: Setting and Patching Form Values
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
defaultQuestion = "pet";
answer = "";
genders = ["male", "female"];
suggestUserName() {
const suggestedName = "Superuser";
// ! will overwrite all values
// this.signupForm.setValue({
// userData: {
// username: suggestedName,
// email: "[email protected]"
// },
// secret: "pet",
// questionAnswer: "",
// gender: "male"
// });
// patch only
this.signupForm.form.patchValue({
userData: {
username: suggestedName
}
});
}
<form (ngSubmit)="onSubmit()" #f="ngForm">
<div id="user-data">
<div
class="form-group"
ngModelGroup="userData"
#userData="ngModelGroup"
>
<label for="username">Username</label>
<input
type="text"
id="username"
name="username"
class="form-control"
ngModel
required
/>
</div>
<button
(click)="suggestUserName()"
class="btn btn-default"
type="button"
>
Suggest an Username
</button>
TD: Using Form Data
export class AppComponent {
@ViewChild("f") signupForm: NgForm;
defaultQuestion = "pet";
answer = "";
genders = ["male", "female"];
user = {
username: "",
email: "",
secretQuestion: "",
answer: "",
gender: ""
};
submitted = false;
..........
onSubmit() {
this.submitted = true;
this.user.username = this.signupForm.value.userData.username;
this.user.email = this.signupForm.value.userData.email;
this.user.secretQuestion = this.signupForm.value.secret;
this.user.answer = this.signupForm.value.questionAnswer;
this.user.gender = this.signupForm.value.gender;
}
<div class="row" *ngIf="submitted">
<div class="col-xs-12">
<h3>Your Data</h3>
<p>Username: {{ user.username }}</p>
<p>Mail: {{ user.email }}</p>
<p>Secret Question: Your first {{ user.secretQuestion }}</p>
<p>Answer: {{ user.answer }}</p>
<p>Gender: {{ user.gender }}</p>
</div>
</div>
Resetting the form
onSubmit() {
this.submitted = true;
this.user.username = this.signupForm.value.userData.username;
this.user.email = this.signupForm.value.userData.email;
this.user.secretQuestion = this.signupForm.value.secret;
this.user.answer = this.signupForm.value.questionAnswer;
this.user.gender = this.signupForm.value.gender;
// will reset the values and css classes
// optional: you can pass an object as in setValue method
this.signupForm.reset();
}
Reactive Forms
// app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, ReactiveFormsModule],
providers: [],
bootstrap: [AppComponent]
})
key features to watch for:
- grouping
- array of form controls
- creating custom validators
- using error codes to display different warnings
- Creating a Custom Async Validator
- Reacting to Status or Value Changes
- on the form or an individual input
- Setting and Patching Values
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<div formGroupName="userData">
<div class="form-group">
<label for="username">Username</label>
<input
type="text"
id="username"
formControlName="username"
class="form-control"
/>
<span
*ngIf="
!signupForm.get('userData.username').valid &&
signupForm.get('userData.username').touched
"
class="help-block"
>
<span
*ngIf="
signupForm.get('userData.username').errors['nameIsForbidden']
"
>This name is invalid!</span
>
<span
*ngIf="signupForm.get('userData.username').errors['required']"
>This field is required!</span
>
</span>
</div>
<div class="form-group">
<label for="email">email</label>
<input
type="text"
id="email"
formControlName="email"
class="form-control"
/>
<span
*ngIf="
!signupForm.get('userData.email').valid &&
signupForm.get('userData.email').touched
"
class="help-block"
>Please enter a valid email!</span
>
</div>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<input type="radio" formControlName="gender" [value]="gender" />{{
gender
}}
</label>
</div>
<div formArrayName="hobbies">
<h4>Your Hobbies</h4>
<button class="btn btn-default" type="button" (click)="onAddHobby()">
Add Hobby
</button>
<div
class="form-group"
*ngFor="
let hobbyControl of signupForm.get('hobbies').controls;
let i = index
"
>
<input type="text" class="form-control" [formControlName]="i" />
</div>
</div>
<span *ngIf="!signupForm.valid && signupForm.touched" class="help-block"
>Please enter valid data!</span
>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
export class AppComponent implements OnInit {
genders = ["male", "female"];
signupForm: FormGroup;
forbiddenUsernames = ["Chris", "Anna"];
constructor() {}
ngOnInit() {
this.signupForm = new FormGroup({
userData: new FormGroup({
username: new FormControl(null, [
Validators.required,
this.forbiddenNames.bind(this)
]),
email: new FormControl(
null,
[Validators.required, Validators.email],
this.forbiddenEmails
)
}),
gender: new FormControl("male"),
hobbies: new FormArray([])
});
// this.signupForm.valueChanges.subscribe(
// (value) => console.log(value)
// );
this.signupForm.statusChanges.subscribe(status => console.log(status));
this.signupForm.setValue({
userData: {
username: "Max",
email: "[email protected]"
},
gender: "male",
hobbies: []
});
this.signupForm.patchValue({
userData: {
username: "Anna"
}
});
}
onSubmit() {
console.log(this.signupForm);
this.signupForm.reset();
}
onAddHobby() {
const control = new FormControl(null, Validators.required);
(<FormArray>this.signupForm.get("hobbies")).push(control);
}
forbiddenNames(control: FormControl): { [s: string]: boolean } {
if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
return { nameIsForbidden: true };
}
return null;
}
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value === "[email protected]") {
resolve({ emailIsForbidden: true });
} else {
resolve(null);
}
}, 1500);
});
return promise;
}
}
Pipes
Pipes are used for transforming the Output
{{ server.instanceType | uppercase }}
{{ server.started | date }}
Configure a pipe
{{ server.started | date:'fullDate' }}
Where to find how to configure built-in pipes
Chaining pipes
{{ server.started | date:'fullDate' | uppercase }}
Creating a pipe
// shorten.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
transform(value: any, limit: number) {
if (value.length > limit) {
return value.substr(0, limit) + ' ...';
}
return value;
}
}
@NgModule({
declarations: [
AppComponent,
ShortenPipe,
FilterPipe
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
{{ server.name | shorten:15 }}
Creating a pipe with the cli:
ng g p filter
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter',
pure: false // ! may lead to performance issues
})
export class FilterPipe implements PipeTransform {
transform(value: any, filterString: string, propName: string): any {
if (value.length === 0 || filterString === '') {
return value;
}
const resultArray = [];
for (const item of value) {
if (item[propName] === filterString) {
resultArray.push(item);
}
}
return resultArray;
}
}
<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
<input type="text" [(ngModel)]="filteredStatus"> // !
<br>
<button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
<br><br>
<h2>App Status: {{ appStatus | async}}</h2>
<hr>
<ul class="list-group">
<li
class="list-group-item"
*ngFor="let server of servers | filter:filteredStatus:'status'" // !
[ngClass]="getStatusClasses(server)">
<span
class="badge">
{{ server.status }}
</span>
<strong>{{ server.name | shorten:15 }}</strong> |
{{ server.instanceType | uppercase }} |
{{ server.started | date:'fullDate' | uppercase }}
</li>
</ul>
</div>
Using the built-in async pipe
<h2>App Status: {{ appStatus | async}}</h2>
export class AppComponent {
appStatus = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('stable');
}, 2000);
});
HTTP requests - the old way
npm install --save @angular/http
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { HttpModule } from "@angular/http";
import { AppComponent } from "./app.component";
import { ServerService } from "./server.service";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpModule],
providers: [ServerService],
bootstrap: [AppComponent]
})
export class AppModule {}
import { Injectable } from "@angular/core";
import { Headers, Http, Response } from "@angular/http";
@Injectable()
export class ServerService {
constructor(private http: Http) {}
storeServers(servers: any[]) {
// no request is made yet
return this.http.post(
{theUrl},
servers
);
}
}
// app.component.ts
onSaveServers() {
this.serverService
.storeServers(this.servers)
.subscribe(data => console.log(data), error => console.log(error));
}
sending headers
// sending headers
export class ServerService {
constructor(private http: Http) {}
storeServers(servers: any[]) {
const headers = new Headers({ "Content-Type": "application/json" });
return this.http.post(
"{theUrl}",
servers,
{ headers }
);
}
}
GET request
import { Response } from "@angular/http";
onGetServers() {
this.serverService
.getServers()
.subscribe(
(data: Response) => console.log(data.json()),
error => console.log(error)
);
}
getServers() {
return this.http.get("{theUrl}");
}
HTTP requests - using HttpClient
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
@Injectable()
export class ServerService {
constructor(private httpClient: HttpClient) {}
storeServers(servers: any[]) {
return this.httpClient.post(
"{theUrl}",
servers
);
}
getServers() {
return this.httpClient.get(
"{theUrl}"
);
}
}
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { HttpClientModule } from "@angular/common/http";
import { AppComponent } from "./app.component";
import { ServerService } from "./server.service";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
providers: [ServerService],
bootstrap: [AppComponent]
})
export class AppModule {}
onGetServers() {
this.serverService
.getServers()
.subscribe(data => console.log(data), error => console.log(error));
}
Auth with Firebase
npm i --save firebase
init firebase
import { Component, OnInit } from "@angular/core";
import * as firebase from "firebase"; // !
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
title = "test-app";
ngOnInit() { // !
firebase.initializeApp({
apiKey: "{apiKey}",
authDomain: "{authDomain}"
});
}
}
create auth service
import { Injectable } from "@angular/core";
import * as firebase from "firebase";
@Injectable({
providedIn: "root"
})
export class AuthService {
constructor() {}
signupUser(email: string, password: string) {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.catch(error => console.log(error));
}
}
export class SignUpComponent implements OnInit {
@ViewChild("f") signupForm: NgForm;
constructor(private authService: AuthService) {}
ngOnInit() {}
onSubmit() {
const { email, password } = this.signupForm.value;
this.authService.signupUser(email, password);
}
}
Get logged in user's token
signinUser(email: string, password: string) {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(data => {
firebase.auth().currentUser.getIdToken()
.then((token: string) => {
this.token = token;
}).catch((err) => {
console.log(err);
});
})
.catch(error => console.log(error));
}
Creating Guard for checking logged in user
// auth-guard.service.ts
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router
} from "@angular/router";
import { Observable } from "rxjs";
import { Injectable } from "@angular/core";
import { AuthService } from "./auth/auth.service";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): boolean {
if (this.authService.isAuthenticated()) {
return true;
} else {
this.router.navigate(["/"]);
}
}
}
// auth.service.ts
import { Injectable } from '@angular/core';
import * as firebase from 'firebase';
@Injectable({
providedIn: 'root'
})
export class AuthService {
token: string = null;
constructor() {}
signupUser(email: string, password: string) {
firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.catch(error => console.log(error));
}
signinUser(email: string, password: string) {
firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(data => {
firebase
.auth()
.currentUser.getIdToken()
.then((token: string) => {
this.token = token;
})
.catch(err => {
console.log(err);
});
})
.catch(error => console.log(error));
}
isAuthenticated() {
return this.token !== null;
}
logout() {
firebase.auth().signOut();
this.token = null;
}
}
// app.module.ts
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
RecipesComponent,
ShoppingListComponent,
PageNotFoundComponent,
ProfileComponent,
SignUpComponent,
SignInComponent
],
imports: [BrowserModule, AppRoutingModule, FormsModule],
providers: [StaticAuthService, AuthGuard],
bootstrap: [AppComponent]
})
export class AppModule {}
import { NgModule } from "@angular/core";
import { Routes, RouterModule } from "@angular/router";
import { RecipesComponent } from "./recipes/recipes.component";
import { ShoppingListComponent } from "./shopping-list/shopping-list.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { ProfileComponent } from "./profile/profile.component";
import { AuthGuard } from "./auth-guard.service";
import { SignUpComponent } from "./auth/sign-up/sign-up.component";
import { SignInComponent } from "./auth/sign-in/sign-in.component";
const appRoutes: Routes = [
{ path: "", component: RecipesComponent },
{
path: "shopping-list",
component: ShoppingListComponent
},
{
path: "profile",
canActivate: [AuthGuard],
component: ProfileComponent
},
{
path: "sign-up",
component: SignUpComponent
},
{
path: "sign-in",
component: SignInComponent
},
{ path: "not-found", component: PageNotFoundComponent },
{ path: "**", redirectTo: "/not-found" }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
ngRx
the concept
- store - source of truth statewise
- reducers - functions which change the state
- dispatching an action will trigger reducers
npm i --save @ngrx/store
Material Design
npm install --save @angular/material @angular/cdk @angular/animations
// app.module.ts
// mat
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {
MatButtonModule,
MatCheckboxModule,
MatIconModule,
MatSidenavModule
} from '@angular/material';
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
RecipesComponent,
ShoppingListComponent,
PageNotFoundComponent,
ProfileComponent,
SignUpComponent,
SignInComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
BrowserAnimationsModule, // mat
MatButtonModule, // mat
MatCheckboxModule, // mat
MatIconModule, // mat
MatSidenavModule // mat
],
providers: [StaticAuthService, AuthGuard, DataStorageService],
bootstrap: [AppComponent]
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TestApp</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link
href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet"
/>
</head>
<body>
<app-root></app-root>
</body>
</html>
// styles.css
/* You can add global styles to this file, and also import other style files */
@import '~@angular/material/prebuilt-themes/indigo-pink.css';
Modules
create a module
ng g m recipes/recipes
// recipes.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RecipesComponent } from './recipes.component';
import { RecipesRoutingModule } from './recipes-routing.module';
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [RecipesComponent],
imports: [CommonModule, RecipesRoutingModule, ReactiveFormsModule]
})
export class RecipesModule {}
// recipes-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { RecipesComponent } from './recipes.component';
const recipeRoutes: Routes = [{ path: 'recipes', component: RecipesComponent }];
@NgModule({
imports: [RouterModule.forChild(recipeRoutes)], // forChild!
exports: [RouterModule]
})
export class RecipesRoutingModule {}
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { ShoppingListComponent } from './shopping-list/shopping-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ProfileComponent } from './profile/profile.component';
import { AuthGuard } from './auth-guard.service';
import { SignUpComponent } from './auth/sign-up/sign-up.component';
import { SignInComponent } from './auth/sign-in/sign-in.component';
const appRoutes: Routes = [
{
path: '',
redirectTo: '/recipes',
pathMatch: 'full'
},
{
path: 'shopping-list',
component: ShoppingListComponent,
canActivate: [AuthGuard]
},
{
path: 'profile',
canActivate: [AuthGuard],
component: ProfileComponent
},
{
path: 'sign-up',
component: SignUpComponent
},
{
path: 'sign-in',
component: SignInComponent
},
{ path: 'not-found', component: PageNotFoundComponent },
{ path: '**', redirectTo: '/not-found' }
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
// app.module.ts
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
ShoppingListComponent,
PageNotFoundComponent,
ProfileComponent,
SignUpComponent,
SignInComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
RecipesModule
],
providers: [StaticAuthService, AuthGuard, DataStorageService],
bootstrap: [AppComponent]
})
export class AppModule {}
as
operator
*ngFor="let item of items; index as i"