Angular

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
npm install -g @angular/cli
ng new my-app
cd my-app
ng serve --open
ng analytics project off
ng build my-app -c production

ng generate component heroes
ng generate component heroes -t #short for inlineTemplate=true
ng generate component heroes --inline-style
ng generate class hero
ng generate pipe
ng generate directive highlight
ng generate service hero
ng generate module app-routing --flat --module=app

test : Karma + jasmine + chai + puppeteer
build : gulp

Modules

1
2
3
4
5
6
7
8
9
10
11
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
@NgModule({
imports: [ BrowserModule, FormsModule ],
providers: [ Logger ],
declarations: [ AppComponent ],
exports: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

Components

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import { Component, OnInit } from '@angular/core';

class Hero {
constructor(
public id: number,
public name: string) { }
}

@Component({
selector: 'app-heros',
// styleUrls: ['./app.component.css']
// templateUrl: './app.component.html',
template: `
<h1>{{title}}</h1>
<h2>My favorite hero is: {{myHero.name}}</h2>
<p>Heroes:</p>
<ul>
<li *ngFor="let hero of heroes">
{{ hero.name }}
</li>
</ul>
<p *ngIf="heroes.length > 3">There are many heroes!</p>
`
})
export class HerosComponent implements OnInit {

title = 'Tour of Heroes';
heroes = [
new Hero(1, 'Windstorm'),
new Hero(13, 'Bombasto'),
new Hero(15, 'Magneta'),
new Hero(20, 'Tornado')
];
myHero = this.heroes[0];

constructor() { }

ngOnInit(): void {
}

}

Data binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# one-way from data source to view target

{{expression}} - interpolation
[target]="expression" - property bounding
bind-target="expression"

# one-way from view target to data source

(target)="statement" - event binding
on-target="statement"

# two-way

[(target)]="expression"
bindon-target="expression"

User Input

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<button (click)="onClickMe()">Click me!</button>

<input (keyup)="onKey($event)">
<p>{{values}}</p>
export class KeyUpComponent {
values = '';
onKey(event: KeyboardEvent) {
this.values += (event.target as HTMLInputElement).value + ' | ';
}
}
缺点:包含全部 DOM event 信息的$event整个传入component method, 不符合关注点分离原则
最佳实践:pass values, not element

##### get user input from a template reference variable
##### this won't work at all unless you bind to an event
##### (keyup)="0" the shortest template statement possible
<input #box (keyup)="0">
<p>{{box.value}}</p>


<input #box (keyup)="onKey(box.value)">
<p>{{values}}</p>
export class KeyUpComponent {
values = '';
onKey(value: string) {
this.values += value + ' | ';
}
}

##### key event filtering(with key.enter)
<input #box (keyup.enter)="update(box.value)" (blur)="update(box.value);box.value=''">
<p>{{value}}</p>
export class KeyUpComponent {
value = '';
update(value: string) { this.value = value; }
}

Pipes

  • AsyncPipe : impure pipe
  • CurrencyPipe
  • DatePipe
  • DecimalPipe
  • I18nPluralPipe
  • I18nSelectPipe
  • JsonPipe
  • KeyValuePipe
  • LowerCasePipe
  • PercentPipe
  • SlicePipe
  • TitleCasePipe
  • UpperCasePipe
1
2
3
4
5
6
7
{{ birthday | date }}
{{ birthday | date:"yyyy-MM-dd HH:mm:ss" }}
{{ birthday | date | uppercase}}
{{ amount | currency:'en-US'}}
{{ amount | currency:'EUR' }}
{{ amount | currency:'EUR':'Euros '}}
{{ amount | slice:1:5 }}
create custom pipe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'exponentialStrength',
pure: true
})
export class ExponentialStrengthPipe implements PipeTransform {
transform(value: number, exponent?: number): number {
return Math.pow(value, isNaN(exponent) ? 1 : exponent);
}
}

import { FormsModule } from '@angular/forms';
<div>Normal power: <input [(ngModel)]="power"></div>
<div>Boost factor: <input [(ngModel)]="factor"></div>
<p>
Super Hero Power: {{power | exponentialStrength: factor}}
</p>
export class PowerBoostCalculatorComponent {
power = 5;
factor = 1;
}
detecting pure changes to primitives and object references

By default, pipes are defined as pure so that Angular executes the pipe only when it detects a pure change to the input value.
A pure change is either a change to a primitive input value (such as String, Number, Boolean, or Symbol), or a changed object reference (such as Date, Array, Function, or Object).

1
2
3
4
5
6
7
8
9
10
11
12
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'flyingHeroes' })
export class FlyingHeroesPipe implements PipeTransform {
transform(allHeroes: Flyer[]) {
return allHeroes.filter(hero => hero.canFly);
}
}
@Pipe({
name: 'flyingHeroesImpure',
pure: false
})
export class FlyingHeroesImpurePipe extends FlyingHeroesPipe {}
Unwrapping data from an observable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Component } from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map, take } from 'rxjs/operators';

@Component({
selector: 'app-hero-message',
template: `
<h2>Async Hero Message and AsyncPipe</h2>
<p>Message: {{ message$ | async }}</p>
<button (click)="resend()">Resend</button>`,
})
export class HeroAsyncMessageComponent {
message$: Observable<string>;

private messages = [
'You are my hero!',
'You are the best hero!',
'Will you be my hero?'
];

constructor() { this.resend(); }

resend() {
this.message$ = interval(500).pipe(
map(i => this.messages[i]),
take(this.messages.length)
);
}
}

// Alternative message$ formula:
// this.message$ = fromArray(this.messages).pipe(
// map(message => timer(500),
// map(() => message)),
// concatAll()
// );
caching HTTP requests
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { HttpClient } from '@angular/common/http';
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
name: 'fetch',
pure: false
})
export class FetchJsonPipe implements PipeTransform {
private cachedData: any = null;
private cachedUrl = '';

constructor(private http: HttpClient) { }

transform(url: string): any {
if (url !== this.cachedUrl) {
this.cachedData = null;
this.cachedUrl = url;
this.http.get(url).subscribe(result => this.cachedData = result);
}

return this.cachedData;
}
}

<div *ngFor="let hero of ('assets/heroes.json' | fetch) ">
{{hero.name}}
</div>
<p>Heroes as JSON:
{{'assets/heroes.json' | fetch | json}}
</p>`

Component lifecycle hooks

HookPurposeTiming
ngOnChanges()Respond when Angular sets or resets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Note that this happens very frequently, so any operation you perform here impacts performance significantly.Called before ngOnInit() and whenever one or more data-bound input properties change.
ngOnInit()Initialize the directive or component after Angular first displays the data-bound properties and sets the directive or component’s input properties.Called once, after the first ngOnChanges().
ngDoCheck()Detect and act upon changes that Angular can’t or won’t detect on its own.Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run.
ngAfterContentInit()Respond after Angular projects external content into the component’s view, or into the view that a directive is in.Called once after the first ngDoCheck().
ngAfterContentChecked()Respond after Angular checks the content projected into the directive or component.Called after ngAfterContentInit() and every subsequent ngDoCheck().
ngAfterViewInit()Respond after Angular initializes the component’s views and child views, or the view that contains the directive.Called once after the first ngAfterContentChecked().
ngAfterViewChecked()Respond after Angular checks the component’s views and child views, or the view that contains the directive.Called after the ngAfterViewInit() and every subsequent ngAfterContentChecked().
ngOnDestroy()Cleanup just before Angular destroys the directive or component. Unsubscribe Observables and detach event handlers to avoid memory leaks.Called immediately before Angular destroys the directive or component.
Initializing a component or directive

Use the ngOnInit() method to perform the following initialization tasks.

  • Perform complex initializations outside of the constructor. Components should be cheap and safe to construct. You should not, for example, fetch data in a component constructor.
  • Set up the component after Angular sets the input properties. Constructors should do no more than set the initial local variables to simple values.
Cleaning up on instance destruction

Put cleanup logic in ngOnDestroy(), the logic that must run before Angular destroys the directive.

  • Unsubscribe from Observables and DOM events.

  • Stop interval timers.

  • Unregister all callbacks that the directive registered with global or application services.

  • notify another part of the application that the component is going away.

Component Interaction

  • pass data from parent to child with input binding

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    # parent
    import { Component } from '@angular/core';
    @Component({
    selector: 'app-hero-parent',
    template: `
    <app-hero-child *ngFor="let hero of heroes"
    [hero]="hero"
    [master]="master">
    </app-hero-child>
    `
    })
    export class HeroParentComponent {
    heroes = HEROES;
    master = 'Master';
    }

    # child
    import { Component, Input } from '@angular/core';
    @Component({
    selector: 'app-hero-child',
    template: `
    <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
    `
    })
    export class HeroChildComponent {
    @Input() hero: Hero;
    @Input('master') masterName: string;
    }
  • intercept input property changes with a setter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    # parent
    import { Component } from '@angular/core';
    @Component({
    selector: 'app-name-parent',
    template: `
    <app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
    `
    })
    export class NameParentComponent {
    // Displays 'Dr IQ', '<no name set>', 'Bombasto'
    names = ['Dr IQ', ' ', ' Bombasto '];
    }

    # child
    import { Component, Input } from '@angular/core';
    @Component({
    selector: 'app-name-child',
    template: '<h3>"{{name}}"</h3>'
    })
    export class NameChildComponent {
    private _name = '';
    @Input()
    get name(): string { return this._name; }
    set name(name: string) {
    this._name = (name && name.trim()) || '<no name set>';
    }
    }
  • intercept input property changes with ngOnChanges()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    # parent
    import { Component } from '@angular/core';
    @Component({
    selector: 'app-version-parent',
    template: `
    <button (click)="newMinor()">New minor version</button>
    <button (click)="newMajor()">New major version</button>
    <app-version-child [major]="major" [minor]="minor"></app-version-child>
    `
    })
    export class VersionParentComponent {
    major = 1;
    minor = 23;

    newMinor() {
    this.minor++;
    }

    newMajor() {
    this.major++;
    this.minor = 0;
    }
    }

    # child
    import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
    @Component({
    selector: 'app-version-child',
    template: `
    <h3>Version {{major}}.{{minor}}</h3>
    <ul>
    <li *ngFor="let change of changeLog">{{change}}</li>
    </ul>
    `
    })
    export class VersionChildComponent implements OnChanges {
    @Input() major: number;
    @Input() minor: number;
    changeLog: string[] = [];

    ngOnChanges(changes: { [propKey: string]: SimpleChange } ) {
    const log: string[] = [];
    for (const propName in changes) {
    const changedProp = changes[propName];
    const to = JSON.stringify(changedProp.currentValue);
    if (changedProp.isFirstChange()) {
    log.push(`Initial value of ${propName} set to ${to}`);
    } else {
    const from = JSON.stringify(changedProp.previousValue);
    log.push(`${propName} changed from ${from} to ${to}`);
    }
    }
    this.changeLog.push(log.join(', '));
    }
    }
  • parent listens for child event

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    # parent
    import { Component } from '@angular/core';
    @Component({
    selector: 'app-vote-taker',
    template: `
    <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
    <app-voter *ngFor="let voter of voters"
    [name]="voter"
    (voted)="onVoted($event)">
    </app-voter>
    `
    })
    export class VoteTakerComponent {
    agreed = 0;
    disagreed = 0;
    voters = ['Narco', 'Celeritas', 'Bombasto'];

    onVoted(agreed: boolean) {
    agreed ? this.agreed++ : this.disagreed++;
    }
    }

    # child
    import { Component, EventEmitter, Input, Output } from '@angular/core';
    @Component({
    selector: 'app-voter',
    template: `
    <h4>{{name}}</h4>
    <button (click)="vote(true)" [disabled]="didVote">Agree</button>
    <button (click)="vote(false)" [disabled]="didVote">Disagree</button>
    `
    })
    export class VoterComponent {
    @Input() name: string;
    @Output() voted = new EventEmitter<boolean>();
    didVote = false;

    vote(agreed: boolean) {
    this.voted.emit(agreed);
    this.didVote = true;
    }
    }
  • parent interacts with child via local variable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    # parent
    import { Component } from '@angular/core';
    @Component({
    selector: 'app-countdown-parent-lv',
    template: `
    <button (click)="timer.start()">Start</button>
    <button (click)="timer.stop()">Stop</button>
    <div class="seconds">{{timer.seconds}}</div>
    <app-countdown-timer #timer></app-countdown-timer>
    `
    })
    export class CountdownLocalVarParentComponent { }

    # child
    import { Component, OnDestroy, OnInit } from '@angular/core';
    @Component({
    selector: 'app-countdown-timer',
    template: '<p>{{message}}</p>'
    })
    export class CountdownTimerComponent implements OnInit, OnDestroy {

    intervalId = 0;
    message = '';
    seconds = 11;

    clearTimer() { clearInterval(this.intervalId); }

    ngOnInit() { this.start(); }
    ngOnDestroy() { this.clearTimer(); }

    start() { this.countDown(); }
    stop() {
    this.clearTimer();
    this.message = `Holding at T-${this.seconds} seconds`;
    }

    private countDown() {
    this.clearTimer();
    this.intervalId = window.setInterval(() => {
    this.seconds -= 1;
    if (this.seconds === 0) {
    this.message = 'Blast off!';
    } else {
    if (this.seconds < 0) { this.seconds = 10; } // reset
    this.message = `T-${this.seconds} seconds and counting`;
    }
    }, 1000);
    }
    }
  • parent calls an @ViewChild()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    # parent
    import { AfterViewInit, ViewChild } from '@angular/core';
    import { Component } from '@angular/core';
    import { CountdownTimerComponent } from './countdown-timer.component';
    @Component({
    selector: 'app-countdown-parent-vc',
    template: `
    <h3>Countdown to Liftoff (via ViewChild)</h3>
    <button (click)="start()">Start</button>
    <button (click)="stop()">Stop</button>
    <div class="seconds">{{ seconds() }}</div>
    <app-countdown-timer></app-countdown-timer>
    `
    })
    export class CountdownViewChildParentComponent implements AfterViewInit {

    @ViewChild(CountdownTimerComponent)
    private timerComponent: CountdownTimerComponent;

    seconds() { return 0; }

    ngAfterViewInit() {
    // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
    // but wait a tick first to avoid one-time devMode
    // unidirectional-data-flow-violation error
    // Angular's unidirectional data flow rule prevents updating the parent view's in the same cycle.
    setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
    }

    start() { this.timerComponent.start(); }
    stop() { this.timerComponent.stop(); }
    }
  • parent and children communicate via a service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    # service
    import { Injectable } from '@angular/core';
    import { Subject } from 'rxjs';
    @Injectable()
    export class MissionService {

    private missionAnnouncedSource = new Subject<string>();
    private missionConfirmedSource = new Subject<string>();

    missionAnnounced$ = this.missionAnnouncedSource.asObservable();
    missionConfirmed$ = this.missionConfirmedSource.asObservable();

    announceMission(mission: string) {
    this.missionAnnouncedSource.next(mission);
    }

    confirmMission(astronaut: string) {
    this.missionConfirmedSource.next(astronaut);
    }
    }

    # parent
    import { Component } from '@angular/core';
    import { MissionService } from './mission.service';
    @Component({
    selector: 'app-mission-control',
    template: `
    <button (click)="announce()">Announce mission</button>
    <app-astronaut *ngFor="let astronaut of astronauts"
    [astronaut]="astronaut">
    </app-astronaut>
    <ul>
    <li *ngFor="let event of history">{{event}}</li>
    </ul>
    `,
    providers: [MissionService]
    })
    export class MissionControlComponent {
    astronauts = ['Lovell', 'Swigert', 'Haise'];
    history: string[] = [];
    missions = ['Fly to the moon!',
    'Fly to mars!',
    'Fly to Vegas!'];
    nextMission = 0;

    constructor(private missionService: MissionService) {
    missionService.missionConfirmed$.subscribe(
    astronaut => {
    this.history.push(`${astronaut} confirmed the mission`);
    });
    }

    announce() {
    const mission = this.missions[this.nextMission++];
    this.missionService.announceMission(mission);
    this.history.push(`Mission "${mission}" announced`);
    if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
    }
    }

    # child
    import { Component, Input, OnDestroy } from '@angular/core';

    import { MissionService } from './mission.service';
    import { Subscription } from 'rxjs';

    @Component({
    selector: 'app-astronaut',
    template: `
    <p>
    {{astronaut}}: <strong>{{mission}}</strong>
    <button
    (click)="confirm()"
    [disabled]="!announced || confirmed">
    Confirm
    </button>
    </p>
    `
    })
    export class AstronautComponent implements OnDestroy {
    @Input() astronaut: string;
    mission = '<no mission announced>';
    confirmed = false;
    announced = false;
    subscription: Subscription;

    constructor(private missionService: MissionService) {
    this.subscription = missionService.missionAnnounced$.subscribe(
    mission => {
    this.mission = mission;
    this.announced = true;
    this.confirmed = false;
    });
    }

    confirm() {
    this.confirmed = true;
    this.missionService.confirmMission(this.astronaut);
    }

    ngOnDestroy() {
    // prevent memory leak when component destroyed
    this.subscription.unsubscribe();
    }
    }

Component Styles

  • using component styles

    • The styles specified in @Component metadata apply only within the template of that component.
    • They are not inherited by any components nested within the template nor by any content projected into the component.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component({
    selector: 'app-root',
    template: `
    <h1>Tour of Heroes</h1>
    `,
    styles: ['h1 { font-weight: normal; }']
    })
    export class HeroAppComponent {
    /* . . . */
    }
  • Special selectors

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # The :host selector is the only way to target the host element. You can't reach the host element from inside the component with other selectors because it's not part of the component's own template. The host element is in a parent component's template.
    :host {
    display: block;
    border: 1px solid black;
    }

    :host(.active) {
    border-width: 3px;
    }

    # looks for a CSS class in any ancestor of the component host element, up to the document root
    :host-context(.theme-light) h2 {
    background-color: #eef;
    }

Dynamic Component loader

  • the anchor directive
1
2
3
4
5
6
7
8
9
10
import { Directive, ViewContainerRef } from '@angular/core';

@Directive({
selector: '[adHost]'
})
export class AdDirective {

constructor(public viewContainerRef: ViewContainerRef) { }

}
  • Resolving components
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { Component, Input, OnInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';

import { AdDirective } from './ad.directive';
import { AdItem } from './ad-item';
import { AdComponent } from './ad.component';

@Component({
selector: 'app-ad-banner',
template: `
<div class="ad-banner-example">
<h3>Advertisements</h3>
<ng-template adHost></ng-template>
</div>
`
})
export class AdBannerComponent implements OnInit, OnDestroy {
@Input() ads: AdItem[];
currentAdIndex = -1;
@ViewChild(AdDirective, { static: true }) adHost: AdDirective;
interval: any;

constructor(private componentFactoryResolver: ComponentFactoryResolver) { }

ngOnInit() {
this.loadComponent();
this.getAds();
}

ngOnDestroy() {
clearInterval(this.interval);
}

loadComponent() {
this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
const adItem = this.ads[this.currentAdIndex];

const componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);

const viewContainerRef = this.adHost.viewContainerRef;
viewContainerRef.clear();

const componentRef = viewContainerRef.createComponent(componentFactory);
(<AdComponent>componentRef.instance).data = adItem.data;
}

getAds() {
this.interval = setInterval(() => {
this.loadComponent();
}, 3000);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Component, OnInit } from '@angular/core';

import { AdService } from './ad.service';
import { AdItem } from './ad-item';

@Component({
selector: 'app-ad-app',
providers: [AdService],
template: `
<div>
<app-ad-banner [ads]="ads"></app-ad-banner>
</div>
`
})
export class AdAppComponent implements OnInit {
ads: AdItem[];

constructor(private adService: AdService) { }

ngOnInit() {
this.ads = this.adService.getAds();
}
}

Templates

not allowed in template statement - event

  • new
  • ++ –
  • += -= *= \=
  • | &
  • Pipe operator
1
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>
HTML attributes vs. DOM property

The distinction between an HTML attribute and a DOM property is key to understanding how Angular binding works. Attributes are defined by HTML. Properties are accessed from DOM (Document Object Model) nodes.

  • A few HTML attributes have 1:1 mapping to properties; for example, id.
  • Some HTML attributes don’t have corresponding properties; for example, aria-*.
  • Some DOM properties don’t have corresponding attributes; for example, textContent.

Template binding works with properties and events, not attributes.

Attributes initialize DOM properties and then they are done. Property values can change; attribute values can’t. There is one exception to this rule. Attributes can be changed by setAttribute(), which re-initializes corresponding DOM properties.

1
2
<input [disabled]="condition ? true : false">
<input [attr.disabled]="condition ? 'disabled' : null">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# element property
<img [src]="heroImageUrl">

# component property
<app-hero-detail [hero]="currentHero"></app-hero-detail>

# directive property
<div [ngClass]="{'special': isSpecial}"></div>

# element event
<button (click)="onSave($event)">Save</button>

# component event
<app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail>

# directive event
<div (myClick)="clicked=$event" clickable>click me</div>

# event and property
<input [(ngModel)]="name">

# attribute
<button [attr.aria-label]="help">help</button>

# class property
<div [class.special]="isSpecial">Special</div>
[class.foo]="hasFoo" ( boolean | undefined | null )
[class]="class1 class2 class3"
[class]={foo: true, bar: false}
[class]=[foo, bar]

# style property
<button [style.color]="isSpecial ? 'red' : 'green'">
[style.width]="100px"
[style.width.px]="100"
[style]="width:100px;height:100px;"
[style]={width:100px, height:100px}
[style]=['width','100px','height','100px']

Property binding

Interpolation is a convenient alternative to property binding in many cases. When rendering data values as strings, there is no technical reason to prefer one form to the other, though readability tends to favor interpolation. However, when setting an element property to a non-string data value, you must use property binding.

Event binding

1
2
3
# without ngModel
<input [value]="currentItem.name"
(input)="currentItem.name=$event.target.value" >
1
2
3
4
5
6
// This component makes a request but it can't actually delete a hero.
@Output() deleteRequest = new EventEmitter<Item>();

delete() {
this.deleteRequest.emit(this.item);
}

Two-way binding [(…)]

The [()] syntax is easy to demonstrate when the element has a settable property called x and a corresponding event named xChange.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
selector: 'app-sizer',
template: `
<div>
<button (click)="dec()" title="smaller">-</button>
<button (click)="inc()" title="bigger">+</button>
<label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>
`,
styleUrls: ['./sizer.component.css']
})
export class SizerComponent {

@Input() size: number | string;
@Output() sizeChange = new EventEmitter<number>();

dec() { this.resize(-1); }
inc() { this.resize(+1); }

resize(delta: number) {
this.size = Math.min(40, Math.max(8, +this.size + delta));
this.sizeChange.emit(this.size);
}

}

# two-way binding size property && sizeChange event
<app-sizer [(size)]="fontSizePx"></app-sizer>
<div [style.font-size.px]="fontSizePx">Resizable Text</div>

# equal to
<app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>

Template reference variable

Angular assigns each template reference variable a value based on where you declare the variable:

  • If you declare the variable on a component, the variable refers to the component instance.
  • If you declare the variable on a standard HTML tag, the variable refers to the element.
  • If you declare the variable on an <ng-template> element, the variable refers to a TemplateRef instance, which represents the template.
  • If the variable specifies a name on the right-hand side, such as #var="ngModel", the variable refers to the directive or component on the element with a matching exportAs name.
1
2
3
4
5
6
7
8
9
10
<input #phone placeholder="phone number" />

<!-- lots of other elements -->

<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>

# alternative syntax
<input ref-fax placeholder="fax number" />
<button (click)="callFax(fax.value)">Fax</button>
1
2
3
4
5
6
7
8
9
10
<form #itemForm="ngForm" (ngSubmit)="onSubmit(itemForm)">
<label for="name">
Name <input class="form-control" name="name" ngModel required />
</label>
<button type="submit">Submit</button>
</form>

<div [hidden]="!itemForm.form.valid">
<p>JSON: {{ itemForm.form.value | json }}</p>
</div>
1
2
3
4
5
6
7
// tslint:disable: no-inputs-metadata-property no-outputs-metadata-property
inputs: ['input1: saveForLaterItem'], // propertyName:alias
outputs: ['outputEvent1: saveForLaterEvent']
// tslint:disable: no-inputs-metadata-property no-outputs-metadata-property

@Input('wishListItem') input2: string; // @Input(alias)
@Output('wishEvent') outputEvent2 = new EventEmitter<string>(); // @Output(alias) propertyName = ...
1
2
3
4
5
6
7
8
9
# safe navigation operator
<p>The item name is: {{item?.name}}</p>
a?.b?.c?.d

# non-null operator
<p>The item's color is: {{item.color!.toUpperCase()}}</p>

<p>The item's undeclared best by date is: {{$any(item).bestByDate}}</p>
<p>The item's undeclared best by date is: {{$any(this).bestByDate}}</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component } from '@angular/core';

@Component({
selector: 'app-svg',
template: `
<svg>
<g>
<rect x="0" y="0" width="100" height="100" [attr.fill]="fillColor" (click)="changeColor()" />
<text x="120" y="50">click the rectangle to change the fill color</text>
</g>
</svg>
`,
styleUrls: ['./svg.component.css']
})
export class SvgComponent {
fillColor = 'rgb(255, 0, 0)';

changeColor() {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
this.fillColor = `rgb(${r}, ${g}, ${b})`;
}
}

Directives

Built-in directives

  • Attribute directives
    • NgClass
    • NgStyle
    • NgModel
    • NgSwitch
  • structural directives
    • NgIf
    • NgFor
    • NgSwitchCase
    • NgSwitchDefault
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- toggle the "special" class on/off with a property -->
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>

currentStyles: {};
/* . . . */
setCurrentStyles() {
// CSS styles: set per current state of component properties
this.currentStyles = {
'font-style': this.canSave ? 'italic' : 'normal',
'font-weight': !this.isUnchanged ? 'bold' : 'normal',
'font-size': this.isSpecial ? '24px' : '12px'
};
}
<div [ngStyle]="currentStyles">
This div is initially italic, normal weight, and extra large (24px).
</div>
1
2
3
4
5
6
# ngModel only works for element supported by a ControlValueAccessor
<input [(ngModel)]="currentItem.name" id="example-ngModel">

<input [value]="currentItem.name" (input)="currentItem.name=$event.target.value" id="without">

<input [ngModel]="currentItem.name" (ngModelChange)="currentItem.name=$event" id="example-change">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>

<div *ngFor="let item of items">{{item.name}}</div>
<div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>

trackByItems(index: number, item: Item): number { return item.id; }
<div *ngFor="let item of items; trackBy: trackByItems">
({{item.id}}) {{item.name}}
</div>

<div [ngSwitch]="currentItem.feature">
<app-stout-item *ngSwitchCase="'stout'" [item]="currentItem"></app-stout-item>
<app-device-item *ngSwitchCase="'slim'" [item]="currentItem"></app-device-item>
<app-lost-item *ngSwitchCase="'vintage'" [item]="currentItem"></app-lost-item>
<app-best-item *ngSwitchCase="'bright'" [item]="currentItem"></app-best-item>
<!-- . . . -->
<app-unknown-item *ngSwitchDefault [item]="currentItem"></app-unknown-item>
</div>

Attribute Directives

There are three kinds of directives in Angular:

  1. Components—directives with a template.
  2. Structural directives—change the DOM layout by adding and removing DOM elements.
  3. Attribute directives—change the appearance or behavior of an element, component, or another directive.