docs(template-syntax/structural-directives): refresh both guides for style, accuracy, understanding (#3110)

Move details of structural directives from template-syntax to structural-directives guide
Add <ng-container> to structural-directives
Fix samples in both guides
Touch up glossary
Better conformance to google doc guidelines: we->you
closes #2303, #2885
This commit is contained in:
Ward Bell
2017-02-06 19:06:13 -08:00
committed by GitHub
parent 390e28080c
commit f24b830c8a
60 changed files with 2820 additions and 1378 deletions
@@ -0,0 +1,17 @@
a.to-toc { margin: 30px 0; }
button { font-size: 100%; margin: 0 2px; }
div[clickable] {cursor: pointer; max-width: 200px; margin: 16px 0}
#noTrackByCnt, #withTrackByCnt {color: darkred; max-width: 450px; margin: 4px;}
img {height: 100px;}
.box {border: 1px solid black; padding: 6px; max-width: 450px;}
.child-div {margin-left: 1em; font-weight: normal}
.context {margin-left: 1em;}
.hidden {display: none}
.parent-div {margin-top: 1em; font-weight: bold}
.special {font-weight:bold; font-size: x-large}
.bad {color: red;}
.saveable {color: limegreen;}
.curly, .modified {font-family: "Brush Script MT"}
.toe {margin-left: 1em; font-style: italic;}
little-hero {color:blue; font-size: smaller; background-color: Turquoise }
.to-toc {margin-top: 10px; display: block}
@@ -2,6 +2,8 @@
<a id="toc"></a>
<h1>Template Syntax</h1>
<a href="#interpolation">Interpolation</a><br>
<a href="#expression-context">Expression context</a><br>
<a href="#statement-context">Statement context</a><br>
<a href="#mental-model">Mental Model</a><br>
<a href="#buttons">Buttons</a><br>
<a href="#prop-vs-attrib">Properties vs. Attributes</a><br>
@@ -22,15 +24,14 @@
<a href="#ngClass">NgClass Binding</a><br>
<a href="#ngStyle">NgStyle Binding</a><br>
<a href="#ngIf">NgIf</a><br>
<a href="#ngSwitch">NgSwitch</a><br>
<a href="#ngFor">NgFor</a><br>
<div style="margin-left:8px">
<a href="#ngFor-index">NgFor with index</a><br>
<a href="#ngFor-trackBy">NgFor with trackBy</a><br>
</div>
<a href="#ngSwitch">NgSwitch</a><br>
</div>
<br>
<a href="#star-prefix">* prefix and &lt;template&gt;</a><br>
<a href="#ref-vars">Template reference variables</a><br>
<a href="#inputs-and-outputs">Inputs and outputs</a><br>
<a href="#pipes">Pipes</a><br>
@@ -41,7 +42,7 @@
<hr><h2 id="interpolation">Interpolation</h2>
<!-- #docregion first-interpolation -->
<p>My current hero is {{currentHero.firstName}}</p>
<p>My current hero is {{currentHero.name}}</p>
<!-- #enddocregion first-interpolation -->
<!-- #docregion title+image -->
@@ -63,6 +64,68 @@
<a class="to-toc" href="#toc">top</a>
<hr><h2 id="expression-context">Expression context</h2>
<p>Component expression context (&#123;&#123;title&#125;&#125;, [hidden]="isUnchanged")</p>
<div class="context">
<!-- #docregion context-component-expression -->
{{title}}
<span [hidden]="isUnchanged">changed</span>
<!-- #enddocregion context-component-expression -->
</div>
<p>Template input variable expression context (let hero)</p>
<!-- template hides the following; plenty of examples later -->
<template>
<!-- #docregion context-var -->
<div *ngFor="let hero of heroes">{{hero.name}}</div>
<!-- #enddocregion context-var -->
</template>
<p>Template reference variable expression context (#heroInput)</p>
<div (keyup)="0" class="context">
Type something:
<!-- #docregion context-var -->
<input #heroInput> {{heroInput.value}}
<!-- #enddocregion context-var -->
</div>
<a class="to-toc" href="#toc">top</a>
<hr><h2 id="statement-context">Statement context</h2>
<p>Component statement context ( (click)="onSave() )
<div class="context">
<!-- #docregion context-component-statement -->
<button (click)="deleteHero()">Delete hero</button>
<!-- #enddocregion context-component-statement -->
</div>
<p>Template $event statement context</p>
<div class="context">
<!-- #docregion context-var-statement -->
<button (click)="onSave($event)">Save</button>
<!-- #enddocregion context-var-statement -->
</div>
<p>Template input variable statement context (let hero)</p>
<!-- template hides the following; plenty of examples later -->
<div class="context">
<!-- #docregion context-var-statement -->
<button *ngFor="let hero of heroes" (click)="deleteHero(hero)">{{hero.name}}</button>
<!-- #enddocregion context-var-statement -->
</div>
<p>Template reference variable statement context (#heroForm)</p>
<div class="context">
<!-- #docregion context-var-statement -->
<form #heroForm (ngSubmit)="onSubmit(heroForm)"> ... </form>
<!-- #enddocregion context-var-statement -->
</div>
<a class="to-toc" href="#toc">top</a>
<!-- New Mental Model -->
<hr><h2 id="mental-model">New Mental Model</h2>
@@ -105,16 +168,17 @@
<!-- #docregion event-binding-syntax-1 -->
<button (click) = "onSave()">Save</button>
<hero-detail (deleteRequest)="deleteHero()"></hero-detail>
<div (myClick)="clicked=$event">click me</div>
<div (myClick)="clicked=$event" clickable>click me</div>
<!-- #enddocregion event-binding-syntax-1 -->
{{clicked}}
<br><br>
<div>
Hero Name:
<!-- #docregion 2-way-binding-syntax-1 -->
<input [(ngModel)]="heroName">
<!-- #enddocregion 2-way-binding-syntax-1 -->
Hero Name: {{heroName}}
{{heroName}}
</div>
<br><br>
@@ -205,6 +269,10 @@ button</button>
<!-- #enddocregion property-binding-vs-interpolation -->
<!-- #docregion property-binding-vs-interpolation-sanitization -->
<!--
Angular generates warnings for these two lines as it sanitizes them
WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).
-->
<p><span>"{{evilTitle}}" is the <i>interpolated</i> evil title.</span></p>
<p>"<span [innerHTML]="evilTitle"></span>" is the <i>property bound</i> evil title.</p>
<!-- #enddocregion property-binding-vs-interpolation-sanitization -->
@@ -307,7 +375,7 @@ button</button>
<!-- #docregion event-binding-3 -->
<!-- `myClick` is an event on the custom `ClickDirective` -->
<!-- #docregion myClick -->
<div (myClick)="clickMessage=$event">click with myClick</div>
<div (myClick)="clickMessage=$event" clickable>click with myClick</div>
<!-- #enddocregion myClick -->
<!-- #enddocregion event-binding-3 -->
{{clickMessage}}
@@ -326,26 +394,25 @@ button</button>
</big-hero-detail>
<!-- #docregion event-binding-bubbling -->
<div class="parent-div" (click)="onClickMe($event)">Click me
<div class="parent-div" (click)="onClickMe($event)" clickable>Click me
<div class="child-div">Click me too!</div>
</div>
<!-- #enddocregion event-binding-bubbling -->
<br><br>
<!-- #docregion event-binding-no-propagation -->
<!-- Will save only once -->
<div (click)="onSave()">
<div (click)="onSave()" clickable>
<button (click)="onSave()">Save, no propagation</button>
</div>
<!-- #enddocregion event-binding-no-propagation -->
<br><br>
<!-- #docregion event-binding-propagation -->
<!-- Will save twice -->
<div (click)="onSave()">
<div (click)="onSave()" clickable>
<button (click)="onSave() || true">Save w/ propagation</button>
</div>
<!-- #enddocregion event-binding-propagation -->
<br><br>
<a class="to-toc" href="#toc">top</a>
<hr><h2 id="two-way">Two-way Binding</h2>
@@ -363,7 +430,6 @@ button</button>
<my-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></my-sizer>
<!-- #enddocregion two-way-2 -->
</div>
<br><br>
<a class="to-toc" href="#toc">top</a>
@@ -371,38 +437,37 @@ button</button>
passing the changed display value to the event handler via `$event` -->
<hr><h2 id="ngModel">NgModel (two-way) Binding</h2>
<h3>Result: {{currentHero.firstName}}</h3>
<h3>Result: {{currentHero.name}}</h3>
<!-- #docregion without-NgModel -->
<input [value]="currentHero.firstName"
(input)="currentHero.firstName=$event.target.value" >
<input [value]="currentHero.name"
(input)="currentHero.name=$event.target.value" >
<!-- #enddocregion without-NgModel -->
without NgModel
<br>
<!-- #docregion NgModel-1 -->
<input [(ngModel)]="currentHero.firstName">
<input [(ngModel)]="currentHero.name">
<!-- #enddocregion NgModel-1 -->
[(ngModel)]
<br>
<!-- #docregion NgModel-2 -->
<input bindon-ngModel="currentHero.firstName">
<input bindon-ngModel="currentHero.name">
<!-- #enddocregion NgModel-2 -->
bindon-ngModel
<br>
<!-- #docregion NgModel-3 -->
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="currentHero.firstName=$event">
[ngModel]="currentHero.name"
(ngModelChange)="currentHero.name=$event">
<!-- #enddocregion NgModel-3 -->
(ngModelChange) = "...firstName=$event"
(ngModelChange) = "...name=$event"
<br>
<!-- #docregion NgModel-4 -->
<input
[ngModel]="currentHero.firstName"
(ngModelChange)="setUpperCaseFirstName($event)">
[ngModel]="currentHero.name"
(ngModelChange)="setUppercaseName($event)">
<!-- #enddocregion NgModel-4 -->
(ngModelChange) = "setUpperCaseFirstName($event)"
<br>
(ngModelChange) = "setUppercaseName($event)"
<a class="to-toc" href="#toc">top</a>
@@ -417,7 +482,7 @@ bindon-ngModel
<!-- not used in chapter -->
<br>
<label>saveable <input type="checkbox" [(ngModel)]="canSave"></label> |
<label>modified: <input type="checkbox" [value]="!isUnchanged" (change)="isUnchanged=!isUnchanged"></label> |
<label>modified: <input type="checkbox" [value]="!isUnchanged" (change)="isUnchanged=!isUnchanged"></label> |
<label>special: <input type="checkbox" [(ngModel)]="isSpecial"></label>
<button (click)="setCurrentClasses()">Refresh currentClasses</button>
<br><br>
@@ -462,7 +527,6 @@ bindon-ngModel
This div should be {{ canSave ? "italic": "plain"}},
{{ isUnchanged ? "normal weight" : "bold" }} and,
{{ isSpecial ? "extra large": "normal size"}} after clicking "refresh".</div>
<br>
<a class="to-toc" href="#toc">top</a>
@@ -470,21 +534,17 @@ bindon-ngModel
<hr><h2 id="ngIf">NgIf Binding</h2>
<!-- #docregion NgIf-1 -->
<div *ngIf="currentHero">Hello, {{currentHero.firstName}}</div>
<hero-detail *ngIf="isActive"></hero-detail>
<!-- #enddocregion NgIf-1 -->
<!-- #docregion NgIf-2 -->
<!-- because of the ngIf guard
`nullHero.firstName` never has a chance to fail -->
<div *ngIf="nullHero">Hello, {{nullHero.firstName}}</div>
<!-- Hero Detail is not in the DOM because isActive is false-->
<hero-detail *ngIf="isActive"></hero-detail>
<div *ngIf="currentHero">Hello, {{currentHero.name}}</div>
<div *ngIf="nullHero">Hello, {{nullHero.name}}</div>
<!-- #enddocregion NgIf-2 -->
<!-- NgIf binding with template (no *) -->
<template [ngIf]="currentHero">Add {{currentHero.firstName}} with template</template>
<template [ngIf]="currentHero">Add {{currentHero.name}} with template</template>
<!-- Does not show because isActive is false! -->
<div>Hero Detail removed from DOM (via template) because isActive is false</div>
@@ -506,170 +566,117 @@ bindon-ngModel
<a class="to-toc" href="#toc">top</a>
<!-- NgSwitch binding -->
<hr><h2 id="ngSwitch">NgSwitch Binding</h2>
<fieldset #toePicker (click)="toeChooser(toePicker)" >
<input type="radio" name="toes" value="Eenie">Eenie
<input type="radio" name="toes" value="Meanie">Meanie
<input type="radio" name="toes" value="Miney">Miney
<input type="radio" name="toes" value="Moe">Moe
<input type="radio" name="toes" value="???">???
</fieldset>
<div class="toe">
<div *ngIf="!toeChoice">Pick a toe</div>
<div *ngIf="toeChoice">
You picked ...
<!-- #docregion NgSwitch, NgSwitch-expanded -->
<span [ngSwitch]="toeChoice">
<!-- #enddocregion NgSwitch -->
<!-- with *NgSwitch -->
<!-- #docregion NgSwitch -->
<span *ngSwitchCase="'Eenie'">Eenie</span>
<span *ngSwitchCase="'Meanie'">Meanie</span>
<span *ngSwitchCase="'Miney'">Miney</span>
<span *ngSwitchCase="'Moe'">Moe</span>
<span *ngSwitchDefault>other</span>
<!-- #enddocregion NgSwitch -->
<!-- with <template> -->
<template [ngSwitchCase]="'Eenie'"><span>Eenie</span></template>
<template [ngSwitchCase]="'Meanie'"><span>Meanie</span></template>
<template [ngSwitchCase]="'Miney'"><span>Miney</span></template>
<template [ngSwitchCase]="'Moe'"><span>Moe</span></template>
<template ngSwitchDefault><span>other</span></template>
<!-- #docregion NgSwitch -->
</span>
<!-- #enddocregion NgSwitch, NgSwitch-expanded -->
</div>
</div>
<a class="to-toc" href="#toc">top</a>
<!-- NgFor binding -->
<hr><h2 id="ngFor">NgFor Binding</h2>
<div class="box">
<!-- #docregion NgFor-1 -->
<div *ngFor="let hero of heroes">{{hero.fullName}}</div>
<!-- #enddocregion NgFor-1 -->
<!-- #docregion NgFor-1, NgFor-1-2 -->
<div *ngFor="let hero of heroes">{{hero.name}}</div>
<!-- #enddocregion NgFor-1, NgFor-1-2 -->
</div>
<br>
<div class="box">
<!-- *ngFor w/ hero-detail Component -->
<!-- #docregion NgFor-2 -->
<!-- #docregion NgFor-2, NgFor-1-2 -->
<hero-detail *ngFor="let hero of heroes" [hero]="hero"></hero-detail>
<!-- #enddocregion NgFor-2 -->
<!-- #enddocregion NgFor-2, NgFor-1-2 -->
</div>
<a class="to-toc" href="#toc">top</a>
<h4 id="ngFor-index">NgFor with index</h4>
<h4 id="ngFor-index">*ngFor with index</h4>
<p>with <i>semi-colon</i> separator</p>
<div class="box">
<!-- #docregion NgFor-3 -->
<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.fullName}}</div>
<div *ngFor="let hero of heroes; let i=index">{{i + 1}} - {{hero.name}}</div>
<!-- #enddocregion NgFor-3 -->
</div>
<p>with <i>comma</i> separator</p>
<div class="box">
<!-- Ex: "1 - Hercules Son of Zeus"" -->
<div *ngFor="let hero of heroes, let i=index">{{i + 1}} - {{hero.fullName}}</div>
<!-- Ex: "1 - Hercules" -->
<div *ngFor="let hero of heroes, let i=index">{{i + 1}} - {{hero.name}}</div>
</div>
<a class="to-toc" href="#toc">top</a>
<h4 id="ngFor-trackBy">NgForTrackBy</h4>
<button (click)="refreshHeroes()">Refresh heroes</button>
<p>First hero: <input [(ngModel)]="heroes[0].firstName"></p>
<h4 id="ngFor-trackBy">*ngFor trackBy</h4>
<button (click)="resetHeroes()">Reset heroes</button>
<button (click)="changeIds()">Change ids</button>
<button (click)="clearTrackByCounts()">Clear counts</button>
<p><i>without</i> trackBy</p>
<div #noTrackBy class="box">
<!-- #docregion NgForTrackBy-1 -->
<div *ngFor="let hero of heroes">({{hero.id}}) {{hero.fullName}}</div>
<!-- #enddocregion NgForTrackBy-1 -->
</div>
<div id="noTrackByCnt" *ngIf="heroesNoTrackByChangeCount" style="background-color:bisque">
Hero DOM elements change #<span style="background-color:gold">{{heroesNoTrackByChangeCount}}</span> without trackBy
<div class="box">
<div #noTrackBy *ngFor="let hero of heroes">({{hero.id}}) {{hero.name}}</div>
<div id="noTrackByCnt" *ngIf="heroesNoTrackByCount" >
Hero DOM elements change #{{heroesNoTrackByCount}} without trackBy
</div>
</div>
<p>with trackBy and <i>semi-colon</i> separator</p>
<div #withTrackBy class="box">
<!-- #docregion NgForTrackBy-2 -->
<div *ngFor="let hero of heroes; trackBy:trackByHeroes">({{hero.id}}) {{hero.fullName}}</div>
<!-- #enddocregion NgForTrackBy-2 -->
<p>with trackBy</p>
<div class="box">
<div #withTrackBy *ngFor="let hero of heroes; trackBy: trackByHeroes">({{hero.id}}) {{hero.name}}</div>
<div id="withTrackByCnt" *ngIf="heroesWithTrackByCount">
Hero DOM elements change #{{heroesWithTrackByCount}} with trackBy
</div>
</div>
<div id="withTrackByCnt" *ngIf="heroesWithTrackByChangeCount" style="background-color:bisque">
Hero DOM elements change #<span style="background-color:gold">{{heroesWithTrackByChangeCount}}</span> with trackBy
<br><br><br>
<p>with trackBy and <i>semi-colon</i> separator</p>
<div class="box">
<!-- #docregion trackBy -->
<div *ngFor="let hero of heroes; trackBy: trackByHeroes">
({{hero.id}}) {{hero.name}}
</div>
<!-- #enddocregion trackBy -->
</div>
<p>with trackBy and <i>comma</i> separator</p>
<div class="box">
<div *ngFor="let hero of heroes, trackBy:trackByHeroes">({{hero.id}}) {{hero.fullName}}</div>
<div *ngFor="let hero of heroes, trackBy: trackByHeroes">({{hero.id}}) {{hero.name}}</div>
</div>
<p>with trackBy and <i>space</i> separator</p>
<div class="box">
<div *ngFor="let hero of heroes trackBy:trackByHeroes">({{hero.id}}) {{hero.fullName}}</div>
<div *ngFor="let hero of heroes trackBy: trackByHeroes">({{hero.id}}) {{hero.name}}</div>
</div>
<p>with <i>generic</i> trackById function</p>
<div class="box">
<div *ngFor="let hero of heroes, trackBy:trackById">({{hero.id}}) {{hero.fullName}}</div>
<div *ngFor="let hero of heroes, trackBy: trackById">({{hero.id}}) {{hero.name}}</div>
</div>
<a class="to-toc" href="#toc">top</a>
<!-- * and template -->
<hr><h2 id="star-prefix">* prefix and &lt;template&gt;</h2>
<!-- NgSwitch binding -->
<hr><h2 id="ngSwitch">NgSwitch Binding</h2>
<h3>*ngIf expansion</h3>
<p><i>*ngIf</i></p>
<!-- #docregion Template-1 -->
<hero-detail *ngIf="currentHero" [hero]="currentHero"></hero-detail>
<!-- #enddocregion Template-1 -->
<div>Pick your favorite hero</div>
<p>
<ng-container *ngFor="let h of heroes">
<label>
<input type="radio" name="heroes" [(ngModel)]="currentHero" [value]="h">{{h.name}}
</label>
</ng-container>
</p>
<p><i>expand to template = "..."</i></p>
<!-- #docregion Template-2a -->
<hero-detail template="ngIf:currentHero" [hero]="currentHero"></hero-detail>
<!-- #enddocregion Template-2a -->
<p><i>expand to &lt;template&gt;</i></p>
<!-- #docregion Template-2 -->
<template [ngIf]="currentHero">
<hero-detail [hero]="currentHero"></hero-detail>
</template>
<!-- #enddocregion Template-2 -->
<h3>*ngFor expansion</h3>
<p><i>*ngFor</i></p>
<!-- *ngFor w/ hero-detail Component -->
<!-- #docregion Template-3a -->
<hero-detail *ngFor="let hero of heroes; trackBy:trackByHeroes" [hero]="hero"></hero-detail>
<!-- #enddocregion Template-3a -->
<p><i>expand to template = "..."</i></p>
<div class="box">
<!-- ngFor w/ hero-detail Component and a template "attribute" directive -->
<!-- #docregion Template-3 -->
<hero-detail template="ngFor let hero of heroes; trackBy:trackByHeroes" [hero]="hero"></hero-detail>
<!-- #enddocregion Template-3 -->
</div>
<p><i>expand to &lt;template&gt;</i></p>
<div class="box">
<!-- ngFor w/ hero-detail Component inside a template element -->
<!-- #docregion Template-4 -->
<template ngFor let-hero [ngForOf]="heroes" [ngForTrackBy]="trackByHeroes">
<hero-detail [hero]="hero"></hero-detail>
</template>
<!-- #enddocregion Template-4 -->
<!-- #docregion NgSwitch -->
<div [ngSwitch]="currentHero.emotion">
<happy-hero *ngSwitchCase="'happy'" [hero]="currentHero"></happy-hero>
<sad-hero *ngSwitchCase="'sad'" [hero]="currentHero"></sad-hero>
<confused-hero *ngSwitchCase="'confused'" [hero]="currentHero"></confused-hero>
<!-- #enddocregion NgSwitch -->
<!-- #docregion NgSwitch-div -->
<div *ngSwitchCase="'confused'">Are you as confused as {{currentHero.name}}?</div>
<!-- #enddocregion NgSwitch-div -->
<!-- #docregion NgSwitch -->
<unknown-hero *ngSwitchDefault [hero]="currentHero"></unknown-hero>
</div>
<!-- #enddocregion NgSwitch -->
<a class="to-toc" href="#toc">top</a>
@@ -677,34 +684,27 @@ bindon-ngModel
<hr><h2 id="ref-vars">Template reference variables</h2>
<!-- #docregion ref-phone -->
<!-- phone refers to the input element; pass its `value` to an event handler -->
<!-- #docregion ref-var -->
<input #phone placeholder="phone number">
<button (click)="callPhone(phone.value)">Call</button>
<!-- #enddocregion ref-var -->
<!-- fax refers to the input element; pass its `value` to an event handler -->
<input ref-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>
<!-- lots of other elements -->
<!-- phone refers to the input element; pass its `value` to an event handler -->
<button (click)="callPhone(phone.value)">Call</button>
<!-- #enddocregion ref-phone -->
<h4>Example Form</h4>
<!-- #docregion ref-form -->
<!-- #docregion ref-form-a -->
<form (ngSubmit)="onSubmit(theForm)" #theForm="ngForm">
<!-- #enddocregion ref-form-a -->
<div class="form-group">
<label for="name">Name</label>
<input class="form-control" name="name" required [(ngModel)]="currentHero.firstName">
</div>
<!-- #docregion ref-form-a -->
<button type="submit" [disabled]="!theForm.form.valid">Submit</button>
</form>
<!-- #enddocregion ref-form-a -->
<!-- #enddocregion ref-form -->
<br><br>
<!-- #docregion ref-fax -->
<input ref-fax placeholder="fax number">
<button (click)="callFax(fax.value)">Fax</button>
<!-- #enddocregion ref-fax -->
<!-- btn refers to the button element; show its disabled state -->
<button #btn disabled [innerHTML]="'disabled by attribute: '+btn.disabled"></button>
<h4>Example Form</h4>
<hero-form [hero]="currentHero"></hero-form>
<a class="to-toc" href="#toc">top</a>
<!-- inputs and output -->
@@ -720,7 +720,7 @@ bindon-ngModel
</hero-detail>
<!-- #enddocregion io-2 -->
<div (myClick)="clickMessage2=$event">myClick2</div>
<div (myClick)="clickMessage2=$event" clickable>myClick2</div>
{{clickMessage2}}
<a class="to-toc" href="#toc">top</a>
@@ -769,43 +769,42 @@ bindon-ngModel
<div>
<!-- #docregion safe-2 -->
The current hero's name is {{currentHero?.firstName}}
The current hero's name is {{currentHero?.name}}
<!-- #enddocregion safe-2 -->
</div>
<div>
<!-- #docregion safe-3 -->
The current hero's name is {{currentHero.firstName}}
The current hero's name is {{currentHero.name}}
<!-- #enddocregion safe-3 -->
</div>
<!--
The null hero's name is {{nullHero.firstName}}
The null hero's name is {{nullHero.name}}
See console log:
TypeError: Cannot read property 'firstName' of null in [null]
TypeError: Cannot read property 'name' of null in [null]
-->
<!-- #docregion safe-4 -->
<!--No hero, div not displayed, no error -->
<div *ngIf="nullHero">The null hero's name is {{nullHero.firstName}}</div>
<div *ngIf="nullHero">The null hero's name is {{nullHero.name}}</div>
<!-- #enddocregion safe-4 -->
<div>
<!-- #docregion safe-5 -->
The null hero's name is {{nullHero && nullHero.firstName}}
The null hero's name is {{nullHero && nullHero.name}}
<!-- #enddocregion safe-5 -->
</div>
<div>
<!-- #docregion safe-6 -->
<!-- No hero, no problem! -->
The null hero's name is {{nullHero?.firstName}}
The null hero's name is {{nullHero?.name}}
<!-- #enddocregion safe-6 -->
</div>
<a class="to-toc" href="#toc">top</a>
<!-- TODO: discuss this in the Style binding section -->
@@ -2,7 +2,6 @@
// #docplaster
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Hero } from './hero';
@@ -19,20 +18,26 @@ export enum Color {Red, Green, Blue};
@Component({
moduleId: module.id,
selector: 'my-app',
templateUrl: './app.component.html'
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements AfterViewInit, OnInit {
ngOnInit() {
this.refreshHeroes();
this.resetHeroes();
this.setCurrentClasses();
this.setCurrentStyles();
}
ngAfterViewInit() {
this.detectNgForTrackByEffects();
// Detect effects of NgForTrackBy
trackChanges(this.heroesNoTrackBy, () => this.heroesNoTrackByCount += 1);
trackChanges(this.heroesWithTrackBy, () => this.heroesWithTrackByCount += 1);
}
@ViewChildren('noTrackBy') heroesNoTrackBy: QueryList<ElementRef>;
@ViewChildren('withTrackBy') heroesWithTrackBy: QueryList<ElementRef>;
actionName = 'Go for it';
alert = alerter;
badCurly = 'bad curly';
@@ -42,20 +47,40 @@ export class AppComponent implements AfterViewInit, OnInit {
callPhone(value: string) {this.alert(`Calling ${value} ...`); }
canSave = true;
changeIds() {
this.resetHeroes();
this.heroes.forEach(h => h.id += 10 * this.heroIdIncrement++);
this.heroesWithTrackByCountReset = -1;
}
clearTrackByCounts() {
const trackByCountReset = this.heroesWithTrackByCountReset;
this.resetHeroes();
this.heroesNoTrackByCount = -1;
this.heroesWithTrackByCount = trackByCountReset;
this.heroIdIncrement = 1;
}
clicked = '';
clickMessage = '';
clickMessage2 = '';
Color = Color;
color = Color.Red;
colorToggle() {this.color = (this.color === Color.Red) ? Color.Blue : Color.Red; }
currentHero = Hero.MockHeroes[0];
currentHero: Hero;
deleteHero(hero: Hero) {
this.alert('Deleted hero: ' + (hero && hero.firstName));
this.alert(`Delete ${hero ? hero.name : 'the hero'}.`);
}
// #docregion evil-title
evilTitle = 'Template <script>alert("evil never sleeps")</script>Syntax';
// #enddocregion evil-title
fontSizePx = 16;
title = 'Template Syntax';
getStyles(el: Element) {
@@ -69,22 +94,26 @@ export class AppComponent implements AfterViewInit, OnInit {
getVal() { return this.val; }
hero: Hero; // defined to demonstrate template context precedence
heroes: Hero[];
// trackBy change counting
heroesNoTrackByCount = 0;
heroesWithTrackByCount = 0;
heroesWithTrackByCountReset = 0;
heroIdIncrement = 1;
// heroImageUrl = 'http://www.wpclipart.com/cartoon/people/hero/hero_silhoutte_T.png';
// Public Domain terms of use: http://www.wpclipart.com/terms.html
heroImageUrl = 'images/hero.png';
// iconUrl = 'https://angular.io/resources/images/logos/standard/shield-large.png';
clicked = '';
clickMessage = '';
clickMessage2 = '';
iconUrl = 'images/ng-logo.png';
isActive = false;
isSpecial = true;
isUnchanged = true;
nullHero: Hero = null; // or undefined
nullHero: Hero = null;
onCancel(event: KeyboardEvent) {
let evtMsg = event ? ' Event target is ' + (<HTMLElement>event.target).innerHTML : '';
@@ -101,26 +130,20 @@ export class AppComponent implements AfterViewInit, OnInit {
this.alert('Saved.' + evtMsg);
}
onSubmit(form: NgForm) {
let evtMsg = form.valid ?
' Form value is ' + JSON.stringify(form.value) :
' Form is invalid';
this.alert('Form submitted.' + evtMsg);
}
onSubmit() { /* referenced but not used */}
product = {
name: 'frimfram',
price: 42
};
// #docregion refresh-heroes
// update this.heroes with fresh set of cloned heroes
refreshHeroes() {
this.heroes = Hero.MockHeroes.map(hero => Hero.clone(hero));
// updates with fresh set of cloned heroes
resetHeroes() {
this.heroes = Hero.heroes.map(hero => hero.clone());
this.currentHero = this.heroes[0];
this.heroesWithTrackByCountReset = 0;
}
// #enddocregion refresh-heroes
// #docregion same-as-it-ever-was
private samenessCount = 5;
moreOfTheSame() { this.samenessCount++; };
get sameAsItEverWas() {
@@ -131,11 +154,9 @@ export class AppComponent implements AfterViewInit, OnInit {
// return {id:id, text: 'same as it ever was ...'};
// });
}
// #enddocregion same-as-it-ever-was
setUpperCaseFirstName(firstName: string) {
// console.log(firstName);
this.currentHero.firstName = firstName.toUpperCase();
setUppercaseName(name: string) {
this.currentHero.name = name.toUpperCase();
}
// #docregion setClasses
@@ -162,69 +183,31 @@ export class AppComponent implements AfterViewInit, OnInit {
}
// #enddocregion setStyles
toeChoice = '';
toeChooser(picker: HTMLFieldSetElement) {
let choices = picker.children;
for (let i = 0; i < choices.length; i++) {
let choice = <HTMLInputElement>choices[i];
if (choice.checked) {return this.toeChoice = choice.value; }
}
}
// #docregion trackByHeroes
trackByHeroes(index: number, hero: Hero) { return hero.id; }
trackByHeroes(index: number, hero: Hero): number { return hero.id; }
// #enddocregion trackByHeroes
// #docregion trackById
trackById(index: number, item: any): string { return item['id']; }
trackById(index: number, item: any): number { return item['id']; }
// #enddocregion trackById
val = 2;
// villainImageUrl = 'http://www.clker.com/cliparts/u/s/y/L/x/9/villain-man-hi.png'
// Public Domain terms of use http://www.clker.com/disclaimer.html
villainImageUrl = 'images/villain.png';
//////// Detect effects of NgForTrackBy ///////////////
@ViewChildren('noTrackBy') childrenNoTrackBy: QueryList<ElementRef>;
@ViewChildren('withTrackBy') childrenWithTrackBy: QueryList<ElementRef>;
private _oldNoTrackBy: HTMLElement[];
private _oldWithTrackBy: HTMLElement[];
heroesNoTrackByChangeCount = 0;
heroesWithTrackByChangeCount = 0;
private detectNgForTrackByEffects() {
this._oldNoTrackBy = toArray(this.childrenNoTrackBy);
this._oldWithTrackBy = toArray(this.childrenWithTrackBy);
this.childrenNoTrackBy.changes.subscribe((changes: any) => {
let newNoTrackBy = toArray(changes);
let isSame = this._oldNoTrackBy.every((v: any, i: number) => v === newNoTrackBy[i]);
if (!isSame) {
this._oldNoTrackBy = newNoTrackBy;
this.heroesNoTrackByChangeCount++;
}
});
this.childrenWithTrackBy.changes.subscribe((changes: any) => {
let newWithTrackBy = toArray(changes);
let isSame = this._oldWithTrackBy.every((v: any, i: number) => v === newWithTrackBy[i]);
if (!isSame) {
this._oldWithTrackBy = newWithTrackBy;
this.heroesWithTrackByChangeCount++;
}
});
}
///////////////////
}
// helper to convert viewChildren to an array of HTMLElements
function toArray(viewChildren: QueryList<ElementRef>) {
let result: HTMLElement[] = [];
let children = viewChildren.toArray()[0].nativeElement.children;
for (let i = 0; i < children.length; i++) { result.push(children[i]); }
return result;
// helper to track changes to viewChildren
function trackChanges(views: QueryList<ElementRef>, changed: () => void) {
let oldRefs = views.toArray();
views.changes.subscribe((changes: QueryList<ElementRef>) => {
const changedRefs = changes.toArray();
// Is every changed ElemRef the same as old and in the same position
const isSame = oldRefs.every((v, i) => v === changedRefs[i]);
if (!isSame) {
oldRefs = changedRefs;
// wait a tick because called after views are constructed
setTimeout(changed, 0);
}
});
}
@@ -1,18 +1,15 @@
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { FormsModule } from '@angular/forms'; // <--- JavaScript import from Angular
import { AppComponent } from './app.component';
/* Other imports */
@NgModule({
imports: [
BrowserModule,
FormsModule
FormsModule // <--- import into the NgModule
],
declarations: [
AppComponent
],
bootstrap: [ AppComponent ]
/* Other module metadata */
})
export class AppModule { }
@@ -1,11 +1,13 @@
import { NgModule } from '@angular/core';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { BigHeroDetailComponent, HeroDetailComponent } from './hero-detail.component';
import { ClickDirective, ClickDirective2 } from './click.directive';
import { SizerComponent } from './sizer.component';
import { HeroFormComponent } from './hero-form.component';
import { heroSwitchComponents } from './hero-switch.components';
import { SizerComponent } from './sizer.component';
@NgModule({
imports: [
@@ -16,6 +18,8 @@ import { SizerComponent } from './sizer.component';
AppComponent,
BigHeroDetailComponent,
HeroDetailComponent,
HeroFormComponent,
heroSwitchComponents,
ClickDirective,
ClickDirective2,
SizerComponent
@@ -18,7 +18,7 @@ import { Hero } from './hero';
<div>
<img src="{{heroImageUrl}}">
<span [style.text-decoration]="lineThrough">
{{prefix}} {{hero?.fullName}}
{{prefix}} {{hero?.name}}
</span>
<button (click)="delete()">Delete</button>
</div>`
@@ -27,7 +27,7 @@ import { Hero } from './hero';
})
// #enddocregion input-output-2
export class HeroDetailComponent {
hero: Hero = new Hero('', 'Zzzzzzzz'); // default sleeping hero
hero: Hero = new Hero(-1, '', 'Zzzzzzzz'); // default sleeping hero
// heroImageUrl = 'http://www.wpclipart.com/cartoon/people/hero/hero_silhoutte_T.png';
// Public Domain terms of use: http://www.wpclipart.com/terms.html
heroImageUrl = 'images/hero.png';
@@ -50,18 +50,22 @@ export class HeroDetailComponent {
@Component({
selector: 'big-hero-detail',
template: `
<div style="border: 1px solid black; padding:3px">
<img src="{{heroImageUrl}}" style="float:left; margin-right:8px;">
<div><b>{{hero?.fullName}}</b></div>
<div>First: {{hero?.firstName}}</div>
<div>Last: {{hero?.lastName}}</div>
<div class="detail">
<img src="{{heroImageUrl}}">
<div><b>{{hero?.name}}</b></div>
<div>Name: {{hero?.name}}</div>
<div>Emotion: {{hero?.emotion}}</div>
<div>Birthdate: {{hero?.birthdate | date:'longDate'}}</div>
<div>Web: <a href="{{hero?.url}}" target="_blank">{{hero?.url}}</a></div>
<div>Rate/hr: {{hero?.rate | currency:'EUR'}}</div>
<br clear="all">
<button (click)="delete()">Delete</button>
</div>
`
`,
styles: [`
.detail { border: 1px solid black; padding: 4px; max-width: 450px; }
img { float: left; margin-right: 8px; height: 100px; }
`]
})
export class BigHeroDetailComponent extends HeroDetailComponent {
@@ -0,0 +1,16 @@
<div id=heroForm>
<!-- #docregion -->
<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
<div class="form-group">
<label for="name">Name
<input class="form-control" name="name" required [(ngModel)]="hero.name">
</label>
</div>
<button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>
<div [hidden]="!heroForm.form.valid">
{{submitMessage}}
<div>
<!-- #enddocregion -->
</div>
@@ -0,0 +1,31 @@
import { Component, Input, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Hero } from './hero';
@Component({
moduleId: module.id,
selector: 'hero-form',
templateUrl: './hero-form.component.html',
styles: [`
button { margin: 6px 0; }
#heroForm { border: 1px solid black; margin: 20px 0; padding: 8px; max-width: 350px; }
`]
})
export class HeroFormComponent {
@Input() hero: Hero;
@ViewChild('heroForm') form: NgForm;
private _submitMessage = '';
get submitMessage() {
if (!this.form.valid) {
this._submitMessage = '';
}
return this._submitMessage;
}
onSubmit(form: NgForm) {
this._submitMessage = 'Submitted. form value is ' + JSON.stringify(form.value);
}
}
@@ -0,0 +1,42 @@
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'happy-hero',
template: `Wow. You like {{hero.name}}. What a happy hero ... just like you.`
})
export class HappyHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'sad-hero',
template: `You like {{hero.name}}? Such a sad hero. Are you sad too?`
})
export class SadHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'confused-hero',
template: `Are you as confused as {{hero.name}}?`
})
export class ConfusedHeroComponent {
@Input() hero: Hero;
}
@Component({
selector: 'unknown-hero',
template: `{{message}}`
})
export class UnknownHeroComponent {
@Input() hero: Hero;
get message() {
return this.hero && this.hero.name ?
`${this.hero.name} is strange and mysterious.` :
'Are you feeling indecisive?';
}
}
export const heroSwitchComponents =
[ HappyHeroComponent, SadHeroComponent, ConfusedHeroComponent, UnknownHeroComponent ];
@@ -1,36 +1,33 @@
export class Hero {
static nextId = 1;
static MockHeroes = [
static heroes: Hero[] = [
new Hero(
325,
'Hercules',
'Son of Zeus',
'happy',
new Date(1970, 1, 25),
'http://www.imdb.com/title/tt0065832/',
325),
new Hero('eenie', 'toe'),
new Hero('Meanie', 'Toe'),
new Hero('Miny', 'Toe'),
new Hero('Moe', 'Toe')
'http://www.imdb.com/title/tt0065832/'
),
new Hero(1, 'Mr. Nice', 'happy'),
new Hero(2, 'Narco', 'sad' ),
new Hero(3, 'Windstorm', 'confused' ),
new Hero(4, 'Magneta')
];
public id: number;
static clone({firstName, lastName, birthdate, url, rate, id}: Hero) {
return new Hero(firstName, lastName, birthdate, url, rate, id);
}
constructor(
public firstName: string,
public lastName?: string,
public id?: number,
public name?: string,
public emotion?: string,
public birthdate?: Date,
public url?: string,
public rate = 100,
id?: number) {
this.id = id != null ? id : Hero.nextId++;
) {
this.id = id ? id : Hero.nextId++;
}
get fullName() { return `${this.firstName} ${this.lastName}`; }
clone(): Hero {
return Object.assign(new Hero(), this);
}
}
@@ -6,7 +6,6 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="template-syntax.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>