<img alt="item" [src]="itemImageUrl">
The [src]
property is bound to the <img>
HTML element. We can utilize this property binding in our template code.
<!-- Bind button disabled state to the `isUnchanged` property -->
<button type="button" [disabled]="isUnchanged">Disabled Button</button>
We can also use the [disabled]
property to control whether the button is enabled or not. This property corresponds to the HTML attribute disabled. As seen, property binding enables us to control HTML elements dynamically.
<button (click)="onSave()">Save</button>
Clicking the above button triggers the execution of the onSave()
method in the component. The (click)
event binding in Angular functions similarly to the onClick
event in HTML. Angular provides this method of event binding to manage HTML events.
<!-- Toggle the "special" class on/off with a property -->
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>
We can alter class state using ngClass
. In the above example, the state of the [ngClass]
value changes based on the value of isSpecial
.
currentClasses: Record<string, boolean> = {};
/* . . . */
setCurrentClasses() {
// CSS classes: added/removed based on the current state of component properties
this.currentClasses = {
saveable: this.canSave,
modified: !this.isUnchanged,
special: this.isSpecial
};
}
The currentClasses
object is updated based on the values of the properties. These properties determine which CSS classes are added or removed.
<div [ngClass]="currentClasses">This div is initially saveable, unchanged, and special.</div>
The class of the div
element is determined by the currentClasses
object.
currentStyles: Record<string, string> = {};
/* . . . */
setCurrentStyles() {
// CSS styles: set based on the current state of component properties
this.currentStyles = {
'font-style': this.canSave ? 'italic' : 'normal',
'font-weight': !this.isUnchanged ? 'bold' : 'normal',
'font-size': this.isSpecial ? '24px' : '12px'
};
}
Similar to ngClass
, ngStyle
is used to dynamically apply styles. In the currentStyles
object, various styles change based on the values of canSave
, isUnchanged
, and isSpecial
.
<div [ngStyle]="currentStyles">
This div is initially italic, normal weight, and extra large (24px).
</div>
The ngModel
directive is used to display a data property and update it when the user makes changes. Before using this directive, ensure you add FormsModule
to the imports
list of your NgModule.
import { FormsModule } from '@angular/forms';
/* . . . */
@NgModule({
/* . . . */
imports: [
BrowserModule,
FormsModule
],
/* . . . */
})
export class AppModule { }
Afterward, you can use the ngModel
directive like so:
<label for="example-ngModel">[(ngModel)]:</label>
<input [(ngModel)]="currentItem.name" id="example-ngModel">
This enables two-way binding between the input element and the currentItem.name
property.
Customizing the directive is also possible:
<input [ngModel]="currentItem.name" (ngModelChange)="setUppercaseName($event)" id="example-uppercase">
To conditionally add or remove HTML elements, use ngIf
. The app-item-detail
component will be displayed or hidden based on the value of the isActive
property. If isActive
is true
, the app-item-detail
component will be visible; otherwise, it will not be displayed.
<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>
To display a list of items, use ngFor
. In the following example, the items
array is iterated over and each item's name is displayed.
<div *ngFor="let item of items">{{item.name}}</div>
You can also pass each item to the child component using ngFor
:
<app-item-detail *ngFor="let item of items" [item]="item"></app-item-detail>
To display the index of the ngFor
iteration, use the following syntax:
<div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>
You can use <ng-container>
it's especially useful when we need to avoid interfering with styles or layout because Angular doesn't render it in the DOM.
<p>
I turned the corner
<ng-container *ngIf="hero">
and saw {{hero.name}}. I waved
</ng-container>
and continued on my way.
</p>
In this section, I will demonstrate how to bind custom properties and events. We will create an application that allows the addition and deletion of servers using the Add Server
and Add Server Blueprint
functionalities.
The application comprises three components:
app.component.ts
import {
Component,
OnDestroy
} from '@angular/core';
import { Server } from './server.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
title = 'deep-dive-angular';
serverElements: Server[] = [{
type: 'server',
name: 'Test Server',
content: 'Just a test',
}];
onServerAdded(serverData: { serverName: string, serverContent: string }) {
this.serverElements.push({
type: 'server',
name: serverData.serverName,
content: serverData.serverContent
});
}
onServerBlueprintAdded(serverData: { serverName: string, serverContent: string }) {
this.serverElements.push({
type: 'blueprint',
name: serverData.serverName,
content: serverData.serverContent
});
}
onChangeFirst() {
this.serverElements[0].name = 'Changed!';
}
onDestroyFirst() {
this.serverElements.splice(0, 1);
}
ngOnDestroy(): void {
// Cleanup logic if needed
}
}
app.component.html
<div class="container">
<app-cockpit
(serverCreated)="onServerAdded($event)"
(bpCreated)="onServerBlueprintAdded($event)"
></app-cockpit>
<hr>
<div class="row">
<div class="col-xs-12">
<button class="btn btn-primary" (click)="onChangeFirst()">Change first element</button>
<button class="btn btn-danger" (click)="onDestroyFirst()">Destroy first component</button>
<app-server-element
*ngFor="let element of serverElements"
[svrElement]="element"
[name]="element.name">
<p #contentParagraph>
<strong *ngIf="element.type === 'server'" style="color: red">
{{ element.content }}
</strong>
<em *ngIf="element.type === 'blueprint'">
{{ element.content }}
</em>
</p>
</app-server-element>
</div>
</div>
<hr>
<div class="col-xs-12">
<label for="title">ngOnChanges TEST </label> <br>
<input id="title" type="text" [(ngModel)]="title">
</div>
</div>
In the AppComponent, we have an app-cockpit
component with event bindings (serverCreated)
and (bpCreated)
. This implies that when these events are triggered within the app-cockpit
, the changed values will be emitted to the parent component (AppComponent). Then, AppComponent will update the serverData
property values using the onServerAdded($event)
and onServerBlueprintAdded($event)
methods.
AppComponent also contains an app-server-element
component with property bindings [svrElement]
and [name]
. The element
is provided by the serverElements
array used in the ngFor
loop statement.
cockpit.component.ts
import { Component, EventEmitter, Output } from '@angular/core';
import { Server } from '../server.model';
@Component({
selector: 'app-cockpit',
templateUrl: './cockpit.component.html',
styleUrls: ['./cockpit.component.css']
})
export class CockpitComponent {
@Output() serverCreated = new EventEmitter<Server>();
@Output('bpCreated') blueprintCreated = new EventEmitter<Server>();
newServerName = '';
newServerContent = '';
onAddServer() {
this.serverCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
});
}
onAddBlueprint() {
this.blueprintCreated.emit({
serverName: this.newServerName,
serverContent: this.newServerContent
});
}
}
cockpit.component.html
<div class="row">
<div class="col-xs-12">
<label for="name">Server Name</label>
<input type="text" class="form-control" id="name" [(ngModel)]="newServerName">
<label for="content">Server Content</label>
<input type="text" class="form-control" id="content" [(ngModel)]="newServerContent">
<br>
<button class="btn btn-primary" (click)="onAddServer()">Add Server</button>
<button class="btn btn-primary" (click)="onAddBlueprint()">Add Server Blueprint</button>
</div>
</div>
In the CockpitComponent
, two properties are defined: newServerName
and newServerContent
, which are initialized to empty strings. In the template, they are used with two-way binding [(ngModel)]
. Typing something in the input
fields updates these property values. When the Add Server
button is clicked, the onAddServer()
method is triggered, emitting the serverName
and serverContent
data that was entered into the input fields.
server-element.component.ts
import {
Component,
Input,
ViewEncapsulation
} from '@angular/core';
@Component({
selector: 'app-server-element',
templateUrl: './server-element.component.html',
styleUrls: ['./server-element.component.css'],
encapsulation: ViewEncapsulation.Emulated
})
export class ServerElementComponent {
@Input('svrElement') element: {
type: string,
name: string,
content: string
};
@Input() name: string;
// Rest of your code
}
server-element.component.html
<div class="panel panel-default">
<div class="panel-heading" #heading> {{ name }} </div>
<div class="panel-body">
<ng-content></ng-content>
</div>
</div>
server-element.component.css
p {
color: blue;
}
label {
color: red;
}
In the ServerElementComponent
, the element
property is bound using the @Input
decorator. This allows the component to receive data from the parent component. The name
property is also bound using @Input
. The template displays the name
using interpolation ({{ name }}
), and the content inside the ng-content
tag is projected from the parent component.
https://medium.com/@su_bak/angular-viewencapsulation-d33fbaf8bf68
View queries are set before the ngAfterViewInit
callback is called.
When you want to access a child component's properties, you can use this decorator @ViewChild
, but sometimes it can make the code more complicated. You should use this decorator carefully.
Components that are included within parent components are referred to as child components. You can use property binding
if you want to send data to a child component. However, if you use the @ViewChild
decorator, you can directly access the property of the child component without using property binding
.
app-component.ts
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
import { ChildComponent } from "./child/child.component";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit {
// Direct Access to Child Data
@ViewChild(ChildComponent) childComp!: ChildComponent;
// Direct Access to Template Element
@ViewChild('top') topRef!: ElementRef;
ngAfterViewInit(): void {
console.log(this.childComp.childData)
console.log(this.topRef.nativeElement.textContent)
}
}
app-component.html
<div class="container">
<div class="row">
<div class="col-md-12">
<app-child>
<p>What's up</p>
</app-child>
<p #top>TOP</p>
</div>
</div>
</div>
child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent {
childData = 'child data'
}
child.component.html
<ng-content></ng-content>
If you need to control multiple child components, you can use ViewChildren
.
Used to retrieve the first element or directive matching the selector from the content DOM. If the content DOM changes and a new child matches the selector, the property will be updated. Content queries are set before ngAfterContentInit
.
app-component.html
<div class="container">
<div class="row">
<div class="col-md-12">
<app-child>
<p #greeting>What's up</p>
</app-child>
<p #top>TOP</p>
</div>
</div>
</div>
child.component.ts
import { AfterContentInit, Component, ContentChild, ElementRef } from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements AfterContentInit {
childData = 'child data'
@ContentChild('greeting') greeting!: ElementRef;
ngAfterContentInit(): void {
console.log('CONTENT CHILD SAID: ' + this.greeting.nativeElement.textContent)
}
}
child.component.html
<ng-content></ng-content>
If you need to control multiple child components, you can use ContentChildren
.
https://angular.io/guide/architecture
https://angular.io/guide/architecture-next-steps
https://angular.io/guide/observables
https://angular.io/guide/lifecycle-hooks
https://angular.io/guide/forms-overview
Angular makes signle-page client application using HTML and TypeScript. As a set of TypeScript library, you can implements core and optional functionality.
The architecture of Angular relies on certain fundamental concepts. Angular components which are building block of the Angular framework are organized into ngModules
. the ngModules
collect related code into functional sets and an Angular application is defined by the ngModules
. An Angular application has at least a root module.
views
which are sets of screen eletments.services
which provide specific funtionality not directly related to views
.Modules
, Components
, Services
are clases that use decorators.
And an Angular framework provides router
service which provides sophisticated in-browser navigatinal capabilities.
A component instance has a lifecycle that starts when Angular instantiates the component class and the component view along with it's child views. The lifecycle continues with change detection, when the data-bound properties change, Angular updates both the view and component instance and the lifecycle ends when Angular destoys the component instance and removes it's rendered template from the DOM. directives have a similar lifecyle, as Angular creates, updates, and destroys instaces in the course of excution.
Respond when Angular sets or resets data-bound input properties. The method receives a SimpleChanges
object of current and previous property values. Called before ngOnInit() (if the component has bound inputs) and whenever one or more data-bound input properties change.
Initialize the directive or component after Angular first displays the data-bound properties and sets the directive or component's input properties. Called once, after the first ngOnChanges(). ngOnInit() is still called even when ngOnChanges() is not (which is the case when there are no template-bound inputs).
Detect and act upon changes that Angular can't or won't detect on its own. Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run.
Defining custom change detection
Respond after Angular projects external content into the component's view, or into the view that a directive is in. Called once after the first ngDoCheck().
When the <ng-content>
is projected by a parent template to add contents into a child template ngOnAfterContentInit
is executed.
Respond after Angular checks the content projected into the directive or component. Called after ngAfterContentInit() and every subsequent ngDoCheck().
Respond after Angular initializes the component's views and child views, or the view that contains the directive. Called once after the first ngAfterContentChecked(). When views are rendered ngOnAfterViewInit
is executed.
Respond after Angular checks the component's views and child views, or the view that contains the directive. Called after the ngAfterViewInit() and every subsequent ngAfterContentChecked().
Cleanup just before Angular destroys the directive or component. Unsubscribe Observables and detach event handlers to avoid memory leaks. Called immediately before Angular destroys the directive or component.
We want to make users find routes which they want and Router module helps users find the routes with route module
like below. appRoute
is like a routing table of Angular application and we should import the appRoute
to Angular module to make Angular aware this.
import {RouterModule, Routes} from "@angular/router";
import {NgModule} from "@angular/core";
import {HomeComponent} from "./home/home.component";
const appRoute: Routes = [
{path: '', component: HomeComponent}
]
@NgModule({
imports: [
RouterModule.forRoot(appRoute)
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
After creating the AppRoutingModule
, we should add the module in the imports list of AppModule
to make Angular aware the AppRoutingModule
.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import {RouterOutlet} from "@angular/router";
import { ServersComponent } from './servers/servers.component';
import { UsersComponent } from './users/users.component';
import { HomeComponent } from './home/home.component';
import {AppRoutingModule} from "./app-routing.module";
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
ServersComponent,
UsersComponent,
HomeComponent
],
imports: [
BrowserModule,
RouterOutlet,
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Finally we add RouterOutlet
in AppComponent
to make Angular navigate routes with our AppRoutingModule
after adding the codes we can check below.
HeaderComponent
<div class="container">
<div class="row">
<div class="col-md-12">
<h4>HOME</h4>
</div>
</div>
</div>
AppComponent
<app-header></app-header>
<router-outlet></router-outlet>
In our Angular application, navigating between different pages like Servers
and Users
is made effortless using RouterLink
. These links, placed in the header, seamlessly guide users to their desired destinations. It's important to note that this mechanism is tailored for Single Page Applications (SPAs). Unlike traditional websites, where clicking a link would trigger a server request and reload the entire page, RouterLink ensures a smooth transition within the application.
import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'servers', component: ServersComponent },
{ path: 'users', component: UsersComponent },
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
<!-- Your header component HTML remains unchanged. -->
In our HomeComponent, we've implemented a button that, when clicked, navigates users to the /servers
route.
import { Component } from '@angular/core';
import { Router } from "@angular/router";
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent {
constructor(private router: Router) { }
OnLoadServers() {
this.router.navigate(['servers']);
}
}
When reloading the current page within the ServersComponent, we've used the relativeTo
option to ensure a relative path is followed, maintaining the context of the current route.
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from "@angular/router";
@Component({
selector: 'app-servers',
templateUrl: './servers.component.html',
styleUrls: ['./servers.component.css']
})
export class ServersComponent {
constructor(private router: Router, private route: ActivatedRoute) { }
onReload() {
this.router.navigate(['servers'], { relativeTo: this.route });
}
}
For parameter parsing, the ServerComponent dynamically fetches server details based on the ID provided in the route parameters. We've employed observables to ensure accurate handling of URL parameter changes.
import { Component, OnInit } from '@angular/core';
import { ServersService } from "../servers-service";
import { Server } from "../server.model";
import { ActivatedRoute, Params } from "@angular/router";
@Component({
selector: 'app-server',
templateUrl: './server.component.html',
styleUrls: ['./server.component.css']
})
export class ServerComponent implements OnInit {
server: Server | undefined;
constructor(private serversService: ServersService, private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.params.subscribe((params: Params) => {
const id = +params['id'];
this.server = this.serversService.getServer(id);
});
}
}
In our application, child routes have been implemented to display detailed server information. The AppRoutingModule has been configured to handle these nested routes, allowing for a structured and intuitive user experience.
import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'servers', component: ServersComponent, children: [
{ path: ':id', component: ServerComponent }
]},
{ path: 'users', component: UsersComponent },
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
In your Angular application, you can seamlessly redirect users to specific pages for non-existent routes or routes that require special permissions using AppRouterApp
.
import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { AuthGuardService } from "./auth-guard.service";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { CanDeactivateGuard } from "./servers/edit-server/can-deactivate-guard.service";
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'servers',
component: ServersComponent,
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent },
]
},
{ path: 'users', component: UsersComponent },
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Certain pages require permission, such as logging in. To redirect users to a specific page when they are not logged in, utilize the canActivate
feature. This evaluates the user's login status and redirects them accordingly.
First, create the AuthService
with methods like login
, logout
, and isAuthenticated
to manage user login state.
@Injectable()
export class AuthService {
private isLogin: boolean = false;
isAuthenticated(): Promise<boolean> {
return new Promise(resolve => setTimeout(() => resolve(this.isLogin), 500));
}
login() {
this.isLogin = true;
}
logout() {
this.isLogin = false;
}
}
Next, implement the AuthGuardService
using the CanActivate
interface, checking the user's authentication status and redirecting if necessary.
@Injectable()
export class AuthGuardService 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 => {
if (authenticated) {
return true;
} else {
this.router.navigate(['/']);
return false;
}
});
}
}
To apply the canActivate
feature, add the canActivate
option to the route where you want to enforce authentication.
const appRoutes: Routes = [
// ... other routes
{
path: 'servers',
component: ServersComponent,
canActivate: [AuthGuardService], // <-- Apply canActivate guard here
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent },
]
},
// ... other routes
];
Finally, create login and logout buttons to access the servers
page, which requires login. Clicking the login
button allows access to the servers
page, and clicking the logout
button denies access.
HomeComponent
import { Component } from '@angular/core';
import { Router } from "@angular/router";
import { AuthService } from "../auth.service";
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent {
constructor(private router: Router, private authService: AuthService) {}
onLoadServers() {
this.router.navigate(['servers']);
}
onLogin() {
this.authService.login();
}
onLogout() {
this.authService.logout();
}
}
HomeComponent Template
<div class="container">
<div class="row">
<div class="col-md-12">
<h4>HOME</h4>
<button class="btn btn-primary" (click)="onLoadServers()">Load Servers</button>
<button class="btn btn-info" (click)="onLogin()">Login</button>
<button class="btn btn-warning" (click)="onLogout()">Logout</button>
</div>
</div>
</div>
If you wish to implement this feature in child routes, utilize the canActivateChild
method in your AuthGuardService
.
In Angular, the canDeactivate
feature allows you to perform actions when a route is deactivated, giving you the opportunity to handle scenarios where a user attempts to leave a page without saving their data. For instance, you can alert the user that unsaved changes will be lost using the canDeactivate
method.
EditServerComponent
Firstly, create the EditServerComponent
which enables users to edit server details.
import { Component, OnInit } from '@angular/core';
import { ServersService } from "../servers-service";
import { ActivatedRoute, Router } from "@angular/router";
import { Server } from "../server.model";
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 {
serverName: string = '';
serverStatus: string = '';
serverId: number;
server: Server;
changesSaved: boolean = false;
constructor(private serversService: ServersService,
private route: ActivatedRoute,
private router: Router) {}
ngOnInit(): void {
this.serverId = +this.route.snapshot.params['id'];
this.server = this.serversService.getServer(this.serverId);
if (this.server) {
this.serverName = this.server.name;
this.serverStatus = this.server.status;
}
}
onUpdateServer() {
this.serversService.updateServer(this.serverId, { name: this.serverName, status: this.serverStatus });
this.changesSaved = true;
this.router.navigate(['../'], { relativeTo: this.route });
}
canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.changesSaved) {
return confirm('You have unsaved changes! Do you really want to leave?');
}
return true;
}
}
EditServerComponent Template
<h4>Server Edit Page</h4>
<div class="form-group">
<label for="name">Server Name</label>
<input type="text" id="name" class="form-control" [(ngModel)]="serverName">
</div>
<div class="form-group">
<label for="status">Server Status</label>
<select type="text" id="status" class="form-control" [(ngModel)]="serverStatus">
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
<button class="btn btn-primary" (click)="onUpdateServer()">Update Server</button>
</div>
CanDeactivateGuard
Next, create the CanDeactivateGuard
and CanComponentDeactivate
interface. The guard will be used in your AppRoutingModule
with the CanDeactivate
option.
import { Observable } from "rxjs";
import { ActivatedRouteSnapshot, CanDeactivate, RouterStateSnapshot, UrlTree } 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 | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
return component.canDeactivate();
}
}
AppRoutingModule Implementation
Finally configure the canDeactivate
guard in your AppRoutingModule
to enable the alert functionality when a user attempts to leave the editing server page without saving changes.
import { RouterModule, Routes } from "@angular/router";
import { NgModule } from "@angular/core";
import { HomeComponent } from "./home/home.component";
import { ServersComponent } from "./servers/servers.component";
import { UsersComponent } from "./users/users.component";
import { ServerComponent } from "./servers/server/server.component";
import { PageNotFoundComponent } from "./page-not-found/page-not-found.component";
import { AuthGuardService } from "./auth-guard.service";
import { EditServerComponent } from "./servers/edit-server/edit-server.component";
import { CanDeactivateGuard } from "./servers/edit-server/can-deactivate-guard.service";
const appRoutes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'servers',
component: ServersComponent,
canActivateChild: [AuthGuardService],
children: [
{ path: ':id', component: ServerComponent },
{ path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard] },
]
},
{ path: 'users', component: UsersComponent },
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
With these implementations, users will be prompted with an alert when trying to leave the editing server page without saving changes, ensuring a smooth and intuitive user experience.
The reolve gurad is used to retrieve data from the API and once we get the data we will navigate to the route. We create resolve service which retrieve the server data with resolve method.
General routing flow is that
1. User clicks the link
2. Angular loads the respective component
Routing flow with resolver
1. User click the link
2. Angular executes certain code and returns a value or observable
3. You can collect the returned value or observable in constructor or in ngOnInit, in class of your component which is about to load
4. Use the collected the data for your purpose
5. Now you can load your component
Steps 2, 3, 4 are done with a code called Resolver.
Basically resolver is that intermediate code, which can be executed when a link has been clicked and before a component is loaded.
ServerResorverService
import { Injectable } from '@angular/core';
import {ServersService} from "../servers-service";
import {ActivatedRouteSnapshot, Resolve, RouterStateSnapshot} from "@angular/router";
import {Server} from "../server.model";
import {Observable} from "rxjs";
@Injectable()
export class ServerResolverService implements Resolve<Server>{
constructor(private serversService:ServersService) { }
resolve(route: ActivatedRouteSnapshot,
state: RouterStateSnapshot):
Observable<Server> | Promise<Server> | Server {
return this.serversService.getServer(+route.params['id']);
}
}
Next add the resolve option into the servers/:id
route
AppRoutingModule
import {RouterModule, Routes} from "@angular/router";
import {NgModule} from "@angular/core";
import {HomeComponent} from "./home/home.component";
import {ServersComponent} from "./servers/servers.component";
import {UsersComponent} from "./users/users.component";
import {ServerComponent} from "./servers/server/server.component";
import {PageNotFoundComponent} from "./page-not-found/page-not-found.component";
import {AuthGuardService} from "./auth-guard.service";
import {EditServerComponent} from "./servers/edit-server/edit-server.component";
import {CanDeactivateGuard} from "./servers/edit-server/can-deactivate-guard.service";
import {ServerResolverService} from "./servers/server/server-resolver.service";
const appRoute: Routes = [
{path: '', component: HomeComponent},
{path: 'servers',
component: ServersComponent,
// canActivate: [AuthGuardService],
canActivateChild: [AuthGuardService],
children: [
{path: ':id', component: ServerComponent, resolve: {server: ServerResolverService}},
{path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard]},
]},
{path: 'users', component: UsersComponent},
{path: '**', component: PageNotFoundComponent},
]
@NgModule({
imports: [
RouterModule.forRoot(appRoute)
],
exports: [RouterModule]
})
export class AppRoutingModule {
}
Finally in the ServerComponent, we can subscribe the server
data.
Observable has various of a veriety of data like (user input) Events, Http Request, Triggerd in Code and We can trigger the Observable with Observer like below.
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit, OnDestroy {
private firstObsSubscription: Subscription;
constructor() {
}
ngOnInit() {
this.firstObsSubscription = interval(1000).subscribe(count => {
console.log(count);
})
... skip ...
in the example, the interval
of rxjs
is observable which can be subcribed and if you come to the AppCopoment view, It woulb be executed in the console of user's browser.
You should destroy the observable when you finish your job with this. Otherwise it will lead memory leak.
... skip ...
ngOnDestroy(): void {
this.activatedSub.unsubscribe();
}
... skip ...
in this section we are going to make our custom observable.
... Skip ...
ngOnInit() {
// Let's make our custom observable
const customIntervalObservable = Observable.create(observer => {
let count = 0;
setInterval(() => {
observer.next(count);
if (count == 2) {
observer.complete();
}
if (count > 3) {
observer.error(new Error('Count is greater 3!'));
}
count++;
}, 1000)
});
this.firstObsSubscription = customIntervalObservable.subscribe(data =>{
console.log(data);
}, error => {
console.log(error.message);
}, () => {
console.log('Completed!')
})
... Skip ...
We made Observable
with create
function which will be triggerd from Observer
. this observable has setInterval
function executed next
, error
, complete
functions of the Observable
. firstObsSubscription
subscribes the customIntervalObservable
to use the data emitted by the Observable
which changes the count
located in observable.next(count);
per one second. if count
become 2, the Observable
is finished and executes console.log('Completed!')
at the customIntervalObservable.subscribe
. It will be executed when the logic of Observable
is ended without any erorrs. if the count
is bigger than 3, error occures and Observable
is finished. when it comes to error, the observer.complete();
is not working.
We can manage the data we received by the Observable
with Operator
of rxjs
.
this.firstObsSubscription = customIntervalObservable
.pipe(filter(data => {
return data > 0;
})
, map((data: number) => {
return 'Round: ' + (data + 1);
})).subscribe(
data => {
console.log(data);
}, error => {
console.log(error.message);
alert(error.message)
}, () => {
console.log('Completed!');
});
We should add the pipe
before subscribe
to create, read, update, delete or filter the data Observable
sent to the Observer
. We can do anything you want in the operator pipe
. In the pipe
operator, We can filter and edit the data and then the data will go through subscribe
. Therefore the data processed in subscribe
will be below because the data is filtered and edited with pipe
operators.
Subject
is type of Observable
but It's little different with Observable
We learned.
Observable
works as Unicast
and Subject
works as multicast. Observable
need to be subscribed to trigger some of works located in the Observable
with Observer
.
But Subject
doesn't need Observer
to trigger the Observable
because Subject
is Observable
and Observer
at the same time. so Subeject
can call the next
, error
, complete
functions out of the code without Observer
because it had the Observer
already.
We are going to change @Output
value to Subject
in the below example.
user.service.ts
import {EventEmitter, Injectable} from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UserService {
activateEmitter = new EventEmitter<boolean>();
constructor() { }
}
user.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import {UserService} from "../user.service";
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
id: number;
constructor(private route: ActivatedRoute,
private userService: UserService) {
}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.id = +params.id;
});
}
onActivate() {
this.userService.activateEmitter.emit(true);
}
}
user.component.html
<p>User with <strong>ID {{ id }}</strong> was loaded</p>
<button class="btn btn-primary" (click)="onActivate()">Activate</button>
app.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import {UserService} from "./user.service";
import {Subscription} from "rxjs";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
userActivated = false;
private activatedSub: Subscription;
constructor(private userService: UserService) {}
ngOnInit() {
this. activatedSub = this.userService.activateEmitter.subscribe(didActivate => {
this.userActivated = didActivate;
})
}
ngOnDestroy(): void {
this.activatedSub.unsubscribe();
}
}
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">
<a routerLink="/">Home</a> |
<a [routerLink]="['user', 1]">
User 1
</a>
|
<a [routerLink]="['user', 2]">
User 2
</a>
</div>
</div>
<hr />
<p *ngIf="userActivated">Activated!</p>
<hr />
<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>
Above example indicates the usage of @Output
value to send the emmited data activateEmitter
and AppComponent subscribes the activateEmitter
with userService
. We can change this logic with Subject
.
user.service.ts
import {EventEmitter, Injectable} from '@angular/core';
import {Subject} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class UserService {
// activateEmitter = new EventEmitter<boolean>();
activateEmitter= new Subject<boolean>();
constructor() { }
}
user.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import {UserService} from "../user.service";
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
id: number;
constructor(private route: ActivatedRoute,
private userService: UserService) {
}
ngOnInit() {
this.route.params.subscribe((params: Params) => {
this.id = +params.id;
});
}
onActivate() {
this.userService.activateEmitter.next(true);
}
}
As I said We can use next
with activateEmitter.next(true)
afterwards it triggered to transfer the boolean
data to activateEmitter
located in UserService.
Angular infers the From Object from the DOM.
In order to use forms' input data as javascript object in our Angular application, we should use @ViewChild
annotation. we added #f="ngForm"
for this and used it at the component of the template. after that we can use the forms' data as javascript objects.
to make Agnaulr application know the forms' data, we added ngModel
in the input
or select
tag and various options are used in the tags. required
option indicates we should type the form essentially.
to control a specific tag data, we should add template variable like #email=ngModel
. we can access the email
data directrly in the template level with the template variable. we can validate the data with the template variable also.
for advanced user experience, we can use css functions which can explain or express the state of the forms' data user is typing or user has typed.
app.component.ts
import {Component, ViewChild} from '@angular/core';
import {NgForm} from "@angular/forms";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
@ViewChild('f') signupForm: NgForm;
defaultQuestion = 'pet';
answer: string = '';
genders = ['male', 'female'];
user = {
username: '',
email: '',
secretQuestion: '',
answer: '',
gender: '',
};
submitted = false;
suggestUserName() {
const suggestedName = 'Superuser';
// It's not a best practice because the code is long
// And we should change other things not related to the username
// this.signupForm.setValue({
// userData: {
// username: suggestedName,
// email: ''
// },
// secret: 'pet',
// questionAnswer: '',
// gender: 'mail',
// })
// this way is better than before, because we don't have to override others
this.signupForm.form.patchValue({
userData: {
username: suggestedName,
}
})
}
// Below indicates Template-driven approach
// The way to access javascript DOM Object of the form we wrote at the view page
// onSubmit(f: NgForm) {
// console.log(f)
// }
// We can access the form data with @ViewChild like below
// We can validate the user's inputs with the NgForm object
// We should validate it at front-end and back-end always
// There are many types of validators provided by Angular : https://angular.io/api/forms/Validators
onSubmit() {
console.log(this.signupForm);
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;
this.submitted = true;
this.signupForm.reset();
}
}
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">
<form (ngSubmit)="onSubmit()" #f="ngForm">
<!-- You can group the user-data inputs with `ngModelGroup`-->
<!-- After that, You can check the `userData` in the From object-->
<div id="user-data"
ngModelGroup="userData"
#userData="ngModelGroup"
>
<div class="form-group">
<label
for="username">Username</label>
<input type="text"
id="username"
class="form-control"
ngModel
name="username"
required>
</div>
<button class="btn btn-default"
type="button"
(click)="suggestUserName()">
Suggest an Username
</button>
<div class="form-group">
<label for="email">Mail</label>
<input type="email"
id="email"
ngModel
name="email"
class="form-control"
required
email
#email="ngModel">
<span class="help-block"
*ngIf="!email.valid && email.touched"
>
Please enter a valid email!</span>
<!-- the local value "email" exposes the form object's control information about Email-->
</div>
</div>
<p
*ngIf="!userData.valid && userData.touched"
class="help-block"
>User Data is invalid!</p>
<div class="form-group">
<label for="secret">Secret Questions</label>
<!-- If you use [(ngModel)] in there, you can choose the default value we wrote-->
<select id="secret"
[(ngModel)]="defaultQuestion"
name="secret"
class="form-control">
<option value="pet">Your first Pet?</option>
<option value="teacher">Your first teacher?</option>
</select>
</div>
<div class="form-group">
<textarea name="questionAnswer"
rows="3"
[(ngModel)]="answer"
class="form-control"></textarea>
</div>
<div class="radio"
*ngFor="let gender of genders">
<label>
<input
type="radio"
name="gender"
ngModel
required
[value]="gender">
{{ gender }}
</label>
</div>
<p>Your reply : {{ answer }}</p>
<button class="btn btn-primary"
[disabled]="!f.valid"
type="submit">Submit
</button>
</form>
</div>
</div>
<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 : {{ user.secretQuestion }} </p>
<p>Answer : {{ user.answer }}</p>
<p>Gender : {{ user.gender }}</p>
</div>
</div>
<hr>
<app-form-pratice></app-form-pratice>
</div>
app.component.css
.container {
margin-top: 30px;
}
input.ng-invalid.ng-touched {
border: 1px solid red;
}
Form is created programmatically and synchronized with the DOM.
In this section, we can make forms with the reactive approach unlike the template driven approch, the reactive approach makes forms with typescript code. we need to add ReactiveFormModule
in our app.module.ts
before you use ReactiveForm
.
We should declare FormGroup
and FormControl
at a ngOnInit
method. those are the forms' data we want to control. we made a value signUpForm
and initialized this at ngOnInit
method. in order to apply the FormGroup
and FormControl
in the template of the component, we added the signUpForm
as FormGroup
name and the forms' data like username
, email
as FormControl
name. after that we can check that the angular control the forms' data.
From now on, we can control the forms' data with various validators which include custrom validators and we can show up various messages for a user interface using the forms' data which made by javascript object.
You can also make various custom validators which work as an async and sync processes.
app.component.ts
import {Component, OnInit} from '@angular/core';
import {FormArray, FormControl, FormGroup, Validators} from "@angular/forms";
import {Observable} from "rxjs";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
genders = ['male', 'female'];
signupForm: FormGroup;
forbiddenUsernames = ['superuser', 'root'];
ngOnInit(): void {
// the validators' return type should be Observable or Promise
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)
);
}
onSubmit() {
console.log(this.signupForm);
}
onAddHobby() {
const control = new FormControl(null,
Validators.required);
(<FormArray>this.signupForm.get('hobbies')).push(control);
}
getControls() {
return (<FormArray>this.signupForm.get('hobbies')).controls;
}
forbiddenNames(control: FormControl): {[s: string]: boolean} {
if (this.forbiddenUsernames.indexOf(control.value) !== -1) {
return {'nameIsForbidden': true};
} return null;
// return {'nameIsForbidden': false};
// you can tell angular the form is valid or not with above method
// you can use both return types 'null' or javascript object 'statement'
}
// async validator method
forbiddenEmails(control: FormControl): Promise<any> | Observable<any> {
return new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value === 'test@test.com') {
resolve({'emailIsForbidden': true});
} else {
resolve(null);
}
}, 1500);
});
}
}
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">
<!-- We should connect signupForm with formGroup of the template -->
<form [formGroup]="signupForm" (ngSubmit)="onSubmit()">
<!-- We should make angular know fromGroupName is 'userData' we declared at 'signupForm' -->
<div formGroupName="userData">
<div class="form-group">
<label for="username">Username</label>
<!-- We should make angular know formControlName is 'username' -->
<input
type="text"
id="username"
class="form-control"
formControlName="username"
>
<span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched">
<span class="help-block"
*ngIf="signupForm.get('userData.username').errors['nameIsForbidden']">
this name is invalid
</span>
<span class="help-block"
*ngIf="signupForm.get('userData.username').errors['required']">
this username field is required.
</span>
</span>
</div>
<div class="form-group">
<label for="email">email</label>
<!-- We should make angular know formControlName is 'email' -->
<input
type="text"
id="email"
class="form-control"
formControlName="email"
>
<span class="help-block"
*ngIf="!signupForm.get('userData.email').valid && signupForm.get('userData.email').touched"
>Please enter a valid email!</span>
</div>
</div>
<div class="radio" *ngFor="let gender of genders">
<label>
<!-- We should make angular know formControlName is 'gender' -->
<input
type="radio"
[value]="gender"
formControlName="gender"
>{{ gender }}
</label>
</div>
<!-- we should make angular know the formArrayName is 'hobbies' we declared at signupForm-->
<div formArrayName="hobbies">
<h4>Your Hobbies</h4>
<button
class="btn btn-info"
type="button"
(click)="onAddHobby()"
>
Add Hobby
</button>
<div
class="form-group"
*ngFor="let hobbyControl of getControls(); let i = index">
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
<span class="help-block"
*ngIf="!signupForm.valid && signupForm.touched"
>Please enter a valid data!</span> <br>
<button class="btn btn-primary" type="submit">Submit</button>
</form>
<br><hr>
<app-reactive-form-practice></app-reactive-form-practice>
</div>
</div>
</div>
app.component.css
.container {
margin-top: 30px;
}
input.ng-invalid.ng-touched {
border: 1px solid red;
}
We can transform strings, currency amounts, dates and other data for display with pipes. If we want to express date
type data to user, we can simply use this with DatePipe
. the DatePipe
provides various date types we want to customize. like DatePipe
, we can change strings to uppercase with uppercase
.
We can make custom pipes like shorten
and filter
pipes. If you want to execute the fitler
pipe as async mode, you should add pure: false
option in the pipe decoration of filter
pipe.
We used Promise
obejct and added this as mustache at template but Angular doesn't know this is Promise
type. We should make Angular know that type is Promise
to make Angular subscribe the Promise
object to express appStatus
with Async
mode. We should add the pipes in the declaration section of AppModule.
app.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
appStatus= new Promise((resolve, reject) => {
setTimeout(() => {
resolve('stable');
}, 2000)
})
servers = [
{
instanceType: 'medium',
name: 'Production',
status: 'stable',
started: new Date(15, 1, 2017)
},
{
instanceType: 'large',
name: 'User Database',
status: 'stable',
started: new Date(15, 1, 2017)
},
{
instanceType: 'small',
name: 'Development Server',
status: 'offline',
started: new Date(15, 1, 2017)
},
{
instanceType: 'small',
name: 'Testing Environment Server',
status: 'critical',
started: new Date(15, 1, 2017)
}
];
filterStatus = '';
// Pipe API
// https://angular.io/api?query=pipe
getStatusClasses(server: { instanceType: string, name: string, status: string, started: Date }) {
return {
'list-group-item-success': server.status === 'stable',
'list-group-item-warning': server.status === 'offline',
'list-group-item-danger': server.status === 'critical'
};
}
onAddServer() {
this.servers.push({
instanceType: 'small',
name: 'New Server',
status: 'stable',
started: new Date(15, 1, 2017)
})
}
}
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">
<input type="text" [(ngModel)]="filterStatus">
<br>
<button class="btn btn-primary" (click)="onAddServer()">
Add Server
</button>
<br>
<!-- Angular doesn't know the 'appStatus' Promise,
so we should make it know that using 'async' pipe option then Angular
would subscribes the Promise object and response as async mode-->
<h2>Status : {{ appStatus | async }}</h2>
<hr>
<ul class="list-group">
<li
class="list-group-item"
*ngFor="let server of servers | filter: filterStatus:'status'"
[ngClass]="getStatusClasses(server)">
<span
class="badge">
{{ server.status }}
</span>
<!-- "shorten" is a custom pipe -->
<strong>{{ server.name | shorten: 15 }}</strong> |
{{ server.instanceType | uppercase }} |
{{ server.started | date: 'fullDate' | uppercase }}
</li>
</ul>
</div>
</div>
</div>
<hr>
<app-assignment></app-assignment>
shorten.pipe.ts
import {Pipe, PipeTransform} from "@angular/core";
@Pipe({
name: 'shorten',
})
export class ShortenPipe implements PipeTransform{
// Rest parameters
// https://yamoo9.gitbook.io/typescript/ts-vs-es6/spread-default-reset
// transform(value: any, ...args: any[]): any {
// }
transform(value: any, limit: number) {
if (value.length > limit) {
return value.substr(0, limit) + ' ...';
}
return value;
}
}
filter.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
// If you want to use this pipe as async mode, you should add pure: false option in the pipe decoration.
// But this option would lead the user to performance issue.
@Pipe({
name: 'filter',
pure: false,
})
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;
}
}
We can make http requests with Angular. it's really useful when you need to request and response with RestAPI. For example you can request a backend server to gather some data we need with RestAPI (like Create, Read, Update, Delete, ..). Let's make the http request.
At first we made a firebase database called Realtime database. we can send our http requests to the database because the database provides RestAPI function in default.
We made PostService
to make http requests contain createAndStorePost
, fetchPosts
, onClearPosts
methodes.
in the createAndStorePost
we can store our input data with post
method. and the HttpClident
is made in observable
so we can subscribe that and show responseData
on console log. if we don't subscribe the HttpClient
, it would be not working because it would not control the response and then Angular regards the HttpClient
not be used. we can specify the response type with < {name: string} >
. it's totally optional but recommended. after that, I used my firebase url to store my data with post
request and made custom headers
and params
. We can make multiple params
also and specify responseType
what we want. we wrote responseData
with console.log
in the subscribe context and change the error
subject to the error.message
if the error occurs.
In the fetchPosts
, We used get
method to require some data we stored before. If you use pipe
you can change the reponse data to what you want. in the fetchPosts
we used map
to change the responseData
to what we want. we made postsArray
made by Post[]
type. the responseData
has key
so we check that with responseData.hasOwnProperty(key)
if it's true, we push the data({responseData[key], id: key}
) into postsArray
. we added custom field id
in the example. catchError
context is useful when you need to control error handling (like sending a request to handle the error..). if you subscribe the fetchPosts
in a component wihch uses the method, you can return the HttpClient
directly unlike createAndStorePost
which subscribe the HttpClient
in the service method. but you have to subscribe fetchPosts
in a component certainly.
In the onClearPosts
, we used observe
it gives us events
or response
.. and we can check the response data with tap
. tap
executes some codes we want without change the response data. so we can write the log on console.log
with tap
function.
app.component.html
<div class="container">
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3">
<form #postForm="ngForm" (ngSubmit)="onCreatePost(postForm.value)">
<div class="form-group">
<label for="title">Title</label>
<input
type="text"
class="form-control"
id="title"
required
ngModel
name="title"
/>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea
class="form-control"
id="content"
required
ngModel
name="content"
></textarea>
</div>
<button
class="btn btn-primary"
type="submit"
[disabled]="!postForm.valid"
>
Send Post
</button>
</form>
</div>
</div>
<hr />
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3">
<button class="btn btn-primary" (click)="onFetchPosts()">
Fetch Posts
</button>
|
<button
class="btn btn-danger"
[disabled]="loadedPosts.length < 1"
(click)="onClearPosts()"
>
Clear Posts
</button>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3">
<p *ngIf="loadedPosts.length < 1 && !isFetching">No posts available!</p>
<ul class="list-group" *ngIf="loadedPosts.length >= 1 && !isFetching">
<li class="list-group-item" *ngFor="let post of loadedPosts">
<h3>{{ post.title }}</h3>
<p>{{ post.content }}</p>
</li>
</ul>
<p *ngIf="isFetching && !error">Loading...</p>
<div class="alert alert-danger" *ngIf="error">
<h1>An Error Occurred!</h1>
<p>{{ error }}</p>
</div>
</div>
</div>
</div>
app.component.ts
import {Component, OnDestroy, OnInit} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {map} from "rxjs/operators";
import {Post} from "./post.model";
import {PostsService} from "./posts.service";
import {Subscription} from "rxjs";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
loadedPosts: Post[] = [];
isFetching = false;
error = null;
errorSub: Subscription;
constructor(private http: HttpClient, private postsService: PostsService) {
}
ngOnInit() {
this.errorSub = this.postsService.error.subscribe(
error => {
this.error = error;
}
)
// this.fetchPosts();
this.isFetching = true;
this.postsService.fetchPosts().subscribe(posts => {
this.isFetching = false;
this.loadedPosts = posts;
}, err => {
this.isFetching = false;
this.error = err.message;
})
}
onCreatePost(postData: { title: string; content: string }) {
// Send Http request
// console.log(postData);
// this.http.post< {name: string } >('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
// postData).subscribe(
// responseData => {
// console.log(responseData);
// }
// )
// Change the http request to service.
this.postsService.createAndStorePost(postData['title'], postData['content']);
}
onFetchPosts() {
// Send Http request
// this.fetchPosts();
this.isFetching = true;
this.postsService.fetchPosts().subscribe(posts => {
this.isFetching = false;
this.loadedPosts = posts;
}, err => {
this.isFetching = false;
this.error = err.message;
})
}
onClearPosts() {
// Send Http request
this.postsService.onClearPosts().subscribe(
() => {
this.loadedPosts = [];
}
);
}
ngOnDestroy(): void {
this.errorSub.unsubscribe();
}
// private fetchPosts() {
// this.isFetching = true;
// this.http
// .get<{[key: string]: Post}>('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json')
// .pipe(
// map(responseData => {
// const postsArray: Post[] = [];
// for (const key in responseData) {
// if (responseData.hasOwnProperty(key)) {
// // 펼침 연산자 : https://cotak.tistory.com/72
// postsArray.push({...responseData[key], id: key});
// }
// }
// return postsArray;
// })
// )
// .subscribe(posts => {
// console.log(posts);
// this.isFetching = false;
// this.loadedPosts = posts;
// }
// )
// }
onHandleError() {
this.error = null;
}
}
posts.service.ts
import {Injectable} from '@angular/core';
import {HttpClient, HttpEventType, HttpHeaders, HttpParams} from "@angular/common/http";
import {Post} from "./post.model";
import {catchError, map, tap} from "rxjs/operators";
import {Observable, Subject, throwError} from "rxjs";
@Injectable({
providedIn: 'root'
})
export class PostsService {
constructor(private http: HttpClient) { }
// loadedPosts = new Subject<Post[]>();
error = new Subject<string>();
createAndStorePost( title: string, content: string ) {
const postData: Post = {title: title, content: content};
// < {name : string} > means we will receive the response type {name: string}. it's totally optional but recommended
this.http.post< {name: string } >('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
postData,
{
// You can check the full response of our request like headers, status code, body..
// observe: "response",
}
).subscribe(
responseData => {
console.log(responseData);
}, error => {
this.error.next(error.message);
}
)
}
// at the below, we split the subscribe method to control the data we return.
// in the past, we used subject to control changed data but this time we use another way to solve it.
fetchPosts(): Observable<Post[]> {
let searchParams = new HttpParams();
searchParams = searchParams.append('print', 'pretty');
searchParams = searchParams.append('custom', 'key');
return this.http
.get<{[key: string]: Post}>(
'https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
{
headers: new HttpHeaders({'Custom-Header': 'Hello'}),
// params: new HttpParams().set('print', 'pretty'),
// If you want to apply multiple params, you can use below method.
params: searchParams,
responseType: "json",
}
)
.pipe(
map(responseData => {
const postsArray: Post[] = [];
console.log(responseData)
for (const key in responseData) {
if (responseData.hasOwnProperty(key)) {
// 펼침 연산자 : https://cotak.tistory.com/72
postsArray.push({...responseData[key], id: key});
}
}
return postsArray;
}), catchError(errorRes => {
// You can make logics when user faces the error
// sending error message to server or redirect user to another page..
console.log(errorRes);
return throwError(errorRes);
})
)
// .subscribe(posts => {
// // console.log(posts);
// }
// )
}
onClearPosts() {
return this.http
.delete('https://ng-complete-guide-b0c9e-default-rtdb.asia-southeast1.firebasedatabase.app/posts.json',
{
observe: 'events',
responseType: 'text',
}).pipe(tap(
events => {
console.log(events);
if (events.type === HttpEventType.Response) {
console.log(events.body);
}
if (events.type === HttpEventType.Sent) {
console.log('type: 0')
}
}
));
}
}
Interceptos sends outgoing request before passing it to the next interceptor in the chain. we can add or update the request data in the interceptor. if we need add an authentication header, we can add it into all requests with the interceptors.
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
import { AppComponent } from './app.component';
import {AuthInterceptorService} from "./auth-interceptor.service";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
}
],
bootstrap: [AppComponent]
})
export class AppModule {}
auth-interceptor.service.ts
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
export class AuthInterceptorService implements HttpInterceptor{
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request is on its way');
return next.handle(req);
}
}
Afterwards, we can check the message Request is on its way
in all of the requests like below.
If you want to add auth
header into all of the reuqests, you can make it like below.
auth-interceptor.service.ts
import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {tap} from "rxjs/operators";
export class AuthInterceptorService implements HttpInterceptor{
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Request is on its way');
console.log(req.url);
const modifiedRequest = req.clone({
headers: req.headers.append('Auth', 'xyz')
});
return next.handle(modifiedRequest).pipe(
tap(event => {
console.log(event);
if (event.type === HttpEventType.Response) {
console.log('Response arrived, body data: ');
console.log(event.body);
}
})
)
}
}
also you can handle the response data with tap
because it's made by observable
.
you can use multiple interceptors like below.
logging-interceptor.service.ts
import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {tap} from "rxjs/operators";
export class LoggingInterceptorService implements HttpInterceptor{
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
console.log('Outgoing request');
console.log(req.url);
return next.handle(req).pipe(
tap(event => {
if (event.type === HttpEventType.Response) {
console.log('Incoming response');
console.log(event.body);
}
})
);
}
}
auth=interceptor.service.ts
import {HttpEvent, HttpEventType, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {tap} from "rxjs/operators";
export class AuthInterceptorService implements HttpInterceptor{
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// console.log('Request is on its way');
// console.log(req.url);
const modifiedRequest = req.clone({
headers: req.headers.append('Auth', 'xyz')
});
return next.handle(modifiedRequest).pipe(
tap(event => {
// console.log(event);
if (event.type === HttpEventType.Response) {
// console.log('Response arrived, body data: ');
// console.log(event.body);
}
})
)
}
}
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule} from '@angular/common/http';
import { AppComponent } from './app.component';
import {AuthInterceptorService} from "./auth-interceptor.service";
import {LoggingInterceptorService} from "./logging-interceptor.service";
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, FormsModule, HttpClientModule],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
},
{
provide: HTTP_INTERCEPTORS,
useClass: LoggingInterceptorService,
multi: true,
},
],
bootstrap: [AppComponent]
})
export class AppModule {}
the recipe application uses data storage service like below :
data-storage.service.ts
import {Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {exhaustMap, map, take, tap} from 'rxjs/operators';
import {Recipe} from '../recipes/recipe.model';
import {RecipeService} from '../recipes/recipe.service';
import {AuthService} from "../auth/auth.service";
@Injectable({providedIn: 'root'})
export class DataStorageService {
constructor(private http: HttpClient,
private recipeService: RecipeService,
private authService: AuthService) {
}
storeRecipes() {
const recipes = this.recipeService.getRecipes();
this.http
.put(
'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json',
recipes
)
.subscribe(response => {
console.log(response);
});
}
fetchRecipes() {
// return this.authService.user.pipe(
// // take means we get values which we specified and unsubscribe after it.
// // like below, we gather 1 user and unsubscribe this.
// take(1),
//
// exhaustMap(user => {
// return this.http
// .get<Recipe[]>(
// 'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
// , {
// params: new HttpParams().set('auth', user.token)
// }
// );
// }), map(recipes => {
// return recipes.map(recipe => {
// return {
// ...recipe,
// ingredients: recipe.ingredients ? recipe.ingredients : []
// };
// });
// }),
// tap(recipes => {
// this.recipeService.setRecipes(recipes);
// })
// )
return this.http
.get<Recipe[]>(
'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
).pipe(map(recipes => {
return recipes.map(recipe => {
return {
...recipe,
ingredients: recipe.ingredients ? recipe.ingredients : []
};
});
}),
tap(recipes => {
this.recipeService.setRecipes(recipes);
})
)
// return this.http
// .get<Recipe[]>(
// 'https://ng-course-recipe-book-b669a-default-rtdb.asia-southeast1.firebasedatabase.app/recipes.json'
// )
// .pipe(
// map(recipes => {
// return recipes.map(recipe => {
// return {
// ...recipe,
// ingredients: recipe.ingredients ? recipe.ingredients : []
// };
// });
// }),
// tap(recipes => {
// this.recipeService.setRecipes(recipes);
// })
// )
}
}
You can make an authentication process with Angular. with this authentication function, you can make users login Angular web application. when users login to the Angular, the Angular will send login request with id
and password
to a backend system which checks the reuqest is correct or not. you should add the endpoint of the auth component in routing module with auth
.
You can make form auth component which has signup and login forms users can login or sginup to. Auth component will send the request data to an Auth service which has various function related to authentication. the Auth service has signup
, login
, autoLogin
, autoLogout
methods which use HttpClient
library to communicate with a backend application connected with the Angular application. in the signup
, login
methods, you can use observable
and pipe
function to request and response the login or signup data and alter the response data. pipe
function is very efficient to alter the response data as it is the observable
response data. if you want to control error
or alter, update the response data, you can do that with pipe
function. there are so many techniques to use the pipe
function.
After login to the Angular application, you should add a login token which responsed from the backend application to all of reuqests which user make with an interceptor function. the interceptor can add headers or parameters. the intercept
method use pipe
with take(1)
which takes 1 data and exhaustMap
which make the modifiedReq
that has auth token in the header of the request. you should add provider
of the interceptor in the app module of the Angular.
auth.component.html
<div class="row">
<div class="col-xs-12 col-md-6 col-md-offset-3">
<div class="alert alert-danger" *ngIf="error">
<p>
{{ error }}
</p>
</div>
<div *ngIf="isLoading" style="text-align: center">
<app-loading-spinner></app-loading-spinner>
</div>
<form #authForm="ngForm" (ngSubmit)="onSubmit(authForm)" *ngIf="!isLoading">
<div class="form-group">
<label for="email">Email</label>
<input type="email"
id="email"
class="form-control"
ngModel
required
email
name="email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password"
id="password"
class="form-control"
ngModel
required
minlength="6"
name="password">
</div>
<div>
<button class="btn btn-primary"
[disabled]="!authForm.valid"
type="submit">{{ isLoginMode ? 'Login' : 'Sign Up' }}
</button> |
<button class="btn btn-primary"
(click)="onSwitchMode()"
type="button">{{ isLoginMode ? 'Switch to Sign Up' : 'Switch to Login' }}</button>
</div>
</form>
</div>
</div>
auth.component.ts
import {Component} from "@angular/core";
import {NgForm} from "@angular/forms";
import {AuthResponseData, AuthService} from "./auth.service";
import {Observable} from "rxjs";
import {Router} from "@angular/router";
@Component({
selector: 'app-auth',
templateUrl: './auth.component.html'
})
export class AuthComponent {
isLoginMode = true;
isLoading = false;
error: string = null;
constructor(private authService: AuthService,
private router: Router) {
}
onSwitchMode() {
this.isLoginMode = !this.isLoginMode;
}
onSubmit(form: NgForm) {
if (!form.valid) {
return;
}
const email = form.value.email;
const password = form.value.password;
// repeat code is improved with authObs
let authObs: Observable<AuthResponseData>;
this.isLoading = true;
if (this.isLoginMode) {
authObs = this.authService.login(email, password)
} else {
authObs = this.authService.signup(email, password)
}
// authObs works like below! (we don't use repeated code anymore)
authObs.subscribe(
resData => {
console.log(resData);
this.isLoading = false;
this.error = null;
this.router.navigate(['/recipes']);
}, errorMessage => {
console.log(errorMessage);
this.error = errorMessage;
this.isLoading = false;
}
)
form.reset();
this.authService.user.subscribe(
user => {
console.log(user);
}
);
}
}
app-routing.module.ts
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 {RecipeStartComponent} from './recipes/recipe-start/recipe-start.component';
import {RecipeDetailComponent} from './recipes/recipe-detail/recipe-detail.component';
import {RecipeEditComponent} from './recipes/recipe-edit/recipe-edit.component';
import {RecipesResolverService} from "./recipes/recipes-resolver.service";
import {AuthComponent} from "./auth/auth.component";
import {AuthGuard} from "./auth/auth.guard";
const appRoutes: Routes = [
{path: '', redirectTo: '/recipes', pathMatch: 'full'},
{
path: 'recipes',
component: RecipesComponent,
canActivate: [AuthGuard],
children: [
{path: '', component: RecipeStartComponent},
{path: 'new', component: RecipeEditComponent},
{path: ':id', component: RecipeDetailComponent, resolve: [RecipesResolverService]},
{path: ':id/edit', component: RecipeEditComponent, resolve: [RecipesResolverService]},
]
},
{path: 'shopping-list', component: ShoppingListComponent},
{path: 'auth', component: AuthComponent}
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}
auth-interceptor.service.ts
import {Injectable} from "@angular/core";
import {HttpEvent, HttpHandler, HttpInterceptor, HttpParams, HttpRequest} from "@angular/common/http";
import {Observable} from "rxjs";
import {AuthService} from "./auth.service";
import {exhaustMap, take} from "rxjs/operators";
@Injectable()
export class AuthInterceptorService implements HttpInterceptor{
constructor(private authService: AuthService) {
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.user.pipe(
take(1),
exhaustMap(user=> {
if (!user) {
return next.handle(req);
}
const modifiedReq = req.clone({
params: new HttpParams().set('auth', user.token)
});
return next.handle(modifiedReq);
})
);
}
}
app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeaderComponent } from './header/header.component';
import { RecipesComponent } from './recipes/recipes.component';
import { RecipeListComponent } from './recipes/recipe-list/recipe-list.component';
import { RecipeDetailComponent } from './recipes/recipe-detail/recipe-detail.component';
import { RecipeItemComponent } from './recipes/recipe-list/recipe-item/recipe-item.component';
import { ShoppingListComponent } from './shopping-list/shopping-list.component';
import { ShoppingEditComponent } from './shopping-list/shopping-edit/shopping-edit.component';
import { DropdownDirective } from './shared/dropdown.directive';
import { ShoppingListService } from './shopping-list/shopping-list.service';
import { AppRoutingModule } from './app-routing.module';
import { RecipeStartComponent } from './recipes/recipe-start/recipe-start.component';
import { RecipeEditComponent } from './recipes/recipe-edit/recipe-edit.component';
import { RecipeService } from './recipes/recipe.service';
import {DataStorageService} from "./shared/data-storage.service";
import {HTTP_INTERCEPTORS, HttpClientModule} from "@angular/common/http";
import {AuthComponent} from "./auth/auth.component";
import {LoadingSpinnerComponent} from "./shared/loading-spinner/loading-spinner.component";
import {AuthInterceptorService} from "./auth/auth-interceptor.service";
@NgModule({
declarations: [
AppComponent,
HeaderComponent,
RecipesComponent,
RecipeListComponent,
RecipeDetailComponent,
RecipeItemComponent,
ShoppingListComponent,
ShoppingEditComponent,
DropdownDirective,
RecipeStartComponent,
RecipeEditComponent,
AuthComponent,
LoadingSpinnerComponent,
],
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule,
AppRoutingModule,
// It's really important to use HttpClient module!
HttpClientModule,
],
providers: [ShoppingListService, RecipeService,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptorService,
multi: true,
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Finally we should control the routing table users can visit or not with guard function. the guard check users are authenticated or not and if users are not login status the guard returns /auth
url. if the users are authenticated the guard returns correct url users want to visit with login privilege. the guard should be added into the endpoints which need the login privilege of routing module.
auth.guard.ts
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from "@angular/router";
import {Observable} from "rxjs";
import {Injectable} from "@angular/core";
import {AuthService} from "./auth.service";
import {map, take, tap} from "rxjs/operators";
@Injectable({
providedIn: 'root'
})
export class AuthGuard implements CanActivate {
constructor(private authService: AuthService,
private router: Router) {
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
// return this.authService.user.pipe(map(user => {
// // if user is valid return true, not valid return false.
// return !!user;
// }), tap(isAuth => {
// if (!isAuth) {
// this.router.navigate(['/auth']);
// }
// }))
return this.authService.user.pipe(
take(1),
map(user => {
// if user is valid return true, not valid return false.
const isAuth = !!user;
if (isAuth) {
return true;
}
// it's more advanced version than above code.
return this.router.createUrlTree(['/auth']);
})
)
}
}
app-routing.module.ts
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 {RecipeStartComponent} from './recipes/recipe-start/recipe-start.component';
import {RecipeDetailComponent} from './recipes/recipe-detail/recipe-detail.component';
import {RecipeEditComponent} from './recipes/recipe-edit/recipe-edit.component';
import {RecipesResolverService} from "./recipes/recipes-resolver.service";
import {AuthComponent} from "./auth/auth.component";
import {AuthGuard} from "./auth/auth.guard";
const appRoutes: Routes = [
{path: '', redirectTo: '/recipes', pathMatch: 'full'},
{
path: 'recipes',
component: RecipesComponent,
canActivate: [AuthGuard],
children: [
{path: '', component: RecipeStartComponent},
{path: 'new', component: RecipeEditComponent},
{path: ':id', component: RecipeDetailComponent, resolve: [RecipesResolverService]},
{path: ':id/edit', component: RecipeEditComponent, resolve: [RecipesResolverService]},
]
},
{path: 'shopping-list', component: ShoppingListComponent},
{path: 'auth', component: AuthComponent}
];
@NgModule({
imports: [RouterModule.forRoot(appRoutes)],
exports: [RouterModule]
})
export class AppRoutingModule {
}