Ultimate Course
Typescript: A type checking wrapper on top of javascript
Property binding
both of them are updated dynamically.
[name]=”<expression>”
name=”<plain string> , or {{interpolation}} as string”
Event binding
<input (input)="onChange($event)">
Two way binding
<input [ngModel]="name" (ngModelChange)="handleChange($event)">
<input [(ngModel)="name"] > // internally, angular registers an event handler and update name for you.
Template Ref
<input #inputElem> // This is a template ref, the id for it is inputElem
<button (click)="onClick(inputElem.value)">click me</button> // pass value of inputElem to the function
Rendering flow
ngIf, * syntax and <ng-template>
<div *ngIf="<expression>">
is equal to
<ng-template [ngIf]="name.length > 0">
<p>searching for {{name}}...</p>
</ng-template>
The “*” is a syntax sugar.
ngFor
<div *ngFor="let item of items; let i = index;"> {{i}} : {{item}}</div>
*ngFor is a syntax sugar, and the code block above is equivalent to
<ng-template ngFor let-i="index" let-item [ngForOf]="items">
<div> {{i}} : {{item}} </div>
</ng-template>
ngFor is a structure directive
You can also use ngFor on a single layer of an element.
<div *ngFor="let item of items" [name]="item"></div>
ngClass and className bindings
[class.className]=”expression” and [ngClass]=”{ ‘className’: expression, ‘anotherClassName’: anotherExpression}” can both add className to the element, but ngClass can add multiple classNames to it.
<div [class.checked-in]="isCheckedIn"> // "checked-in" class is added to the element if "isCheckedIn" is evaluated to true
<div [ngClass]=" { 'checked-in': isCheckedIn, 'checked-out': 'isCheckedOut'}"> // 'checked-in' is added to the element if 'isCheckedIn' is evaluated to true; 'checked-out' is added to the element if isCheckedOut is evaluated to true.
ngStyle and style bindings
<div [style.background]="'#2ecc71'"> // bind the color hex code to style.background. becareful, since the content in side "" is expression. so '' is needed to wrap the string.
<div [ngStyle]="{background: checkedIn? '#2ecc71': 'ffffff'}"> // use ngStyle to add multiple styles to it.
Pipes for data transformation
pipes can be used to transform the data right before rendering.
{{dateInSeconds | date: 'yMMM'}} {{ name | json}}
There are bunch of built in pipes available here. And you can create custom pipes to transform the data for rendering
Safe navigation
Use ? operator to avoid null pointer exception.
children?.length // '?' is used as safe navigator. If children is null or undefined, the whole expression is finished, otherwise access 'length' member of children.
let children? : string[];
length = children?.length || 0; // return children.length if children exists, otherwise return 0.
Use ?? for null checking and return default value
const yourName = name??'default_name' ;
// is equal to
const yourName = name == null? 'default_name': name;
Component architecture and feature modules
Dumb and smart component
Dumb/presentational component just renders the UI via @input, and emit events via @output
Smart component communicates with services, and render child components.
One way data flow
The event emits up, the data flows down.
<div [data]="companies" (change)="handleChange()">
Ng-template with context
<ng-container *ngTemplateOutlet="child;context:ctx> // ctx is an context object
<ng-template #child let-node></ng-template> // node = object assigned by $implicit
-----
.ts file
ctx = {
$implicit = object;
// other keys
}
Immutable state changes
remove elements from the array
names = names.filter(name => name.id !== id); // remove names whose id is id
edit elements in an array.
names = names.map(name => {
if (name.id === event.id) {
name = Object.assign({}, name, event) // the name object is merged with the event object. when conflict property value happens, the property value in event object prevails.
}
return name;
})
Service, HTTP and Observation
// service file
export class PassengerService {
constructor(){}
getPassengers(): Passengers[] {
return [];
}
}
// module file
import: [],
export: [],
declaration: [],
providers: [PassengerService] // make the service available in the current module. Make it available for injection.
// component file
// dependency injection
constructor(private passengerService: PassengerService) {}
Injectable annotation
// PersonService.ts
import {HttpClient} from '@angular/common/http';
import {Injectable} from '@angular/core';
@Injectable() // This means this service could use inject dependency (HttpClient)
export class PersonService {
constructor(private http: HttpClient){}
}
// module.ts
import {HttpClientModule} from '@angular/common/http';
@NgModule({
imports: [ BrowserModule, FormsModule, HttpClientModule ],
})
Http data fetch with Observerables
Http put, delete with immutable state
Headers and requestOptions
// PersonService.ts
const PASSENGER_API: string = '/api/passengers';
@Injectable() // This means this service could use inject dependency (HttpClient)
export class PersonService {
constructor(private http: HttpClient){}
getPassengers(): Observable<Passenger[]> {
return this.http.get(PASSENGER_API)
.map((response: Response) => {
return response.json();
})
}
updatePassenger(passenger: Passenger): Observable<Passenger> {
let headers = new Headers({
'Content-type': 'application/json'
});
let options = new RequestOptions({headers: headers});
return this.http.put(`${PASSENGER_API}\${passenger.id}`, passenger, options)
.map((response: Response) => {
return response.json();
})
}
removePassenger(passenger: Passenger): Observable<Passenger> {
return this.http.delete(`${PASSENGER_API}\${passenger.id}`)
.map((response: Response) => {
return response.json();
})
}
}
// component.ts
constructor(private passengerService: PassengerService){}
ngOnInit() {
this.passengerService.getPassengers().subscribe((data: Passenger[]) => {
this.passengers = data;
})
}
handleEdit(event: Passenger) {
this.passengerService.edit(event).subscribe((data: Passenger) => {
this.passengers = this.passengers.map((passenger: Passenger) => {
if(passenger.id === event.id) {
passenger = Object.assign({}, passenger, event);
}
return passenger;
})
})
}
handleRemove(event: Passenger) {
this.passengerService.remove(event).subscribe((data: Passenger) =>
{ // data is not used here, just as a placeholder.
this.passenger = this.passenger.filter((passener: Passenger) => {
return passenger.id !== event.id;
})
})
}
Http promises alternative
toPromise() operator could map the observable to promise.
Observable throw error handling
// service.ts
return this.http.get(PASSENGER_API).map((response : Response) => response.json()).catch((error: any) => Observable.throw(error.json()));
// caller.ts
this.passengerService.getPassenger().subscribe((data: Passenger[]) => handle_data, (error: Error) => handle_error);
Angular Template Driven Form
set up form container component
set up form stateless component: container passes input value to stateless component.
ngForm and ngModel, radio button, checkBox
// stateless component
template=`
<from #form="ngForm"> // activate ngForm directive, assign the model to template reference variable 'form'
// input box
<input ngModel type="number" name="id"> // ngModel: bind the input to the form object, with a key named 'id'
// radio button
<label><input ngModel (ngModelChange)="toggleCheckIn($event)" name="isCheckedIn" type="radio" [value]="true">Yes</label>
<label><input ngModel (ngModelChange)="toggleCheckIn($event)" name="isCheckedIn" type="radio" [value]="false">No</label>
// checkbox
<label><input ngModel (ngModelChange)="toggleCheckIn($event)" name="isCheckedIn" type="checkbox">Check in</label>
// options
<select name="baggage" [ngModel]="detail?.baggage">
<option *ngFor="let item of baggage"
[value]="item.key">{{item.value}}</option>
[selected]="item.key === detail?.baggage" // pre-select an option based on the input.
// You can also use [ngValue]="item.key" to replace [value] and [selected]
</select>
</form>
{{ form.value | json }} // {'id': xxx}
`
export class PassengerFormComponent {
@Input()
detail: Passenger;
toggleCheckIn(checkedIn: boolean) {
detail.checkedInDate = Date.now();
}
}
Form validation
<form #form="ngForm">
<input #fullname="ngModel" required>
<div *ngIf="fullname.errors?.required && fullname.dirty"> Fullname is required</div>
{{ fullname.errors | json}} // {"required": true}
</form>
{{form.valid | json}} // boolean
{{form.invalid | json}} // boolean
Form submit
@Output()
update: EventEmitter<Passenger> = new EventEmitter<Passenger>();
<form (ngSubmit)="handleSubmit(form.value, form.valid)" #form="ngForm">
<button type="submit" [disabled]="form.invalid"></button>
</form>
handleSubmit(passenger: Passenger, isValid: boolean) {
if (isvalid) {
this.update.emit(passenger);
}
}
Component routing
Base href
<head>
<base href="/"> // Important to include it in the index.html
</head>
// module.ts
import {RouterModule} from '@angular/router'
404 handling, routerLink, routerLinkActive
//app.module.ts
const routes: Routes = [
{ path: '', component: HomeComponent, pathMatch: 'full'},
{ path: '*', component: NotFountComponent}
];
@NgModule({
declarations: [],
imports: [],
bootstrap: [AppComponent]
})
// NotFoundComponent.ts
@Component({
selector: 'not-found'
template: `<div> Not Found</div>`
})
// app.component.ts
template: `
<div class='app'>
<nav>
<a routerLink='/' routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">Home</a> // routerLinkActive: add 'active' class to the link if it's active.
<router-outlet></router-outlet>
</div>
`</nav>
// .scss
a {
&.active {
color: #b690f1;
}
}
For multiple elements in nav, you can also use an ngFor to initialize the <a> links.
In a child module, use RouterModule.forChild() to register the router module for child.
Children route, Router params
const route: Routes = [
{
path: 'passengers',
children: [
{path: '', component: PassengerDashboardComponent}, // maps to '/passengers'
{path: ':id', component: PassengerViewComponent}, // maps to '/passengers/123423' any number
]
}
]
Route and ActivatedRoute
template: `
<button (click)="goBack()">Go Back</button>
`
constructor(private router: Router, private route: ActivatedRoute)
ngOnInit(){
this.route.params.switchMap((data: Params) => this.passengerService.getPassenger(data.id)).subscribe((passenger: Passenger) => {this.data = passenger;})
}
// switchMap - listen to an observerable, and switch to return to another observable. The previous observable is cancelled once the value is returned.
goBack(){
this.router.navigate(['/passengers']);
}
viewPassenger(event: Passenger){
this.router.navigate(['/passengers', event.id]); // /passengers/:id
}
Hash location strategy: the parts after ‘#’ symbol in the url never sends to the server.
Thus the parts after the ‘#’ symbol could be used to record the client state. like anchor in the page, etc.
Redirect to
const routes: Routes = [
{path: '', redirectTo: '/passengers'}
] // the '' page would be redirected to the '/passengers' page
Angular Pro Course
Content Projection with <ng-content>
the content in the selector tag could be projected to the child component html using <ng-content>
// app.component.html
<auth-form (submitted)="login($event)">
<h3>Login</h3>
<button type="submit">login</button>
</auth-form>
<auth-form (submitted)="signUp($event)">
<h3>Sign Up</h3>
<button type="submit">join us</button>
</auth-form>
// auth-form.ts
selector: 'auth-form',
template: `
<ng-content select="h3"></ng-content>
<form>
<ng-content select="button"></ng-content> // select is a query selector, it can also select a class by using '.class-name'
</form>
`
Content projection can also bind a component element, rather than basic HTML elements.
// app.component.html
<auth-form>
<auth-remember></auth-remember>
</auth-form>
// auth-form.ts
selector: 'auth-form',
template `
<div> This is the auth-form</div>
<ng-content select='auth-remember'></ng-content>
`
@ContentChild and ngAfterContentInit
Use contentChild to access a content injected child component under current component. The communication between a child component and current component can also be done by @output and @input.
// child.component.ts
selector: 'child',
template: ``
class ChildComponent {
@Output checked: EventEmitter<boolean> = new EventEmitter<boolean>();
}
// parent.component.ts
template:`
<ng-content select='child'></ng-content>
`
// access child component.
@ContentChild(ChildComponent) child: ChildComponent
ngAfterContentInit() {
this.child.checked.subscribe(isChecked => xxxx);
}
// app.component.ts
template: `
<parent>
<child></child>
</parent>
`
@ContentChildren and QueryLists
// child.component.ts
selector: 'child',
template: ``
class ChildComponent {
@Output checked: EventEmitter<boolean> = new EventEmitter<boolean>();
}
// parent.component.ts
template:`<ng-content select='child'></ng-content>`
// access child component.
@ContentChildren(ChildComponent) children: QueryList<ChildComponent>
ngAfterContentInit() {
if (this.children) {
this.children.forEach(item => item.checked.subscribe(isChecked => xxx));
}
}
// app.component.ts
template: `
<parent>
<child></child>
<child></child>
<child></child>
</parent>
`
@ViewChild and ngAfterViewInit
Contain the child component directly in the template
// parent.component.ts
template:`<child></child>`
@ViewChild(ChildComponent) child: ChildComponent;
ngAfterViewInit(){
this.child.xxxx // This could probably cause ChangeAfterCheck error. Use ngAfterContentInit instead.
}
ngAfterContentInit(){
this.child.xxxx; // this would work
}
// child.component.ts
class ChildComponent {}
@ViewChildren and QueryLists
// parent.component.ts
template:`
<child></child>
<child></child>
<child></child>`
@ViewChildren(ChildComponent) children: QueryList<ChildComponent>;
constructor(private cd: ChangeDetectorRef){}
ngAfterViewInit(){
this.children.forEach(child => xxx);
this.cd.detectChanges();
}
ngAfterContentInit(){
this.child.xxxx; // this would work
}
// child.component.ts
class ChildComponent {}
@ViewChild and template #ref
Native element, renderer.
with @ViewChild and nativeElement field, you can manipulate dom tree in the ts code.
// HTML
<input ngModel #email>
// ts
@ViewChild('email') email: ElementRef;
constructor(private renderer: Renderer)
ngAfterViewInit() {
// viewChild is available after view init.
console.log(this.email.nativeElement);
this.email.nativeElement.setAttribute('placeholder', 'Enter your email address');
this.email.nativeElement.classList.add('email');
this.email.nativeElement.focus();
// or use the renderer to set the element attribute
// using renderer is cross-platform. Works on both mobile app,
// web app, etc. Platform agnostic renderer
this.renderer.setElementAttribute(this.email.nativeElement, 'placeholder', 'Enter your email address');
}
Dynamic components with ComponentFactoryResolver
with @input and @output
and destroy the component
move the component in the DOM
// authform.component.ts
class AuthFormComponent{
@Input() title: string; // @Input() can be omitted.
@Output() submitted: EventEmitter<string>();
}
// app.component.html
<div #entry></div>
// app.component.ts
constructor(private resolver: ComponentFactoryResolver){}
component: ComponentRef<AuthFormComponent>;
@ViewChild('entry', {read: ViewContainerRef}) entry: ViewContainerRef;
ngAfterContentInit(){
const authFormFactory = this.resolver.resolveComponentFactory(AuthFormComponent);
this.component = this.entry.createComponent(authFormFactory, 0 /*order of the component when rendering*/);
// Then the #entry div is replaced with the AuthFormComponent component.
// question.
this.component.instance.title = 'New Title'; // bind input
this.component.instance.submitted.subscribe((value: string) => {}); // bind output
}
// move component to a different order.
moveComponent(){
this.entry.move(this.component.hostView, 1);
}
destroyComponent(){
this.component.destroy(); // destroy the component
}
ng-template rendering – skipped
ng-template context
<div #entry><div>
<ng-template #tmpl let-name let-location="location">
{{ name }}: {{ location }}
// name gets the value in $implicit
// location gets the value in location
</ng-template>
export class AppComponent implements AfterContentInit {
@ViewChild('tmpl') tmpl: TemplateRef<any>;
@ViewChild('entry', {read: ViewContainerRef}) entry: ViewContainerRef;
ngAfterContentInit(){
this.entry.createEmbeddedView(this.impl, {
$implicit: 'Motto Todd',
location: 'UK, England',
})
}
}
ng-template rendering with ng-container
template #tmpl is rendered at the ng-container location.
<ng-container [ngTemplateOutlet]='tmpl' [ngTemplateContext]='ctx'></ng-container>
<ng-template #tmpl let-name let-location='location'>{{ name }}: { location } </ng-template>
export class AppComponent {
ctx = {
$implicit = 'Todd Motto',
location = 'England UK',
};
}
ViewEncapsulation and Shadow DOM
The Angular has default ViewEncapsulation.Emulated view, whcih would add the hashed class string to the component, so that the css file defined in component A does not affect the style in component B, even if the class name in A and B are the same.
This strategy can be overwritten manually.
@Component({
selector: 'example-one',
encapsulation: ViewEncapsulation.Emulated
styles:['']
})
ChangeDetectionStrategy
The Angualr change detector works faster if all your component uses immutable objects. And the strategy could be set to ChangeDetectionStrategy.OnPush
In OnPush strategy, the Angular detect changes if the object reference is changed.
@Input() user;
this.user = {...this.user, name: 'Bo'}; // OnPush and Default both detect changes since the user object reference is changed.
this.user.name = 'Bo'; // Only Default strategy detects changes
Directive
// app.component.ts
template: `
<input credit-card>
`
// credit-card.directive.ts
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[credit-card]'
})
export class CreditCardDirective {
constructor(private element /*the host element of the directive. In this case, it is the input element. This is only for illustration purpose. You don't need to inject the host element in prod*/: ElementRef){}
@HostBinding(/*the host element attribute*/'style.border') border: string;
// HostListener listens to the event of the host element
@HostListener(/*the event name fired by the host*/'input', ['$event']) onKeyDown(event: KeyboardEvent){
const input = event.target as HTMLInputElement;
let trimmed = input.value.replace(/\s+/g, '');
if (trimmed.length > 16) {
trimmed = trimmed.substr(0, 16);
}
let numbers = [];
for (let i = 0; i < trimed.length; i+=4) {
numbers.push(trimmed.substr(i, 4));
// ['1323', '2334', '2211', '0000']
}
input.value = numbers.join(' ');
this.boarder = '';
if (/[^\d]+/.test(trimmed)) {
this.boarder = '1px solid red';
}
}
}
// app.module.ts
@NgModule ({
declarations: [CreditCardDirective],
})
Using exportAs property
<label tooltip='3 digits, back of your card'
#myTooltip="tooltip"> Enter your security code
<span
(mouseover)="myTooltip.show()
(mouseout)="myTooltip.hid()">(?)</span>
</label>
// TooltipDirective
@Directive({
selector: '[tooltip]',
exportAs: 'tooltip'
})
export class TooltipDirective implements OnInit {
tooltipElement = document.createElement('div');
visible = false;
constructor(private element: ElementRef){}
@Input() set tooltip(value) {
this.tooltipElement.textContent = value;
}
hide() {this.tooltipElement.classList.remove('tooltip--active');}
show(){this.tooltipElement.classList.add('tooltip--active');}
ngOnInit(){
this.tooltipElement.className = 'tooltip';
this.element.nativeElement.appendChild(this.tooltipElement);
this.element.nativeElement.classList.add('tooltip-container');
}
}
Creating a custom structural Directive
<li *ngFor="let item of items; let i = index;">
{{ i }} Member: {{ item.name | json }}
</li>
// *ngFor equivalent
<ng-template myFor [myForOf]="items" let-item let-i="index">
<li>
{{ i }} Member: {{ item.name | json}}
</li>
</ng-template>
//my-for.directive.ts
@Directive({
selector: '[myFor][myForOf]'
// the [myForOf] value is set via "let item of items"
// if the selector name is [myForIn], then it is set via "let item in items"
// This is achieved by the Angular compiler
@Input() set myForOf(collection) {
console.log(collection); // array of items
this.view.clear() // clear the views
collection.forEach((item, index) => {
this.view.createEmbeddedView(this.template, {
$implicit: item,
index,
});
});
}
constructor(private view: ViewContainerRef, private template: TemplateRef<any>){}
// Why do we need TemplateRef?
})
Creating custom pipe
interface File {
size: number;
}
// app.component.html
{{file.size | filesize:'megabytes' /* extension*/}}
// app.component.ts
decalrations: [FileSizePipe]
// filesize.pipe.ts
@Pipe({
name: 'filesize'
})
export class FileSizePipe implements PipeTransform {
transform(value: number, extension: string = 'MB'){
return (size / (1024 * 1024)).toFixed(2) + extension;
}
}
Pipes as providers
// copy the rest of the code from the previous section "creating custom pipe"
// app.component.html
{{ mapped.size }}
// app.component.ts
@Component({
...
providers: [FileSizePipe]
})
export class AppComponent implements OnInit {
files: File[];
constructor(private fileSizePipe: FileSizePipe){}
ngOnInit(){
this.files = [{xxx, size: 212019, xxx}];
this.mapped = this.files.map(file => {
return {...file, size: this.fileSizePipe.transform(file.size)};
})
}
}
Reactive Form
Set up
stock-inventory.component.ts
@Component({
selector: [],
template: `
<div class="stock-inventory">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<div formGroupName="store">
<input formControlName="branch"
type="text" placeholder="Branch ID">
<input formControlName="code"
type="text" placeholder="Manager Code">
</div>
</form>
</div>
`
})
export class StockInvetoryComponent {
form = new FormGroup({
store: new FormGroup({
branch: new FormControl(''),
code: new FormControl(''),
})
});
onSubmit(){
console.log('Submit: ', this.form.value)
}
}
Componentizing FormGroups
// stock-inventory.component.ts
@Component({
selector: [],
template: `
<div class="stock-inventory">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<stock-branch [parent]="form"></stock-branch>
<stock-selector [parent]="form"></stock-selector>
<stock-products [parent]="form"></stock-products>
</form>
</div>
`
})
export class StockInvetoryComponent {
form = new FormGroup({
store: new FormGroup({
branch: new FormControl(''),
code: new FormControl(''),
}),
selector: new FormGroup({
product_id: new FormControl(''),
quantity: new FormControl(10)
}),
stock: new FormArray([])
});
onSubmit(){
console.log('Submit: ', this.form.value)
}
}
// stock-branch.component.ts
@Component({
selector: 'stock-branch',
styleUrls: [],
template: `
<div [formGroup]="parent">
<div formGroupName="store">
<input formControlName="branch"
type="text" placeholder="Branch ID">
<input formControlName="code"
type="text" placeholder="Manager Code">
</div>
</div>
`
})
export class StockBranchComponent {
@Input() parent: FormGroup
}
// similar structure for StockSelector and StockProducts file
Binding FormControls to <select>
// html
@Component({
template: `
<div class="stock-inventory">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<stock-branch [parent]="form"></stock-branch>
<stock-selector [parent]="form"></stock-selector>
<stock-products [parent]="form" [products]="products"></stock-products>
</form>
</div>
`
})
export class StockInventoryComponent {
products: Product[] = [
{"id": 1, "price": 2800, "name": "MacBook Pro",
{"id": 2, "price": 200, "name": "Airpod",
{"id": 3, "price": 800, "name": "iPhone",
...}
];
form = new FormGroup({
store: new FormGroup({
branch: new FormControl(''),
code: new FormControl(''),
}),
selector: new FormGroup({
product_id: new FormControl(''),
quantity: new FormControl(10)
}),
stock: new FormArray([
new FormGroup({
product_id: new FormControl(3),
quantity: new FormControl(10),
});
])
});
}
@Component({
template: `
<div class="stock-selector" [formGroup]="parent">
<div formGroupName="selector">
<select formControlName="product_id">
<option value="">Select stock</option>
<option *ngFor="let product of products" [value]="product.id">{{product.name}}</option>
</select>
<input type="number"
step="10" min="10"
min="1000" formControlName="quantity">
<button click="" type="button">Add stock</button>
</div>
</div>
` })
export class StockSelectorComponent {
@Input() parent: FormGouop;
@Input() products: Product[];
@Output() added = new EventEmitter()
}
@Component({
selector: 'stock-products'
template: `
<div class="stock-product" [formGroup]="parent">
<div formArrayName="stock">
<div *ngFor="let item of stocks; let i = index">
<div class="stockproduct__content" [formGroupName]="i">
<div class="stock-prodct__name">
{{item.value.product_id}}
</div>
<input type="number" step="10" min="10" max="1000" formConolName="quantity">
<button type="button">Remove</button>
</div>
</div>
</div>
</div>
`
})
export class StockProductsComponent {
@Input() parent: FormGroup;
get stocks(){
return (this.parent.get('stock') as FormArray).controls
}
}
FormArray (skipped)
FormBuilder API
constructor(private fb: FormBuilder) {}
group = this.fb.group({
name: '', // the FormBuilder creates the FormControl for us
gender: '',
})
HttpService
// stock-inventory.service.ts
@Injectable()
export class StockInventoryService {
constructor(private http :HttpModule){}
getCartItems(): Observable<Item[]>{
return this.http.get('/api/cart')
.map((response: Response) => response.json())
.catch((error: any) => Observable.throw(error.json()));
}
}
// module.ts
providers: [StockInventoryService],
// stock-inventory.ts
constructor(private stockService: StockInventoryService){}
const products = this.stockService.getProducts()
const items = this.stockService.getCartItems();
Observable.forkJoin([cart, products])
.subscribe(() => {
// converts an array to a map
products.map<number, product>(product => [product.id, product]);
});
Subscribing to the ValueChanges Observable of a reactive form
constructor(private fb: FormBuilder){}
form = this.fb.group({
store: this.fb.group({
branch: '',
code: ''
}),
});
// subscribe to the valueChanges event of a form
this.form.get('stock').valueChanges.subscribe(value => {
console.log(value);
}))
Reset a form control
this.parent = this.fb.group({
selector: this.fb.group({
product_id: '',
quantity: 10,
})
});
// You need to provide value for each formControl
// the form properties like and dirty, touched will be reset.
this.parent.get('selector').reset({
product_id: '',
quantity: 10,
});
// or do the patchValue, to patch a specific form control value
this.parent.get('selector').patchValue({
product_id: '',
});
// or do the set, set every form control value.
// the properties of the form will not be reset.
this.parent.get('selector').set({
product_id: '',
quantity: 10,
});
Custom form control base and implement a custom control form
// creates a custom stock counter form
// app.ng.html
<stock-counter
formControlName="stockCounter"
></stock-counter>
// app.ts
stockCounter = new FormControl();
// stock-counter.ts
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'
const COUNTER_CONTROL_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
// forward reference. COUNTER_CONTROL_ACCESSOR is defined before
// the StockCounterComponent
useExisting: forwardRef(() => StockCounterComponent)
multi: true
}
@Component({
selector: 'stock-counter',
provider: [COUNTER_CONTROL_ACCESSOR],
styleUrls: ['stock-counter.omponent.scss']
template: `
<div class="stock-counter">
<div>
<div>
<p>{{ value }}</p>
<div>
<button type="button" (click)="increment()" >+</button>
<button type="button" (click)="decrement()" >-</button>
</div>
</div>
</div>
</div>
`
})
class StockCounterComponent implements ControlValueAccessor
{
@Input() step: number = 10;
@Input() min: number = 10;
@Input() max: number = 1000;
private onTouch: Function;
private onModelChange: Function;
value: number = 10;
writeValue(value){
this.value = value || 0;
}
registerOnChange(fn){
this.onModelChange = fn;
}
registerOnTouched(fn){
this.onTouch = fn;
}
increment(){
if (this.value < this.max) {
this.value += this.step;
this.onModelChange(this.value);
}
this.onTouch();
}
decrement(){
if (this.value > this.min) {
this.value -= this.step;
this.onModelChange(this.value);
}
this.onTouch();
}
}
Unit Testing
Testing a pipe
setting up host component
let component: TestComponent;
let fixture: ComponentFixture<TestComponent>;
let el: HTMLElement;
TestBed.initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
@Component({
template: `Size: {{ size | filesize:suffix}}`
})
class TestComponent {
suffix;
size = 123456789;
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
FileSizePipe,
TestComponent
]
});
fixture = TestBed.createComponent(TestComponent);
component = fixture.componentInstance;
el = fixture.nativeElement;
});
it('should convert bytes to megabytes', () => {
fixture.detectChanges();
expect(el.textContent).toContain('Size: 117.75MB');
});
Testing a service with dependencies
function createResponse(body) {
return Observable.of(new Response(new ResponseOptions({
body: JSON.stringify(body) })));
}
class MockHttp {
get() {
return createResponse([]);
}
}
beforeEach(() => {
const bed = TestBed.configureTestingModule({
providers: [StockInventoryService, {provide: Http, useClass: MockHttp }]
});
http = bed.get(Http);
service = bed.get(StockInventoryService);
});
it('should get cart items', () => {
spyOn(http, 'get').and.returnValue(createResponse([...cartItems]));
});
Misc Tips
Create a temp hidden input box , fill the string in the box, simulate user copy event, then delete the input box.
copyToClipboard(value: string) {
const textarea = document.createElement('textarea');
textarea.style.height = '0px';
textarea.style.left = '-100px';
textarea.style.opacity = '0';
textarea.style.position = 'fixed';
textarea.style.top = '-100px';
textarea.style.width = '0px';
document.body.appendChild(textarea);
// Set and select the value (creating an active Selection range).
textarea.value = value;
textarea.select();
// Ask the browser to copy the current selection to the clipboard.
const successful = document.execCommand('copy');
if (successful) {
// show banner
} else {
// handle the error
}
if (textarea && textarea.parentNode) {
textarea.parentNode.removeChild(textarea);
}
}
Measure the Web App performance
The Web Apps should aim for refreshing the page at 60fps. That is, finish a rendering cycle within 16ms. The javascript should be finish within 10ms, thus the browser has 6ms to do the housekeeping works.
https://developers.google.com/web/fundamentals/performance/rendering
browser渲染一个页面分五步:
Javascript: 运行javascript,包括更新变量值,更新DOM,更新variable和DOM element里的binding。
Style:计算每个element应该attach to哪个css class
Layout:计算每个element在页面中的位置
Paint:把每个element的所有pixel画出来。
Composite:paint过程中,实际上element被画在了不同的layer上。composite这步就是把不同layer的画整合到一个layer的screen上。包括谁应该覆盖谁,谁在上面谁在下面。
实际rendering中,并不是每个步骤都一定会被执行。
Udacity的课程,如何优化Web App的performance
https://www.udacity.com/course/browser-rendering-optimization–ud860