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:
parent
5e50b2ef10
commit
ae2d8f2730
|
@ -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 -->
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
Loading…
Reference in New Issue