docs(template-syntax): add brief example of HTML sanitization in binding.

Also clarified interpolation v. property binding example for PR #1564
This commit is contained in:
Ward Bell 2016-05-31 18:36:22 -07:00
parent 5e50b2ef10
commit ae2d8f2730
4 changed files with 92 additions and 64 deletions

View File

@ -197,13 +197,18 @@ button</button>
<!-- #enddocregion property-binding-7 --> <!-- #enddocregion property-binding-7 -->
<!-- #docregion property-binding-vs-interpolation --> <!-- #docregion property-binding-vs-interpolation -->
Interpolated: <img src="{{heroImageUrl}}"><br> <p><img src="{{heroImageUrl}}"> is the <i>interpolated</i> image.</p>
Property bound: <img [src]="heroImageUrl"> <p><img [src]="heroImageUrl"> is the <i>property bound</i> image.</p>
<div>The interpolated title is {{title}}</div> <p><span>"{{title}}" is the <i>interpolated</i> title.</span></p>
<div [innerHTML]="'The [innerHTML] title is '+title"></div> <p>"<span [innerHTML]="title"></span>" is the <i>property bound</i> title.</p>
<!-- #enddocregion property-binding-vs-interpolation --> <!-- #enddocregion property-binding-vs-interpolation -->
<!-- #docregion property-binding-vs-interpolation-sanitization -->
<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 -->
<a class="to-toc" href="#toc">top</a> <a class="to-toc" href="#toc">top</a>
<!-- attribute binding --> <!-- attribute binding -->

View File

@ -1,4 +1,5 @@
//#docplaster /* tslint:disable:member-ordering forin */
// #docplaster
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core';
import { NgForm } from '@angular/common'; import { NgForm } from '@angular/common';
@ -8,7 +9,7 @@ import { HeroDetailComponent, BigHeroDetailComponent } from './hero-detail.compo
import { MyClickDirective, MyClickDirective2 } from './my-click.directive'; import { MyClickDirective, MyClickDirective2 } from './my-click.directive';
// Alerter fn: monkey patch during test // Alerter fn: monkey patch during test
export function alerter(msg?:string) { export function alerter(msg?: string) {
window.alert(msg); window.alert(msg);
} }
@ -27,7 +28,7 @@ export enum Color {Red, Green, Blue};
}) })
export class AppComponent implements AfterViewInit, OnInit { export class AppComponent implements AfterViewInit, OnInit {
ngOnInit(){ ngOnInit() {
this.refreshHeroes(); this.refreshHeroes();
} }
@ -40,43 +41,48 @@ export class AppComponent implements AfterViewInit, OnInit {
badCurly = 'bad curly'; badCurly = 'bad curly';
classes = 'special'; classes = 'special';
callFax(value:string) {this.alert(`Faxing ${value} ...`)} callFax(value: string) {this.alert(`Faxing ${value} ...`); }
callPhone(value:string) {this.alert(`Calling ${value} ...`)} callPhone(value: string) {this.alert(`Calling ${value} ...`); }
canSave = true; canSave = true;
Color = Color; Color = Color;
color = Color.Red; color = Color.Red;
colorToggle() {this.color = (this.color === Color.Red)? Color.Blue : Color.Red} colorToggle() {this.color = (this.color === Color.Red) ? Color.Blue : Color.Red; }
currentHero = Hero.MockHeroes[0]; currentHero = Hero.MockHeroes[0];
deleteHero(hero:Hero){ deleteHero(hero: Hero) {
this.alert('Deleted hero: '+ (hero && hero.firstName)) this.alert('Deleted hero: ' + (hero && hero.firstName));
} }
// DevMode memoization fields // #docregion evil-title
private priorClasses:{}; evilTitle = 'Template <script>alert("evil never sleeps")</script>Syntax';
private _priorStyles:{}; // #enddocregion evil-title
private _priorStyles2:{};
getStyles(el:Element){ title = 'Template Syntax';
// DevMode memoization fields
private priorClasses: {};
private _priorStyles: {};
getStyles(el: Element) {
let styles = window.getComputedStyle(el); let styles = window.getComputedStyle(el);
let showStyles = {}; let showStyles = {};
for (var p in this.setStyles()){ for (let p in this.setStyles()) {
showStyles[p] = styles[p]; showStyles[p] = styles[p];
} }
return JSON.stringify(showStyles); return JSON.stringify(showStyles);
} }
getVal() {return this.val}; getVal() { return this.val; }
heroes:Hero[]; heroes: Hero[];
// heroImageUrl = 'http://www.wpclipart.com/cartoon/people/hero/hero_silhoutte_T.png'; // heroImageUrl = 'http://www.wpclipart.com/cartoon/people/hero/hero_silhoutte_T.png';
// Public Domain terms of use: http://www.wpclipart.com/terms.html // Public Domain terms of use: http://www.wpclipart.com/terms.html
heroImageUrl = 'images/hero.png'; heroImageUrl = 'images/hero.png';
//iconUrl = 'https://angular.io/resources/images/logos/standard/shield-large.png'; // iconUrl = 'https://angular.io/resources/images/logos/standard/shield-large.png';
clicked = ''; clicked = '';
clickMessage = ''; clickMessage = '';
clickMessage2 = ''; clickMessage2 = '';
@ -85,28 +91,28 @@ export class AppComponent implements AfterViewInit, OnInit {
isSpecial = true; isSpecial = true;
isUnchanged = true; isUnchanged = true;
nullHero:Hero = null; // or undefined nullHero: Hero = null; // or undefined
onCancel(event:KeyboardEvent){ onCancel(event: KeyboardEvent) {
let evtMsg = event ? ' Event target is '+ (<HTMLElement>event.target).innerHTML : ''; let evtMsg = event ? ' Event target is ' + (<HTMLElement>event.target).innerHTML : '';
this.alert('Canceled.'+evtMsg) this.alert('Canceled.' + evtMsg);
} }
onClickMe(event:KeyboardEvent){ onClickMe(event: KeyboardEvent) {
let evtMsg = event ? ' Event target class is '+ (<HTMLElement>event.target).className : ''; let evtMsg = event ? ' Event target class is ' + (<HTMLElement>event.target).className : '';
this.alert('Click me.'+evtMsg) this.alert('Click me.' + evtMsg);
} }
onSave(event:KeyboardEvent){ onSave(event: KeyboardEvent) {
let evtMsg = event ? ' Event target is '+ (<HTMLElement>event.target).innerText : ''; let evtMsg = event ? ' Event target is ' + (<HTMLElement>event.target).innerText : '';
this.alert('Saved.'+evtMsg) this.alert('Saved.' + evtMsg);
} }
onSubmit(form:NgForm){ onSubmit(form: NgForm) {
let evtMsg = form.valid ? let evtMsg = form.valid ?
' Form value is '+ JSON.stringify(form.value) : ' Form value is ' + JSON.stringify(form.value) :
' Form is invalid'; ' Form is invalid';
this.alert('Form submitted.'+evtMsg) this.alert('Form submitted.' + evtMsg);
} }
product = { product = {
@ -123,10 +129,10 @@ export class AppComponent implements AfterViewInit, OnInit {
// #docregion same-as-it-ever-was // #docregion same-as-it-ever-was
private samenessCount = 5; private samenessCount = 5;
moreOfTheSame() {this.samenessCount++;}; moreOfTheSame() { this.samenessCount++; };
get sameAsItEverWas() { get sameAsItEverWas() {
var result:string[] = Array(this.samenessCount); let result: string[] = Array(this.samenessCount);
for (var i=result.length; i-- > 0;){result[i]='same as it ever was ...'} for ( let i = result.length; i-- > 0; ) { result[i] = 'same as it ever was ...'; }
return result; return result;
// return [1,2,3,4,5].map(id => { // return [1,2,3,4,5].map(id => {
// return {id:id, text: 'same as it ever was ...'}; // return {id:id, text: 'same as it ever was ...'};
@ -134,8 +140,8 @@ export class AppComponent implements AfterViewInit, OnInit {
} }
// #enddocregion same-as-it-ever-was // #enddocregion same-as-it-ever-was
setUpperCaseFirstName(firstName:string){ setUpperCaseFirstName(firstName: string) {
//console.log(firstName); // console.log(firstName);
this.currentHero.firstName = firstName.toUpperCase(); this.currentHero.firstName = firstName.toUpperCase();
} }
@ -145,10 +151,10 @@ export class AppComponent implements AfterViewInit, OnInit {
saveable: this.canSave, // true saveable: this.canSave, // true
modified: !this.isUnchanged, // false modified: !this.isUnchanged, // false
special: this.isSpecial, // true special: this.isSpecial, // true
} };
// #enddocregion setClasses // #enddocregion setClasses
// compensate for DevMode (sigh) // compensate for DevMode (sigh)
if (JSON.stringify(classes) === JSON.stringify(this.priorClasses)){ if (JSON.stringify(classes) === JSON.stringify(this.priorClasses)) {
return this.priorClasses; return this.priorClasses;
} }
this.priorClasses = classes; this.priorClasses = classes;
@ -165,10 +171,10 @@ export class AppComponent implements AfterViewInit, OnInit {
'font-style': this.canSave ? 'italic' : 'normal', // italic 'font-style': this.canSave ? 'italic' : 'normal', // italic
'font-weight': !this.isUnchanged ? 'bold' : 'normal', // normal 'font-weight': !this.isUnchanged ? 'bold' : 'normal', // normal
'font-size': this.isSpecial ? '24px' : '8px', // 24px 'font-size': this.isSpecial ? '24px' : '8px', // 24px
} };
// #enddocregion setStyles // #enddocregion setStyles
// compensate for DevMode (sigh) // compensate for DevMode (sigh)
if (JSON.stringify(styles) === JSON.stringify(this._priorStyles)){ if (JSON.stringify(styles) === JSON.stringify(this._priorStyles)) {
return this._priorStyles; return this._priorStyles;
} }
this._priorStyles = styles; this._priorStyles = styles;
@ -178,15 +184,14 @@ export class AppComponent implements AfterViewInit, OnInit {
// #enddocregion setStyles // #enddocregion setStyles
toeChoice = ''; toeChoice = '';
toeChooser(picker:HTMLFieldSetElement){ toeChooser(picker: HTMLFieldSetElement) {
let choices = picker.children; let choices = picker.children;
for (let i=0; i<choices.length; i++){ for (let i = 0; i < choices.length; i++) {
var choice = <HTMLInputElement>choices[i]; let choice = <HTMLInputElement>choices[i];
if (choice.checked) {return this.toeChoice = choice.value} if (choice.checked) {return this.toeChoice = choice.value; }
} }
} }
title = 'Template Syntax';
// #docregion trackByHeroes // #docregion trackByHeroes
trackByHeroes(index: number, hero: Hero) { return hero.id; } trackByHeroes(index: number, hero: Hero) { return hero.id; }
@ -196,18 +201,18 @@ export class AppComponent implements AfterViewInit, OnInit {
trackById(index: number, item: any): string { return item['id']; } trackById(index: number, item: any): string { return item['id']; }
// #enddocregion trackById // #enddocregion trackById
val=2; val = 2;
// villainImageUrl = 'http://www.clker.com/cliparts/u/s/y/L/x/9/villain-man-hi.png' // 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 // Public Domain terms of use http://www.clker.com/disclaimer.html
villainImageUrl = 'images/villain.png' villainImageUrl = 'images/villain.png';
//////// Detect effects of NgForTrackBy /////////////// //////// Detect effects of NgForTrackBy ///////////////
@ViewChildren('noTrackBy') childrenNoTrackBy:QueryList<ElementRef>; @ViewChildren('noTrackBy') childrenNoTrackBy: QueryList<ElementRef>;
@ViewChildren('withTrackBy') childrenWithTrackBy:QueryList<ElementRef>; @ViewChildren('withTrackBy') childrenWithTrackBy: QueryList<ElementRef>;
private _oldNoTrackBy:HTMLElement[]; private _oldNoTrackBy: HTMLElement[];
private _oldWithTrackBy:HTMLElement[]; private _oldWithTrackBy: HTMLElement[];
heroesNoTrackByChangeCount = 0; heroesNoTrackByChangeCount = 0;
heroesWithTrackByChangeCount = 0; heroesWithTrackByChangeCount = 0;
@ -216,32 +221,32 @@ export class AppComponent implements AfterViewInit, OnInit {
this._oldNoTrackBy = toArray(this.childrenNoTrackBy); this._oldNoTrackBy = toArray(this.childrenNoTrackBy);
this._oldWithTrackBy = toArray(this.childrenWithTrackBy); this._oldWithTrackBy = toArray(this.childrenWithTrackBy);
this.childrenNoTrackBy.changes.subscribe((changes:any) => { this.childrenNoTrackBy.changes.subscribe((changes: any) => {
let newNoTrackBy = toArray(changes); let newNoTrackBy = toArray(changes);
let isSame = this._oldNoTrackBy.every((v:any, i:number) => v === newNoTrackBy[i]); let isSame = this._oldNoTrackBy.every((v: any, i: number) => v === newNoTrackBy[i]);
if (!isSame) { if (!isSame) {
this._oldNoTrackBy = newNoTrackBy; this._oldNoTrackBy = newNoTrackBy;
this.heroesNoTrackByChangeCount++; this.heroesNoTrackByChangeCount++;
} }
}) });
this.childrenWithTrackBy.changes.subscribe((changes:any) => { this.childrenWithTrackBy.changes.subscribe((changes: any) => {
let newWithTrackBy = toArray(changes); let newWithTrackBy = toArray(changes);
let isSame = this._oldWithTrackBy.every((v:any, i:number) => v === newWithTrackBy[i]); let isSame = this._oldWithTrackBy.every((v: any, i: number) => v === newWithTrackBy[i]);
if (!isSame) { if (!isSame) {
this._oldWithTrackBy = newWithTrackBy; this._oldWithTrackBy = newWithTrackBy;
this.heroesWithTrackByChangeCount++; this.heroesWithTrackByChangeCount++;
} }
}) });
} }
/////////////////// ///////////////////
} }
// helper to convert viewChildren to an array of HTMLElements // helper to convert viewChildren to an array of HTMLElements
function toArray(viewChildren:QueryList<ElementRef>) { function toArray(viewChildren: QueryList<ElementRef>) {
let result: HTMLElement[] = []; let result: HTMLElement[] = [];
let children = viewChildren.toArray()[0].nativeElement.children; let children = viewChildren.toArray()[0].nativeElement.children;
for (var i = 0; i < children.length; i++) { result.push(children[i]); } for (let i = 0; i < children.length; i++) { result.push(children[i]); }
return result; return result;
} }

View File

@ -568,7 +568,8 @@ a(id="one-time-initialization")
The `[hero]` binding, on the other hand, remains a live binding to the component's `currentHero` property. The `[hero]` binding, on the other hand, remains a live binding to the component's `currentHero` property.
### Property binding or interpolation? ### Property binding or interpolation?
We often have a choice between interpolation and property binding. The following binding pairs do the same thing: We often have a choice between interpolation and property binding.
The following binding pairs do the same thing:
+makeExample('template-syntax/ts/app/app.component.html', 'property-binding-vs-interpolation')(format=".") +makeExample('template-syntax/ts/app/app.component.html', 'property-binding-vs-interpolation')(format=".")
:marked :marked
Interpolation is a convenient alternative for property binding in many cases. Interpolation is a convenient alternative for property binding in many cases.
@ -580,6 +581,23 @@ a(id="one-time-initialization")
We suggest establishing coding style rules and choosing the form that We suggest establishing coding style rules and choosing the form that
both conforms to the rules and feels most natural for the task at hand. both conforms to the rules and feels most natural for the task at hand.
:marked
#### Content Security
Imagine the following *malicious content*.
+makeExample('template-syntax/ts/app/app.component.ts', 'evil-title')(format=".")
:marked
Fortunately, Angular data binding is on alert for dangerous HTML.
It *sanitizes* the values before displaying them.
It **will not** allow HTML with script tags to leak into the browser, neither with interpolation
nor property binding.
+makeExample('template-syntax/ts/app/app.component.html', 'property-binding-vs-interpolation-sanitization')(format=".")
:marked
Interpolation handles the script tags differently than property binding but both approaches render the
content harmlessly.
figure.image-display
img(src='/resources/images/devguide/template-syntax/evil-title.png' alt="evil title made safe" width='500px')
.l-main-section .l-main-section
:marked :marked
<a id="other-bindings"></a> <a id="other-bindings"></a>

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB