Last active
August 23, 2021 14:08
-
-
Save bbrt3/af0a27be402aacb8a61dd2d53b41216b to your computer and use it in GitHub Desktop.
Angular
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
List of Angular elements: | |
a) component | |
b) directive | |
c) module | |
d) pipe | |
e) service | |
- interceptors | |
- resolvers | |
f) guard | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app-multi-projection.ts | |
import { Component, OnInit } from '@angular/core'; | |
@Component({ | |
selector: 'app-multi-projection', | |
templateUrl: './multi-projection.component.html', | |
styleUrls: ['./multi-projection.component.scss'] | |
}) | |
export class MultiProjectionComponent implements OnInit { | |
constructor() { } | |
ngOnInit(): void { | |
} | |
} | |
// app-multi-projection.html | |
<h2>Multiple slot projection</h2> | |
<ng-content select="[question]"></ng-content> | |
<ng-content select="[answer]"></ng-content> | |
<ng-content select="#title"></ng-content> | |
<ng-content select=".word"></ng-content> | |
<ng-content></ng-content> | |
// app.component.html | |
<app-multiple-projection> | |
<p question>You sure it's working?</p> | |
<p answer>Yeah dude, trust me.</p> | |
<h3 id="title">extra text yo</h3> | |
<h5 class=".word">ME TOO? </h5> | |
take me home | |
</app-multiple-projection> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// duration.pipe.ts | |
import { Pipe, PipeTransform } from '@angular/core'; | |
@Pipe({ | |
name: 'duration' | |
}) | |
export class DurationPipe implements PipeTransform { | |
transform(value: number): string { | |
switch(value) { | |
case 1: | |
return 'Half hour'; | |
case 2: | |
return 'One hour'; | |
case 3: | |
return 'Half day'; | |
case 4: | |
return 'Full day'; | |
default: | |
return value.toString(); | |
} | |
} | |
} | |
// app.module.ts | |
import { DurationPipe } from '...' | |
declarations: [ | |
DurationPipe | |
]; | |
// app.html | |
{{ sessionStorage.duration || duration }} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
There are three types of directives in Angular: | |
a) Component directives | |
b) Structural directives | |
c) Attribute directives | |
These directives can be used to change the behavior | |
or appearance of DOM elements by applying inline properties | |
along with the appropriate values. | |
There are some built-in ones, but we can create our own. | |
COMPONENT DIRECTIVE | |
It's a class with the @Component decorator attached. | |
Angular application should have at least one component, root. | |
It is used to attach the template and styles along with the component class, | |
and it gives us a flexible way to define components along with the template and stylesheets. | |
Creating component: | |
ng g c | |
Component structure: | |
- app.component.ts | |
- app.component.html | |
- app.component.css | |
Together they form single unit called a component. | |
*/ | |
@Component({ | |
selector: 'my-app', | |
templateUrl: './app.component.html', | |
styleUrls: [ './app.component.css' ] | |
}) | |
export class lol {} | |
/* | |
STRUCTURAL DIRECTIVE | |
Those are used to manipulate the DOM behavior only. | |
More specifically, we can say that they are used to | |
create or destroy the different DOM elements. | |
Built-in structural directives: | |
- NgIf | |
- NgFor | |
- NgSwitch | |
*/ | |
import { Component } from "@angular/core"; | |
@Component({ | |
selector: "app-ng-if-directive", | |
templateUrl: "./ng-if-directive.component.html", | |
styleUrls: ["./ng-if-directive.component.css"] | |
}) | |
export class NgIfDirectiveComponent { | |
isDivVisible: boolean = true; | |
} | |
<div *ngIf='isDivVisible'> | |
This DIV is visible using ngIf condition | |
</div> | |
/* | |
Differences between component and structural directives: | |
The component directive is just a directive that attaches | |
the template and style for the element, along with the specific behavior. | |
The structural directive modifies the DOM element and its behavior | |
by adding/changing/removing different elements. | |
Component directives use the @Component decorator and require | |
a separate view for the component. | |
Structural directives are built-in and only focus on the DOM elements. | |
Component directive can be created multiple times. | |
We cannot apply more than one structura directive to the same HTML element. | |
*/ | |
/* | |
ATTRIBUTE DIRECTIVES | |
They allow you to change the appearance or behavior of DOM elements | |
and Angular components. | |
ng generate directive name | |
*/ | |
// SIMPLE ATTRIBUTE DIRECTIVE WITH NO DATA TRANSFER | |
import { Directive, ElementRef } from '@angular/core'; | |
@Directive({ | |
selector: '[appHighlight]' | |
}) | |
export class HighlightDirective { | |
constructor(el: ElementRef) { | |
el.nativeElement.style.backgroundColor = 'yellow'; | |
} | |
} | |
<p appHighlight>Highlight me!</p> | |
// ATTRIBUTE DIRECTIVE WITH PASSING DATA TO IT | |
import { Directive, ElementRef, HostListener, Input } from '@angular/core'; | |
@Directive({ | |
selector: '[appHighlight]' | |
}) | |
export class HighlightDirective { | |
@Input() appHighlight = ''; | |
constructor(private el: ElementRef) { } | |
@HostListener('mouseenter') onMouseEnter() { | |
this.highlight('yellow'); | |
} | |
@HostListener('mouseleave') onMouseLeave() { | |
this.highlight(''); | |
} | |
private highlight(color: string) { | |
this.el.nativeElement.style.backgroundColor = color; | |
} | |
} | |
export class AppComponent { | |
color = 'yellow'; | |
} | |
<p [appHighlight]="color">Highlight me!</p> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@NgModule({ | |
imports: [ | |
// by using forRoot we are making sure that BsDatepickerModule's services are being loaded too! | |
// | |
BsDatepickerModule.forRoot(), | |
], | |
declarations: [AppComponent], | |
bootstrap: [AppComponent] | |
}) | |
export class AppModule { } | |
@NgModule({ | |
imports: [ | |
// by importing BsDatepickerModule we are making sure | |
// that components directives will be loaded | |
// those are PRIVATE | |
// which means we if we want to use them in different components | |
// we have to import them in each component | |
BsDatepickerModule, | |
], | |
declarations: [OrderComponent], | |
}) | |
export class OrderModule { } | |
// forRoot should be used only in main app.module | |
// because it sets main injector!! | |
/* | |
forChild makes sure our module uses its own injector and lazy loading | |
It's injector will be a child of root injector. | |
When does it make sense to use forChild? | |
a) if service is going to work different in our lazy module | |
b) if we want to let service access some data (configuration for example) | |
RouterModule passes array of routes this way!! | |
With router module it makes sense to use forChild in feature components, | |
to make sure that we don't create another Router services instances | |
and use one from main injector instead! | |
Alternative to using forRoot: | |
*/ | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class AuthorizationService { | |
} | |
/* | |
This decorator's parameter makes the need of adding service to providers array | |
obsolete. | |
Importing module with that parameter will result in adding decorated service | |
to main injector. | |
https://www.p-programowanie.pl/angular/dzialanie-metod-forroot-forchild | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// parent.ts | |
import { Component } from '@angular/core' | |
@Component({ | |
selector: 'app-root', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.scss'], | |
}) | |
export class AppComponent { | |
title = 'content-projection' | |
// data we will be sending to child | |
testArr: string[] = ['a', 'b', 'c', 'd', 'e'] | |
} | |
// parent.html | |
<app-input-test [testInfo]="testArr"></app-input-test> | |
// child.ts | |
import { Component, Input, OnInit } from '@angular/core' | |
@Component({ | |
selector: 'app-input-test', | |
templateUrl: './input-test.component.html', | |
styleUrls: ['./input-test.component.scss'], | |
}) | |
export class InputTestComponent implements OnInit { | |
// variable in which we will store our passed data | |
@Input() | |
public testInfo?: string[]; | |
constructor() {} | |
ngOnInit(): void {} | |
} | |
// child.html | |
// displaying data that we got from parent | |
// only if we have received it | |
<div *ngIf="testInfo"> | |
<h2 *ngFor="let item of testInfo"> | |
{{ item }} | |
</h2> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// child.ts | |
import { Component, EventEmitter, OnInit, Output } from '@angular/core' | |
@Component({ | |
selector: 'app-input-test', | |
templateUrl: './input-test.component.html', | |
styleUrls: ['./input-test.component.scss'], | |
}) | |
export class InputTestComponent { | |
@Output() | |
trash: EventEmitter<string> = new EventEmitter<string>() | |
addToParentList(item: string) { | |
this.trash.emit(item) | |
} | |
} | |
// child.html | |
// on clicking enter we will be emitting our eventEmitter with value typed in input | |
// we also made use of template variable here to get the value itself from input | |
<input #data type="text" (keydown.enter)="addToParentList(data.value)" /> | |
<div *ngIf="testInfo"> | |
<h2 *ngFor="let item of testInfo"> | |
{{ item }} | |
</h2> | |
</div> | |
// parent.ts | |
import { Component } from '@angular/core' | |
@Component({ | |
selector: 'app-root', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.scss'], | |
}) | |
export class AppComponent { | |
title = 'content-projection' | |
testArr: string[] = ['a', 'b', 'c', 'd', 'e'] | |
// this method will be adding new elements to array above | |
// new element value is based on that emitted by child | |
addToList(data: string) { | |
this.testArr.push(data) | |
// SMOOTH SCROLLING! | |
window.scrollTo({ | |
left: 0, | |
top: document.body.clientHeight, | |
behavior: 'smooth', | |
}) | |
} | |
} | |
// parent.html | |
// we specify what happens after emit has been detected | |
<app-input-test | |
[testInfo]="testArr" | |
(trash)="addToList($event)" | |
></app-input-test> | |
<div class="here"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// child.ts | |
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; | |
@Component({ | |
selector: 'app-two-way-binding', | |
templateUrl: './two-way-binding.component.html', | |
styleUrls: ['./two-way-binding.component.css'] | |
}) | |
export class TwoWayBindingComponent { | |
// no extra details needed inside brackets () | |
@Input() | |
size!: number | string; | |
// Change is crucial! | |
@Output() | |
sizeChange: EventEmitter<number> = new EventEmitter<number>(); | |
// making font-size smaller | |
dec() { | |
this.resize(-1); | |
} | |
// making font-size bigger | |
inc(){ | |
this.resize(+1); | |
} | |
// changing font-size and emitting it in our event emitter | |
resize(delta: number){ | |
this.size = Math.min(144, Math.max(8, +this.size + delta)); | |
this.sizeChange.emit(this.size); | |
} | |
} | |
// child.html | |
<div> | |
<button (click)="dec()" title="smaller">-</button> | |
<button (click)="inc()" title="bigger">+</button> | |
<label [style.font-size.px]="size">FontSize: {{size}}px</label> | |
</div> | |
// parent.ts | |
import { Component } from '@angular/core'; | |
@Component({ | |
selector: 'app-root', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.css'] | |
}) | |
export class AppComponent { | |
// parent element whose value is going to change after emitting event to assigned value | |
fontSizePx: number = 16; | |
} | |
// parent.html | |
// [(size)]="fontSizePx" => two way binding | |
// (sizeChange)="fontSizePx=$event" => assigning value of emitted event to our font-size | |
<app-two-way-binding [(size)]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-two-way-binding> | |
<div [style.font-size.px]="fontSizePx">Resizable text</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// blue-checker.validator.ts | |
import {AbstractControl, ValidatorFn} from '@angular/forms'; | |
// we have to export a function | |
// its name will be identifying our validator name | |
// it contains validation logic | |
export function blue(): ValidatorFn { | |
return (control: AbstractControl): { [key: string]: any } | null => | |
control.value?.toLowerCase() === 'blue' | |
? null : {blue: control.value}; | |
} | |
// app.component.ts | |
import { Component, OnInit } from '@angular/core'; | |
import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | |
// we have to import our exported function that specifies validator | |
import { blue } from './validators/blue-checker.validator'; | |
@Component({ | |
selector: 'app-root', | |
templateUrl: './app.component.html', | |
styleUrls: ['./app.component.css'] | |
}) | |
export class AppComponent implements OnInit { | |
// formGroup that will identify our form from html | |
testForm!: FormGroup; | |
// we need to inject formBuilder to build our form | |
constructor(private readonly formBuilder: FormBuilder){} | |
ngOnInit(): void { | |
// here we define fields that our formGroup will contain | |
// validators specifies array of used validators | |
// we can use built-in validators and our own | |
this.testForm = this.formBuilder.group({ | |
name: ['', {validators: [Validators.required, blue()]}] | |
}); | |
} | |
// this function will help us check if validation went successful | |
getErrorMessage(){ | |
// we get field value from field called name | |
const field = this.testForm.get('name'); | |
let output: string = ''; | |
// checking if our validator validation went successful | |
if (field?.hasError('blue')){ | |
output += '\nNOT BLUE\n'; | |
} | |
// checking state of built-in validator | |
if (field?.hasError('required')){ | |
output += '\nTHIS FIELD IS REQUIRED!!\n'; | |
} | |
// returning output that will be displayed after form | |
return output; | |
} | |
} | |
// app.component.html | |
// we have to specify formGroup that will bind it to our form from .ts file | |
<form [formGroup]="testForm"> | |
<label>Name</label> | |
// here we bind our field called name to form | |
<input formControlName="name" type=text/> | |
<div> | |
// if form is invalid then we will display correct messages | |
<label *ngIf="testForm.invalid">{{getErrorMessage()}}</label> | |
</div> | |
</form> | |
// app.module.ts | |
imports: [ | |
// this is crucial! | |
ReactiveFormsModule | |
] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// error-handler.interceptor.ts | |
import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; | |
import { Injectable } from '@angular/core'; | |
import { Observable, of, throwError } from 'rxjs'; | |
import { catchError } from 'rxjs/operators' | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class ErrorHandlerInterceptor implements HttpInterceptor { | |
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |
return next.handle(req) | |
.pipe( | |
catchError(error => { | |
let errorMsg: string; | |
if (error.error instanceof ErrorEvent){ | |
errorMsg = `Error: ${error.error.message}`; | |
} else { | |
errorMsg = this.getServerErrorMessage(error); | |
} | |
return throwError(errorMsg); | |
}) | |
); | |
} | |
private getServerErrorMessage(error: HttpErrorResponse): string { | |
switch (error.status) { | |
case 404: { | |
return `Not Found: ${error.message}`; | |
} | |
case 403: { | |
return `Access Denied: ${error.message}`; | |
} | |
case 500: { | |
return `Internal Server Error: ${error.message}`; | |
} | |
default: { | |
return `Unknown Server Error: ${error.message}`; | |
} | |
} | |
} | |
} | |
// app.module.ts | |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | |
providers: [ | |
{ provide: HTTP_INTERCEPTORS, useClass: ErrorHandlerInterceptor, multi: true } | |
] | |
// handling thrown errors on other components side | |
this.dataService.getAllReaders() | |
.subscribe( | |
(data: Reader[] | BookTrackerError) => this.allReaders = <Reader[]>data, | |
(err: BookTrackerError) => this.loggerService.log(err.friendlyMessage), | |
() => this.loggerService.log('All done getting readers!') | |
); | |
this.mostPopularBook = this.dataService.mostPopularBook; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {catchError} from '@rxjs/operators'; | |
import {Observable} from '@rxjs'; | |
class BookTrackerErrorHandlerService{ | |
getAllBooks(): Observable<Book[]> | BookTrackerError>{ | |
return this.http.get<Book[]>('api/errors/500') | |
.pipe( | |
catchError(err => this.handleHttpError(err)) | |
); | |
} | |
private handleHttpError(error: HttpErrorResponse): Observable<BookTrackerError>{ | |
let dataError = new BookTrackerError(); | |
dataError.errorNumber = 100; | |
dataError.message = error.statusText; | |
dataError.friendlyMessage = 'An error occurred retrieving data.'; | |
} | |
} | |
// app.module.ts | |
@NgModule({ | |
... | |
providers: [ | |
{provide: ErrorHandler, useClass: BookTrackerErrorHandlerService } | |
] | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// http-cache.service.ts | |
import { HttpResponse } from '@angular/common/http'; | |
private requests: any = {}; | |
put(url: string, response: HttpResponse<any>): void { | |
this.requests[url] = response; | |
} | |
get(url: string): HttpResponse<any> | undefined { | |
return this.requests[url]; | |
} | |
invalidateUrl(url: string): void{ | |
this.requests[url] = undefined; | |
} | |
invalidateCache(): void { | |
this.requests = {}; | |
} | |
// cache.interceptor.ts | |
import { Injectable } from "@angular/core"; | |
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse } from "@angular/common/http"; | |
import { Observable, of } from "rxjs"; | |
import { tap } from "rxjs/operators"; | |
import { HttpCacheService } from "./http-cache.service"; | |
@Injectable() | |
export class CacheInterceptor implements HttpInterceptor | |
{ | |
constructor(private readonly _cacheService: HttpCacheService){ | |
} | |
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { | |
// pass along non-cacheable requests and invalidate cache | |
if (req.method !== 'GET'){ | |
console.log(`Invalidating cache: ${req.method} ${req.url}`); | |
this._cacheService.invalidateCache(); | |
return next.handle(req); | |
} | |
// attempt to retrieve a cached response | |
const cachedResponse: HttpResponse<any> = <HttpResponse<any>>this._cacheService.get(req.url); | |
// return cached response | |
if (cachedResponse){ | |
console.log(`Returning a cached response: ${cachedResponse.url}`); | |
console.log(cachedResponse); | |
return of(cachedResponse); | |
} | |
// send request to server and add response to cache | |
return next.handle(req) | |
.pipe( | |
tap(event => { | |
if (event instanceof HttpResponse){ | |
console.log(`Adding item to cache: ${req.url}`); | |
this._cacheService.put(req.url, event); | |
} | |
}) | |
) | |
} | |
} | |
// app.module.ts | |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | |
import { CacheInterceptor } from './core/cache.interceptor'; | |
providers: [ | |
{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true } | |
], |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Interceptors are services that implement HttpInterceptor interface | |
// They let us to manipulate HTTP reequest before they're sent to the server | |
// They also let us manipulate HTTP responses before they're returned to our app | |
// They are useful for: | |
// handling errors | |
// adding headers to all requests | |
// logging | |
// reporting progress of events | |
// client-side catching | |
export class JsonHeaderInterceptor implements HttpInterceptor{ | |
// req: outgoing request, next: next interceptor in the chain of interceptors or client to get back to | |
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{ | |
let jsonReq: HttpRequest<any> = req.clone({ | |
setHeaders: { 'Content-Type': 'application/json' } | |
}); | |
return next.handle(jsonReq) | |
.pipe( | |
.tap(event => { | |
if (event instanceof HttpResponse) { | |
// modify the HttpResponse here | |
} | |
}) | |
} | |
} | |
// app.module.ts | |
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; | |
import { JsonHeaderInterceptor } from '...' | |
@NgModule({ | |
... | |
providers: [ | |
// ORDER MATTERS | |
// FIRST GET REQUEST THEN PASSES IT TO NEXT ONE | |
{provide: HTTP_INTERCEPTORS, useClass: JsonHeaderInterceptor, multi: true } | |
] | |
}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// interceptor | |
export const OPTION_1 = new HttpContextToken<number>(() => 42); | |
// service | |
let my_context: HttpContext = new HttpContext(); | |
my_context.set(OPTION_1, 13); | |
this.http.get('/api/books', { | |
context: my_context | |
// context: new HttpContext().set(CONTENT_TYPE, 'application/xml') | |
}); | |
// interceptor | |
let first_option: number = req.context.get<number>(OPTION_1); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// books.resolver.service.ts | |
import { Injectable } from '@angular/core'; | |
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; | |
import { Observable, of } from 'rxjs'; | |
import { catchError } from 'rxjs/operators'; | |
import { Book } from 'app/models/book'; | |
import { DataService } from 'app/core/data.service'; | |
import { BookTrackerError } from 'app/models/bookTrackerError'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
export class BooksResolverService implements Resolve<Book[] | BookTrackerError>{ | |
constructor(private dataService: DataService) { } | |
resolve(route : ActivatedRouteSnapshot, state: RouterStateSnapshot) : Observable<Book[] | BookTrackerError>{ | |
return this.dataService.getAllBooks() | |
.pipe( | |
catchError(err => of(err)) | |
); | |
} | |
} | |
// app-routing.module.ts | |
const routes: Routes = [ | |
{ path: 'dashboard', component: DashboardComponent, resolve: {resolvedBooks: BooksResolverService } } | |
]; | |
// dashboard.component.ts | |
import { ActivatedRoute } from '@angular/router'; | |
constructor( | |
private readonly route: ActivatedRoute | |
) | |
ngOnInit(){ | |
let resolvedData: Book[] | BookTrackerError = this.route.snapshot.data['resolvedBooks']; | |
// typeof resolvedData === 'string' | |
if (resolvedData instanceof BookTrackerError){ | |
// something | |
} | |
else{ | |
this.allBooks = resolvedData; | |
} | |
} | |
// we dont need to use the service directly when using resolver!! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ngOnInit(){ | |
this.dataService.getAllBooks() | |
.subscribe( // ensures we make api call | |
(data: Book[]) => this.allBooks = data, // what happens to the returned observable | |
(err : any) => console.log(err), // error logging | |
() => console.log('All done!') // what is done after completion of request | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import {map, tap} from '@rxjs/operators' | |
getOldBookById(id: number): Observable<OldBook>{ | |
return this.http.get<Book>(`/api/books/${id}`) | |
.pipe( | |
map(b => <OldBook>{ | |
bookTitle: b.title, | |
year: b.publicationYear | |
}), | |
tap(classicBook => console.log(classicBook)) | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app.form.ts | |
function emailMatcher(c: AbstractControl): { [key: string]: boolean } | null { | |
const emailControl = c.get('email'); | |
const confirmControl = c.get('confirmEmail'); | |
if (emailControl.pristine || confirmControl.pristine) { | |
return null; | |
} | |
if (emailControl.value === confirmControl.value) { | |
return null; | |
} | |
return { match: true }; | |
} | |
ngOnInit() { | |
this.customerForm = this.fb.group({ | |
firstName: ['', [Validators.required, Validators.minLength(3)]], | |
lastName: ['', [Validators.required, Validators.maxLength(50)]], | |
emailGroup: this.fb.group({ | |
email: ['', [Validators.required, Validators.email]], | |
confirmEmail: ['', Validators.required], | |
}, { validator: emailMatcher }), | |
phone: '', | |
notification: 'email', | |
rating: [null, ratingRange(1, 5)], | |
sendCatalog: true | |
}); | |
} | |
// app.form.html | |
<div formGroupName="emailGroup"> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" | |
for="emailId">Email</label> | |
<div class="col-md-8"> | |
<input class="form-control" | |
id="emailId" | |
type="email" | |
placeholder="Email (required)" | |
formControlName="email" | |
[ngClass]="{'is-invalid': customerForm.get('emailGroup').errors || | |
((customerForm.get('emailGroup.email').touched || | |
customerForm.get('emailGroup.email').dirty) && | |
!customerForm.get('emailGroup.email').valid) }" /> | |
<span class="invalid-feedback"> | |
<span *ngIf="customerForm.get('emailGroup.email').errors?.required"> | |
Please enter your email address. | |
</span> | |
<span *ngIf="customerForm.get('emailGroup.email').errors?.email"> | |
Please enter a valid email address. | |
</span> | |
</span> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" | |
for="confirmEmailId">Confirm Email</label> | |
<div class="col-md-8"> | |
<input class="form-control" | |
id="confirmEmailId" | |
type="email" | |
placeholder="Confirm Email (required)" | |
formControlName="confirmEmail" | |
[ngClass]="{'is-invalid': customerForm.get('emailGroup').errors || | |
((customerForm.get('emailGroup.confirmEmail').touched || | |
customerForm.get('emailGroup.confirmEmail').dirty) && | |
!customerForm.get('emailGroup.confirmEmail').valid) }" /> | |
<span class="invalid-feedback"> | |
<span *ngIf="customerForm.get('emailGroup.confirmEmail').errors?.required"> | |
Please confirm your email address. | |
</span> | |
<span *ngIf="customerForm.get('emailGroup').errors?.match"> | |
The confirmation does not match the email address. | |
</span> | |
</span> | |
</div> | |
</div> | |
</div> | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { AbstractControl, ValidatorFn } from '@angular/forms'; | |
// in order to be able to use parameters we need to wrap up our validator function | |
// in wrapper factory function that takes in parameters | |
// and then use arrow function for validation logic and use parameters inside of it | |
function ratingRange(min: number, max: number): ValidatorFn { | |
return (c: AbstractControl): { [key: string]: boolean } | null => { | |
if (c.value !== null && (isNaN(c.value) || c.value < min || c.value > max)) { | |
// failed validation | |
// 'error': value | |
return { 'range': true }; | |
} | |
// successful validation | |
return null; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app.component.ts | |
// this property will be used to return instance of FormArray containing our addresses | |
get addresses(): FormArray { | |
return this.customerForm.get('addresses') as FormArray; | |
} | |
ngOnInit() { | |
this.customerForm = this.fb.group({ | |
firstName: ['', [Validators.required, Validators.minLength(3)]], | |
lastName: ['', [Validators.required, Validators.maxLength(50)]], | |
emailGroup: this.fb.group({ | |
email: ['', [Validators.required, Validators.email]], | |
confirmEmail: ['', Validators.required], | |
}, { validator: emailMatcher }), | |
phone: '', | |
notification: 'email', | |
rating: [null, ratingRange(1, 5)], | |
sendCatalog: true, | |
// here we declare an FormArray that in the beginning | |
// contains only one default address entry for user to fill in | |
addresses: this.fb.array([this.buildAddress()]) | |
}); | |
// this method uses getter of address to | |
// add new address to an array of addresses | |
addAddress(): void { | |
this.addresses.push(this.buildAddress()); | |
} | |
// this method is used to create new FormGroup | |
// for storing new addreses that we will be adding | |
buildAddress(): FormGroup { | |
return this.fb.group({ | |
addressType: 'home', | |
street1: '', | |
street2: '', | |
city: '', | |
state: '', | |
zip: '' | |
}); | |
} | |
// app.component.html | |
<div *ngIf="customerForm.get('sendCatalog').value"> | |
// here we setup our FormArray name | |
<div formArrayName="addresses"> | |
// and we set FormGroup name to index of current address control | |
// using loop with index enabled | |
// that is because array uses indexes to iterate over next elements | |
// so each element inside our array will be another address | |
<div [formGroupName]="i" | |
*ngFor="let address of addresses.controls; let i=index"> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label pt-0">Address Type</label> | |
<div class="col-md-8"> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="home" | |
formControlName="addressType"> Home | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="work" | |
formControlName="addressType"> Work | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="other" | |
formControlName="addressType"> Other | |
</label> | |
</div> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" | |
attr.for="{{'street1Id' + i}}">Street Address 1</label> | |
<div class="col-md-8"> | |
<input class="form-control" | |
id="{{ 'street1Id' + i }}" | |
type="text" | |
placeholder="Street address" | |
formControlName="street1"> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" | |
for="street2Id">Street Address 2</label> | |
<div class="col-md-8"> | |
<input class="form-control" | |
id="street2Id" | |
type="text" | |
placeholder="Street address (second line)" | |
formControlName="street2"> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" | |
for="cityId">City, State, Zip Code</label> | |
<div class="col-md-3"> | |
<input class="form-control" | |
id="cityId" | |
type="text" | |
placeholder="City" | |
formControlName="city"> | |
</div> | |
<div class="col-md-3"> | |
<select class="form-control" | |
id="stateId" | |
formControlName="state"> | |
<option value="" | |
disabled | |
selected | |
hidden>Select a State...</option> | |
<option value="AL">Alabama</option> | |
<option value="AK">Alaska</option> | |
<option value="AZ">Arizona</option> | |
<option value="AR">Arkansas</option> | |
<option value="CA">California</option> | |
<option value="CO">Colorado</option> | |
<option value="WI">Wisconsin</option> | |
<option value="WY">Wyoming</option> | |
</select> | |
</div> | |
<div class="col-md-2"> | |
<input class="form-control" | |
id="zipId" | |
type="number" | |
placeholder="Zip Code" | |
formControlName="zip"> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<div class="col-md-4"> | |
<button class="btn btn-outline-primary" | |
type="button" | |
[title]="addresses.valid ? 'Add another mailing address' : 'Disabled until the existing address data is valid'" | |
[disabled]="!addresses.valid" | |
// here we bind our method for adding new address to the array of addresses | |
(click)="addAddress()"> | |
Add Another Address | |
</button> | |
</div> | |
</div> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { debounceTime } from 'rxjs/operators'; | |
ngOnInit() { | |
this.customerForm = this.fb.group({ | |
firstName: ['', [Validators.required, Validators.minLength(3)]], | |
lastName: ['', [Validators.required, Validators.maxLength(50)]], | |
emailGroup: this.fb.group({ | |
email: ['', [Validators.required, Validators.email]], | |
confirmEmail: ['', Validators.required], | |
}, { validator: emailMatcher }), | |
phone: '', | |
notification: 'email', | |
rating: [null, ratingRange(1, 5)], | |
sendCatalog: true | |
}); | |
const emailControl = this.customerForm.get('emailGroup.email'); | |
emailControl.valueChanges.pipe( | |
// we wait 1 second after user stops typing to display validation | |
// error messages, giving user time to type in the actual values | |
debounceTime(1000) | |
).subscribe( | |
value => this.setMessage(emailControl) | |
); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// product-edit.guard.ts | |
import { Injectable } from '@angular/core'; | |
import { CanDeactivate } from '@angular/router'; | |
import { Observable } from 'rxjs'; | |
import { ProductEditComponent } from './product-edit.component'; | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
// this guard implements CanDeactivate<ProductEditComponent> which means that it will be used | |
// to specify what happens when user try to leave the page of ProductEditComponent | |
// in our case we will ask user if they are sure they want to leave after they left some | |
// unfinished form business - potential data loss. | |
// if they confirm then the redirection will happen | |
export class ProductEditGuard implements CanDeactivate<ProductEditComponent> { | |
canDeactivate(component: ProductEditComponent): Observable<boolean> | Promise<boolean> | boolean { | |
if (component.productForm.dirty) { | |
const productName = component.productForm.get('productName').value || 'New Product'; | |
return confirm(`Navigate away and lose all changes to ${productName}?`); | |
} | |
return true; | |
} | |
} | |
// product-edit.component.ts | |
ngOnInit(): void { | |
this.productForm = this.fb.group({ | |
productName: ['', [Validators.required, | |
Validators.minLength(3), | |
Validators.maxLength(50)]], | |
productCode: ['', Validators.required], | |
starRating: ['', NumberValidators.range(1, 5)], | |
tags: this.fb.array([]), | |
description: '' | |
}); | |
// Read the product Id from the route parameter | |
this.sub = this.route.paramMap.subscribe( | |
params => { | |
const id = +params.get('id'); | |
this.getProduct(id); | |
} | |
); | |
} | |
ngOnDestroy(): void { | |
// unsubscribe from earlier declared subscription!! | |
this.sub.unsubscribe(); | |
} | |
// product.module.ts | |
@NgModule({ | |
imports: [ | |
SharedModule, | |
ReactiveFormsModule, | |
InMemoryWebApiModule.forRoot(ProductData), | |
RouterModule.forChild([ | |
{ path: 'products', component: ProductListComponent }, | |
{ path: 'products/:id', component: ProductDetailComponent }, | |
{ | |
path: 'products/:id/edit', | |
// specifying what needs to be checked before user can leave the page | |
// here our guard comes in, checking if user left some unsaved data in form | |
canDeactivate: [ProductEditGuard], | |
// there is also canActivate but we don't use it here | |
component: ProductEditComponent | |
} | |
]) | |
], | |
declarations: [ | |
ProductListComponent, | |
ProductDetailComponent, | |
ProductEditComponent | |
] | |
}) | |
export class ProductModule { } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
setNotification(notifyVia: string): void { | |
const phoneControl = this.customerForm.get('phone'); | |
if (notifyVia === 'text') { | |
// adding new validator to control's pool of validators | |
phoneControl.setValidators(Validators.required); | |
} else { | |
// clearing control's pool of validators | |
phoneControl.clearValidators(); | |
} | |
// re-validating newly added/deleted validators on control | |
phoneControl.updateValueAndValidity(); | |
} | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label pt-0">Send Notifications</label> | |
<div class="col-md-8"> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
type="radio" | |
value="email" | |
formControlName="notification" | |
(click)="setNotification('email')">Email | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
type="radio" | |
value="text" | |
formControlName="notification" | |
(click)="setNotification('text')">Text | |
</label> | |
</div> | |
</div> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app.component.ts | |
emailMessage: string; | |
private validationMessages = { | |
required: 'Please enter your email address.', | |
email: 'Please enter a valid email address.' | |
}; | |
ngOnInit() { | |
this.customerForm = this.fb.group({ | |
firstName: ['', [Validators.required, Validators.minLength(3)]], | |
lastName: ['', [Validators.required, Validators.maxLength(50)]], | |
emailGroup: this.fb.group({ | |
email: ['', [Validators.required, Validators.email]], | |
confirmEmail: ['', Validators.required], | |
}, { validator: emailMatcher }), | |
phone: '', | |
notification: 'email', | |
rating: [null, ratingRange(1, 5)], | |
sendCatalog: true | |
}); | |
// here we are declaring watcher and reacting to change | |
// when notification radio button gets checked | |
// this is going to replace regular html data binding!! | |
// more reactive way!!! | |
this.customerForm.get('notification').valueChanges.subscribe( | |
value => this.setNotification(value) | |
); | |
const emailControl = this.customerForm.get('emailGroup.email'); | |
emailControl.valueChanges.subscribe( | |
value => this.setMessage(emailControl) | |
); | |
// error checking inside component class! | |
// template file will be much cleaner this way | |
// the downside of this approach is that it will only work | |
// for value changes and going out of focus is not change of value! | |
setMessage(c: AbstractControl): void { | |
this.emailMessage = ''; | |
if ((c.touched || c.dirty) && c.errors) { | |
this.emailMessage = Object.keys(c.errors).map( | |
key => this.validationMessages[key]).join(' '); | |
} | |
} | |
} | |
setNotification(notifyVia: string): void { | |
const phoneControl = this.customerForm.get('phone'); | |
if (notifyVia === 'text') { | |
phoneControl.setValidators(Validators.required); | |
} else { | |
phoneControl.clearValidators(); | |
} | |
phoneControl.updateValueAndValidity(); | |
} | |
// app.component.html | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label pt-0">Send Notifications</label> | |
<div class="col-md-8"> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
type="radio" | |
value="email" | |
formControlName="notification" | |
//(click)="setNotification('email')" | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input class="form-check-input" | |
type="radio" | |
value="text" | |
formControlName="notification" | |
//(click)="setNotification('text')" | |
>Text | |
</label> | |
</div> | |
</div> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { NgModule } from '@angular/core'; | |
import { RouterModule } from '@angular/router'; | |
import { ProductListComponent } from './product-list.component'; | |
import { ProductDetailComponent } from './product-detail.component'; | |
import { ProductEditComponent } from './product-edit/product-edit.component'; | |
import { SharedModule } from '../shared/shared.module'; | |
@NgModule({ | |
imports: [ | |
SharedModule, | |
// Here we import router service | |
// because it is feature component we don't need to bind | |
// this router to whole application | |
// that's why we are using forChild here | |
// it makes sure that router service will not be imported again | |
// and we will use instance that is already inside main injector | |
RouterModule.forChild([ | |
{ path: 'products', component: ProductListComponent } | |
]) | |
], | |
declarations: [ | |
ProductListComponent, | |
ProductDetailComponent, | |
ProductEditComponent | |
] | |
}) | |
export class ProductModule { } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app.component.ts | |
import { ActivatedRoute } from '@angular/router'; | |
constructor(private route: ActivatedRoute) { | |
console.log(this.route.snapshot.queryParamMap.get('filterBy')); | |
} | |
// app.component.html | |
<a [routerLink]="['/products', product.id]" | |
[queryParams]="{filterBy: listFilter}"> | |
{{product.productName}} | |
</a> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// breadcrumbs! | |
@NgModule({ | |
imports: [ | |
RouterModule.forChild([ | |
{ | |
path: 'products', | |
component: ProductListComponent, | |
data: { pageTitle: 'Products List' } | |
}, | |
{ path: 'products/:id', component: ProductDetailComponent } | |
]) | |
] | |
}) | |
// data property is static, so no need to watch for changes (using observable) | |
this.pageTitle = this.PerformanceResourceTiming.snapshot.data['pageTitle']; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
index.html | |
Here we can specify our application's main path - base href. | |
--> | |
<html lang="en"> | |
<head> | |
<base href="/"> | |
</head> | |
<!-- | |
Second place to do that would by using Angular CLI parameter: | |
ng build --base-href /APM/ | |
It is useful when we need to specify some subfolder | |
--> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 1. ROOT INJECTOR providers: [LoggerService] / @Injectable({ providedIn: 'root'}) | |
// 2. LAZY-LOADED MODULE | |
// 3. COMOPNENT | |
// 4. CHILD-COMPONENT | |
// providing service to root injector ensures us that we only use one instance of service class | |
// and data will be the same everywhere if we read some property for example | |
// if we want to have an instance of service just for one component then we need to add service to component providers | |
@Component({ | |
... | |
providers: [DataService] | |
}) | |
constructor(private dataService: DataService) {} | |
// STILL NEED TO INJECT IN CONSTRUCTOR! | |
// this type of providing is located lower in the injector hierarchy | |
// Where should you provide services? | |
// root-injector => when service is needed everywhere |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// services are singletons | |
// there are two ways of PROVIDING a service to INJECTOR | |
// 1. using providers array in app.module.ts | |
providers: [LoggerService] | |
// 2. Using @Injectable decorator pointed on root | |
// logger.service.ts | |
@Injectable({ | |
providedIn: 'root' | |
}) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Promises are official part of javasscript, unlike observables. | |
They are good alternative for observables. | |
Promise can be RESOLVED (passes successful result to callback function) | |
or REJECTED (passess reason why promise was rejected to other callback function) | |
updateSchedule(empID: number): Promise<string> { | |
return new Promise((resolve, reject) => { | |
let result: string = this.processCalendar(); | |
if (result === 'success') { | |
resolve('Done updating schedule'); | |
} else { | |
reject('Unable to update schedule'); | |
} | |
}; | |
} | |
ngOnInit() { | |
this.dataService.updateSchedule(10) | |
.then( | |
data => console.log(`Resolved: ${data}`), | |
reason => console.log(`Rejected: ${reason}`) | |
) | |
.catch( | |
err => console.log(`Error: ${err}`) | |
) | |
} | |
// try-catcher | |
ngOnInit() { | |
this.getAuthorRecommendationAsync(1) | |
.catch(err => this.loggerService.error(err)); | |
} | |
private async getAuthorRecommendationAsync(readerID: number): Promise<void> { | |
let author: string = await this.dataService.getAuthorRecommendation(readerID); | |
this.loggerService.log(author); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// provider is like a recipe | |
// it has DI token associated with it | |
// A dependency provider configures an injector with a DI token, | |
which that injector uses to provide the runtime version of a dependency value. | |
// providers array contains tokens for our services | |
// TOKEN/RECIPE | |
providers: [DataService, | |
// TOKEN // RECIPE | |
// identyfing service // how should it be created? | |
{ provide: LoggerService, useClass: LoggerService }, | |
{ provide: LoggerService, useValue: {implementation} }, | |
{ provide: LoggerService, useFactory: dataServiceFactory, deps: [LoggerService] } | |
] | |
// useClass uses new keyword | |
// useValue uses whatever we pass to it as implementation | |
// useFactory lets us modify the default angular's process from useClass (it is a function) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="card"> | |
<div class="card-header"> | |
Sign Up! | |
</div> | |
<div class="card-body"> | |
<form novalidate (ngSubmit)="save(signupForm)" #signupForm="ngForm"> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="firstNameId"> | |
First Name | |
</label> | |
<div class="col-md-8"> | |
<input | |
class="form-control" | |
id="firstNameId" | |
type="text" | |
placeholder="First Name (required)" | |
required | |
minlength="3" | |
[(ngModel)]="customer.firstName" | |
name="firstName" | |
#firstNameVar="ngModel" | |
[ngClass]="{ | |
'is-invalid': | |
(firstNameVar.touched || firstNameVar.dirty) && | |
!firstNameVar.valid | |
}" | |
/> | |
<span class="invalid-feedback"> | |
<span *ngIf="firstNameVar.errors?.required"> | |
Please enter your first name. | |
</span> | |
<span *ngIf="firstNameVar.errors?.minlength"> | |
The first name must be longer than 3 characters. | |
</span> | |
</span> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="lastNameId"> | |
Last Name | |
</label> | |
<div class="col-md-8"> | |
<input | |
class="form-control" | |
id="lastNameId" | |
type="text" | |
placeholder="Last Name (required)" | |
required | |
maxlength="50" | |
[(ngModel)]="customer.lastName" | |
name="lastName" | |
#lastNameVar="ngModel" | |
[ngClass]="{ | |
'is-invalid': | |
(lastNameVar.touched || lastNameVar.dirty) && !lastNameVar.valid | |
}" | |
/> | |
<span class="invalid-feedback"> | |
<span *ngIf="lastNameVar.errors?.required"> | |
Please enter your last name. | |
</span> | |
<span *ngIf="lastNameVar.errors?.maxlength"> | |
The last name must be less than 50 characters. | |
</span> | |
</span> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="emailId">Email</label> | |
<div class="col-md-8"> | |
<input | |
class="form-control" | |
id="emailId" | |
type="email" | |
placeholder="Email (required)" | |
required | |
[(ngModel)]="customer.email" | |
name="email" | |
#emailVar="ngModel" | |
[ngClass]="{ | |
'is-invalid': | |
(emailVar.touched || emailVar.dirty) && !emailVar.valid | |
}" | |
/> | |
<span class="invalid-feedback"> | |
<span *ngIf="emailVar.errors?.required"> | |
Please enter your email address. | |
</span> | |
<span *ngIf="emailVar.errors?.email"> | |
Please enter a valid email address. | |
</span> | |
</span> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<div class="col-md-8"> | |
<div class="form-check"> | |
<label class="form-check-label"> | |
<input | |
class="form-check-input" | |
id="sendCatalogId" | |
type="checkbox" | |
[(ngModel)]="customer.sendCatalog" | |
name="sendCatalog" | |
/> | |
Send me your catalog | |
</label> | |
</div> | |
</div> | |
</div> | |
<div *ngIf="customer.sendCatalog"> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label pt-0">Address Type</label> | |
<div class="col-md-8"> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input | |
class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="home" | |
[(ngModel)]="customer.addressType" | |
name="addressType" | |
/> | |
Home | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input | |
class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="work" | |
[(ngModel)]="customer.addressType" | |
name="addressType" | |
/> | |
Work | |
</label> | |
</div> | |
<div class="form-check form-check-inline"> | |
<label class="form-check-label"> | |
<input | |
class="form-check-input" | |
id="addressType1Id" | |
type="radio" | |
value="other" | |
[(ngModel)]="customer.addressType" | |
name="addressType" | |
/> | |
Other | |
</label> | |
</div> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="street1Id"> | |
Street Address 1 | |
</label> | |
<div class="col-md-8"> | |
<input | |
class="form-control" | |
id="street1Id" | |
type="text" | |
placeholder="Street address" | |
[(ngModel)]="customer.street1" | |
name="street1" | |
/> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="street2Id"> | |
Street Address 2 | |
</label> | |
<div class="col-md-8"> | |
<input | |
class="form-control" | |
id="street2Id" | |
type="text" | |
placeholder="Street address (second line)" | |
[(ngModel)]="customer.street2" | |
name="street2" | |
/> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<label class="col-md-2 col-form-label" for="cityId"> | |
City, State, Zip Code | |
</label> | |
<div class="col-md-3"> | |
<input | |
class="form-control" | |
id="cityId" | |
type="text" | |
placeholder="City" | |
[(ngModel)]="customer.city" | |
name="city" | |
/> | |
</div> | |
<div class="col-md-3"> | |
<select | |
class="form-control" | |
id="stateId" | |
[(ngModel)]="customer.state" | |
name="state" | |
> | |
<option value="" disabled selected hidden> | |
Select a State... | |
</option> | |
<option value="AL">Alabama</option> | |
<option value="AK">Alaska</option> | |
<option value="AZ">Arizona</option> | |
<option value="AR">Arkansas</option> | |
<option value="CA">California</option> | |
<option value="CO">Colorado</option> | |
<option value="WI">Wisconsin</option> | |
<option value="WY">Wyoming</option> | |
</select> | |
</div> | |
<div class="col-md-2"> | |
<input | |
class="form-control" | |
id="zipId" | |
type="number" | |
placeholder="Zip Code" | |
[(ngModel)]="customer.zip" | |
name="zip" | |
/> | |
</div> | |
</div> | |
</div> | |
<div class="form-group row mb-2"> | |
<div class="offset-md-2 col-md-4"> | |
<button | |
class="btn btn-primary mr-3" | |
type="submit" | |
style="width: 80px;" | |
[title]=" | |
signupForm.valid | |
? 'Save your entered data' | |
: 'Disabled until the form data is valid' | |
" | |
[disabled]="!signupForm.valid" | |
> | |
Save | |
</button> | |
</div> | |
</div> | |
</form> | |
</div> | |
</div> | |
<br /> | |
Dirty: {{ signupForm.dirty }} | |
<br /> | |
Touched: {{ signupForm.touched }} | |
<br /> | |
Valid: {{ signupForm.valid }} | |
<br /> | |
Value: {{ signupForm.value | json }} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Component, OnInit } from '@angular/core' | |
import { NgForm } from '@angular/forms' | |
import { Customer } from '../models/customer.model' | |
@Component({ | |
selector: 'app-template-form', | |
templateUrl: './template-form.component.html', | |
styleUrls: ['./template-form.component.scss'], | |
}) | |
export class TemplateFormComponent implements OnInit { | |
public customer: Customer = new Customer() | |
constructor() {} | |
ngOnInit(): void {} | |
public save(customerForm: NgForm) { | |
console.log(customerForm.form) | |
console.log('Saved: ' + JSON.stringify(customerForm.value)) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
TEMPLATE-DRIVEN FORMS | |
- easy to use | |
- similar to AngularJS | |
- two-way data binding -> minimal component data | |
- automatically tracks form and input element state | |
REACTIVE FORMS | |
- more flexible -> more complex scenarios -> MORE CODE!!! | |
- immutable data model | |
- easier to perform an action on a value change | |
- reactive transformations -> DebounceTime or DistinctUntilChanged | |
- easily add input elements dynamically | |
- easier unit testing | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
HttpClient, | |
HttpErrorResponse, | |
HttpEvent, | |
HttpResponse, | |
HTTP_INTERCEPTORS, | |
} from '@angular/common/http' | |
import { inject, TestBed } from '@angular/core/testing' | |
import { | |
HttpClientTestingModule, | |
HttpTestingController, | |
} from '@angular/common/http/testing' | |
import { ErrorHandlerInterceptor } from './http-error-interceptor.interceptor' | |
import { HubApiService } from './hub-api.service' | |
import { Hub } from './hub.model' | |
import { AppComponent } from './app.component' | |
describe('ErrorHandlerInterceptor', () => { | |
let service: HubApiService | |
let httpClient: HttpClient | |
let httpMock: HttpTestingController | |
let interceptor: ErrorHandlerInterceptor | |
let mockHubs: Hub[] = [ | |
{ | |
id: 1, | |
address: 'a', | |
memberCount: 11, | |
name: 'a', | |
}, | |
{ | |
id: 2, | |
address: 'b', | |
memberCount: 22, | |
name: 'b', | |
}, | |
] | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [HttpClientTestingModule], | |
providers: [ | |
HttpClientTestingModule, | |
ErrorHandlerInterceptor, | |
HubApiService, | |
{ | |
provide: HTTP_INTERCEPTORS, | |
useClass: ErrorHandlerInterceptor, | |
multi: true, | |
}, | |
], | |
declarations: [AppComponent], | |
}) | |
service = TestBed.inject(HubApiService) | |
httpMock = TestBed.inject(HttpTestingController) | |
httpClient = TestBed.inject(HttpClient) | |
interceptor = TestBed.inject(ErrorHandlerInterceptor) | |
}) | |
it('should be created', () => { | |
const interceptor: ErrorHandlerInterceptor = TestBed.inject( | |
ErrorHandlerInterceptor, | |
) | |
expect(interceptor).toBeTruthy() | |
}) | |
it('should throw 500 when there are problems', () => { | |
service.getHubData().subscribe(() => { | |
fail('should have failed with 500 error'), | |
(error: HttpErrorResponse) => { | |
expect(error.status).toEqual(500, 'status') | |
expect(error.statusText).toEqual( | |
'Internal Server Error', | |
'statusText', | |
) | |
} | |
}) | |
const httpRequest = httpMock.expectOne('localhost:2500/getHubs') | |
expect(httpRequest.request.method).toEqual('GET') | |
// interceptor.intercept(httpRequest as HttpRequest, httpRequest); | |
httpRequest.flush('error', { | |
status: 500, | |
statusText: 'Internal Server Error', | |
}) | |
}) | |
}) | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { FormBuilder, Validators } from '@angular/forms'; | |
import { routes } from 'src/app/app-routing.module'; | |
import { RouterTestingModule } from '@angular/router/testing'; | |
import { Router } from '@angular/router'; | |
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; | |
import { ProductEditComponent } from './product-edit.component'; | |
import { HttpClientTestingModule } from '@angular/common/http/testing'; | |
describe('EditProductComponent', () => { | |
let component: ProductEditComponent; | |
let fixture: ComponentFixture<ProductEditComponent>; | |
let router: Router; | |
let formBuilder: FormBuilder; | |
beforeEach(async () => { | |
await TestBed.configureTestingModule({ | |
imports: [ HttpClientTestingModule, RouterTestingModule.withRoutes(routes) ], | |
declarations: [ ProductEditComponent ], | |
providers: [ | |
FormBuilder, | |
ProductsService | |
] | |
}) | |
.compileComponents(); | |
}); | |
beforeEach(() => { | |
fixture = TestBed.createComponent(ProductEditComponent); | |
component = fixture.componentInstance; | |
router = TestBed.inject(Router); | |
formBuilder = TestBed.inject(FormBuilder); | |
component.editProductForm = formBuilder.group( | |
{ | |
productName: [ | |
'', | |
{ | |
validators: [ | |
Validators.required, | |
Validators.maxLength(44), | |
Validators.pattern('^\\S?\\S+(?: \\S+)*\\s?\\S?$') | |
] | |
} | |
], | |
additionalInformation: [ | |
'', | |
{ | |
validators: [ | |
Validators.maxLength(174), | |
Validators.pattern('^\\S?\\S+(?: \\S+)*\\s?\\S?$') | |
] | |
} | |
] | |
}); | |
fixture.detectChanges(); | |
}); | |
it('EDT-PRDT-001: should create', () => { | |
expect(component).toBeTruthy(); | |
}); | |
it('EDT-PRDT-002: should display correct header name', fakeAsync(() => { | |
const id = '1'; | |
router.navigate(['/products', id, 'edit']); | |
tick(); | |
expect(router.url).toContain('/products/1/edit'); | |
expect(component.product?.productName).toEqual('Product 1'); | |
})); | |
}); | |
import { FormGroup, FormBuilder, Validators, AbstractControl } from '@angular/forms'; | |
import { Location } from '@angular/common'; | |
import { Component } from '@angular/core'; | |
import { IProduct } from '@shared/models/product.model'; | |
import { ActivatedRoute, Router } from '@angular/router'; | |
import { ProductsService } from '@products/services/products.api.service'; | |
import { NotificationService } from '@shared/services/notification.service'; | |
@Component({ | |
selector: 'app-product-edit', | |
templateUrl: './product-edit.component.html', | |
styleUrls: ['./product-edit.component.scss'] | |
}) | |
export class ProductEditComponent { | |
public constructor( | |
private productsService: ProductsService, | |
private formBuilder: FormBuilder, | |
private route: ActivatedRoute, | |
private router: Router, | |
private notificationService: NotificationService, | |
public location: Location | |
) { | |
this.editProductForm = this.formBuilder.group( | |
{ | |
productName: [ | |
'', | |
{ | |
validators: [ | |
Validators.required, | |
Validators.maxLength(44), | |
Validators.pattern('^\\S?\\S+(?: \\S+)*\\s?\\S?$') | |
] | |
} | |
], | |
additionalInformation: [ | |
'', | |
{ | |
validators: [ | |
Validators.maxLength(174), | |
Validators.pattern('^\\S?\\S+(?: \\S+)*\\s?\\S?$') | |
] | |
} | |
] | |
} | |
); | |
} | |
public product?: IProduct; | |
public editProductForm!: FormGroup; | |
public ngOnInit(): void { | |
this.getProductDetails(this.getIdFromRoute()); | |
} | |
public getIdFromRoute(): number { | |
return Number(this.route.snapshot.paramMap.get('id')); | |
} | |
private getProductDetails(id: number): void { | |
this.productsService | |
.getProduct(id) | |
.subscribe( | |
(productDetails: IProduct) => { | |
this.product = productDetails; | |
this.editProductForm.get('productName')?.setValue(this.product.productName); | |
this.editProductForm.get('additionalInformation')?.setValue(this.product.additionalInfo); | |
}, | |
); | |
} | |
public back(): void { | |
this.location.back(); | |
} | |
public validateField(fieldName: string): boolean { | |
const field: AbstractControl = this.editProductForm.controls[fieldName]; | |
return field.invalid && (field.dirty || field.touched); | |
} | |
public onSubmit(): void { | |
if (this.editProductForm.valid) { | |
const editedProduct: IProduct = { | |
id: this.product?.id, | |
productKey: String(this.product?.productKey), | |
productName: this.editProductForm.get('productName')?.value, | |
additionalInfo: this.editProductForm.get('productName')?.value, | |
creationDate: this.product?.creationDate | |
}; | |
this.productsService.updateProduct(editedProduct) | |
.subscribe(() => { | |
this.router.navigateByUrl('products'); | |
this.notificationService.success('Saved'); | |
}); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { TestBed } from '@angular/core/testing'; | |
import { DataApiService } from './data-api.service'; | |
import { HttpClientTestingModule, HttpTestingController, TestRequest } from '@angular/common/http/testing' | |
import { IWeatherForecast } from '../models/weather.model'; | |
import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; | |
describe('DataServiceService', () => { | |
let service: DataApiService; | |
let httpTestingController: HttpTestingController; | |
let mockWeather: IWeatherForecast[] = [ | |
{ date: new Date().toISOString(), temperatureC: 10, temperatureF: 34, summary: 'Sunny' }, | |
{ date: new Date().toISOString(), temperatureC: 5, temperatureF: 22, summary: 'Windy' }, | |
{ date: new Date().toISOString(), temperatureC: 666, temperatureF: 1230, summary: 'Volcanic' } | |
]; | |
beforeEach(() => { | |
TestBed.configureTestingModule({ | |
imports: [ HttpClientTestingModule ], | |
providers: [ DataApiService ] | |
}); | |
service = TestBed.inject(DataApiService); | |
httpTestingController = TestBed.inject(HttpTestingController); | |
}); | |
afterEach(() => { | |
httpTestingController.verify(); | |
}); | |
it('should be created', () => { | |
expect(service).toBeTruthy(); | |
}); | |
it('should GET all weathers', () => { | |
service.getWeathers() | |
.subscribe((data: IWeatherForecast[]) => { | |
expect(data.length).toBe(3); | |
}); | |
const weathersRequest: TestRequest = httpTestingController.expectOne('https://localhost:5001/weatherforecast'); | |
expect(weathersRequest.request.method).toEqual('GET'); | |
weathersRequest.flush(mockWeather); | |
}); | |
it('should GET one weather', () => { | |
service.getWeatherById(2) | |
.subscribe((data: IWeatherForecast) => { | |
expect(data).toBeTruthy(); | |
}); | |
const weatherRequest: TestRequest = httpTestingController.expectOne('https://localhost:5001/weatherforecast/2'); | |
expect(weatherRequest.request.method).toEqual('GET'); | |
weatherRequest.flush(mockWeather[2]); | |
}); | |
it('should POST one weather', () => { | |
service.postWeather(mockWeather[0]) | |
.subscribe( | |
data => expect(data).toEqual(mockWeather[0], 'should return weather'), fail); | |
const weatherRequest: TestRequest = httpTestingController.expectOne('https://localhost:5001/weatherforecast/add'); | |
expect(weatherRequest.request.method).toEqual('POST'); | |
expect(weatherRequest.request.body).toEqual(mockWeather[0]); | |
const expectedResponse = new HttpResponse({status: 200, statusText: 'Success', body: mockWeather[0]}); | |
weatherRequest.event(expectedResponse); | |
}); | |
it('should return 400 ERROR when trying to GET weather with incorrect id', () => { | |
service.getWeatherById(-Infinity) | |
.subscribe((data: IWeatherForecast) => { | |
// checking mocked response | |
fail('should have failed with 400 error'), | |
(error: HttpErrorResponse) => { | |
expect(error.status).toEqual(400, 'status'); | |
expect(error.statusText).toEqual('Bad Request', 'statusText'); | |
}; | |
}); | |
const weatherRequest: TestRequest = httpTestingController.expectOne('https://localhost:5001/weatherforecast/-Infinity'); | |
// mocking error response | |
expect(weatherRequest.request.method).toEqual('GET'); | |
weatherRequest.flush('error', { | |
status: 400, | |
statusText: 'Bad Request' | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment