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,3 +1,4 @@
/* tslint:disable:member-ordering forin */
// #docplaster // #docplaster
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core'; import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core';
@ -40,35 +41,40 @@ 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));
} }
// #docregion evil-title
evilTitle = 'Template <script>alert("evil never sleeps")</script>Syntax';
// #enddocregion evil-title
title = 'Template Syntax';
// DevMode memoization fields // DevMode memoization fields
private priorClasses: {}; private priorClasses: {};
private _priorStyles: {}; private _priorStyles: {};
private _priorStyles2:{};
getStyles(el: Element) { 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[];
@ -89,24 +95,24 @@ export class AppComponent implements AfterViewInit, OnInit {
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 = {
@ -125,8 +131,8 @@ export class AppComponent implements AfterViewInit, OnInit {
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 ...'};
@ -145,7 +151,7 @@ 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)) {
@ -165,7 +171,7 @@ 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)) {
@ -181,12 +187,11 @@ export class AppComponent implements AfterViewInit, OnInit {
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; }
@ -199,7 +204,7 @@ export class AppComponent implements AfterViewInit, OnInit {
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 ///////////////
@ -223,7 +228,7 @@ export class AppComponent implements AfterViewInit, OnInit {
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);
@ -232,7 +237,7 @@ export class AppComponent implements AfterViewInit, OnInit {
this._oldWithTrackBy = newWithTrackBy; this._oldWithTrackBy = newWithTrackBy;
this.heroesWithTrackByChangeCount++; this.heroesWithTrackByChangeCount++;
} }
}) });
} }
/////////////////// ///////////////////
@ -242,6 +247,6 @@ export class AppComponent implements AfterViewInit, OnInit {
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