Angular + Typescript Notes


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

Copy a string in clipboard

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

The full pixel pipeline

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中,并不是每个步骤都一定会被执行。

The full pixel pipeline
case 1. 所有阶段都被执行一遍
The  pixel pipeline without layout.
case 2. layout不用执行。比如element的layout不用改。可能只改了UI string,换了背景颜色等。
The pixel pipeline without layout or paint.
case 3. layout和paint都不用改,也即画面不需要update。这种cycle最节省资源。

Udacity的课程,如何优化Web App的performance

https://www.udacity.com/course/browser-rendering-optimization–ud860

Leave a comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.