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,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; | ||||||
| } | } | ||||||
|  | |||||||
| @ -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> | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								public/resources/images/devguide/template-syntax/evil-title.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/resources/images/devguide/template-syntax/evil-title.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 32 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user