Skip to content

Instantly share code, notes, and snippets.

@bbrt3
Last active August 23, 2021 14:08
Show Gist options
  • Save bbrt3/af0a27be402aacb8a61dd2d53b41216b to your computer and use it in GitHub Desktop.
Save bbrt3/af0a27be402aacb8a61dd2d53b41216b to your computer and use it in GitHub Desktop.
Angular
/*
List of Angular elements:
a) component
b) directive
c) module
d) pipe
e) service
- interceptors
- resolvers
f) guard
*/
// 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>
// 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 }}
/*
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>
@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
*/
// 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>
// 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>
// 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>
// 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
]
// 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;
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 }
]
})
// 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 }
],
// 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 }
]
})
// 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);
// 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!!
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
);
}
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))
);
}
// 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>
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;
}
}
// 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>
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)
);
}
// 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 { }
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>
// 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')"
>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>
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 { }
/*
There are two options of activating a route:
a) router.navigate('['products'])
This one replaces only the part of the url
that is responsible for redirection
(secondary routes won't be touched)
b) router.navigateByUrl('products')
This one clears any other path segments
It replaces whole Url with what we pass to it
*/
// 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>
// 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'];
<!--
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
-->
// 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
// 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'
})
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);
}
// 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)
<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
email
[(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 }}
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))
}
}
/*
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
*/
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',
})
})
})
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');
});
}
}
}
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