diff --git a/public/docs/_examples/template-syntax/e2e-spec.ts b/public/docs/_examples/template-syntax/e2e-spec.ts index 124f633280..b8621c4d2d 100644 --- a/public/docs/_examples/template-syntax/e2e-spec.ts +++ b/public/docs/_examples/template-syntax/e2e-spec.ts @@ -1,4 +1,4 @@ -'use strict'; // necessary for es6 output in node +'use strict'; // necessary for es6 output in node import { browser, element, by } from 'protractor'; @@ -30,4 +30,15 @@ describe('Template Syntax', function () { let specialButtonEle = element(by.cssContainingText('div.special~button', 'button')); expect(specialButtonEle.getAttribute('style')).toMatch('color: red'); }); + + it('should two-way bind to sizer', function () { + let buttons = element.all(by.css('div#two-way-1 my-sizer button')); + let input = element(by.css('input#fontsize')); + + input.getAttribute('value').then(size => { + buttons.get(1).click(); + browser.waitForAngular(); + expect(input.getAttribute('value')).toEqual((+size + 1).toString()); + }); + }); }); diff --git a/public/docs/_examples/template-syntax/ts/app/app.component.html b/public/docs/_examples/template-syntax/ts/app/app.component.html index 50c07a1fc8..92964ff137 100644 --- a/public/docs/_examples/template-syntax/ts/app/app.component.html +++ b/public/docs/_examples/template-syntax/ts/app/app.component.html @@ -14,7 +14,7 @@
Event Binding
- +Two-way Binding

Directives
@@ -349,9 +349,26 @@ button


- top +

Two-way Binding

+
+ + +
Resizable Text
+ + +
+
+
+

De-sugared two-way binding

+ + + +
+

+ +top

NgModel (two-way) Binding

diff --git a/public/docs/_examples/template-syntax/ts/app/app.component.ts b/public/docs/_examples/template-syntax/ts/app/app.component.ts index fb128c88d8..072c634162 100644 --- a/public/docs/_examples/template-syntax/ts/app/app.component.ts +++ b/public/docs/_examples/template-syntax/ts/app/app.component.ts @@ -1,4 +1,4 @@ -/* tslint:disable forin */ +/* tslint:disable:forin member-ordering */ // #docplaster import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core'; @@ -50,6 +50,8 @@ export class AppComponent implements AfterViewInit, OnInit { this.alert('Deleted hero: ' + (hero && hero.firstName)); } + fontSize = 10; + // #docregion evil-title evilTitle = 'Template Syntax'; // #enddocregion evil-title diff --git a/public/docs/_examples/template-syntax/ts/app/app.module.ts b/public/docs/_examples/template-syntax/ts/app/app.module.ts index 2a99d8b395..db940fad08 100644 --- a/public/docs/_examples/template-syntax/ts/app/app.module.ts +++ b/public/docs/_examples/template-syntax/ts/app/app.module.ts @@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { BigHeroDetailComponent, HeroDetailComponent } from './hero-detail.component'; import { MyClickDirective, MyClickDirective2 } from './my-click.directive'; +import { SizerComponent } from './sizer.component'; @NgModule({ imports: [ @@ -16,7 +17,8 @@ import { MyClickDirective, MyClickDirective2 } from './my-click.directive'; BigHeroDetailComponent, HeroDetailComponent, MyClickDirective, - MyClickDirective2 + MyClickDirective2, + SizerComponent ], bootstrap: [ AppComponent ] }) diff --git a/public/docs/_examples/template-syntax/ts/app/sizer.component.ts b/public/docs/_examples/template-syntax/ts/app/sizer.component.ts new file mode 100644 index 0000000000..cf0ef4af47 --- /dev/null +++ b/public/docs/_examples/template-syntax/ts/app/sizer.component.ts @@ -0,0 +1,25 @@ +// #docregion +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'my-sizer', + template: ` +
+ + + +
` +}) +export class SizerComponent { + @Input() size: number; + @Output() sizeChange = new EventEmitter(); + + dec() { this.resize(-1); } + inc() { this.resize(+1); } + + resize(delta: number) { + const size = +this.size + delta; + this.size = Math.min(40, Math.max(8, size)); + this.sizeChange.emit(this.size); + } +} diff --git a/public/docs/ts/latest/guide/change-log.jade b/public/docs/ts/latest/guide/change-log.jade index 637f604ea4..314875496c 100644 --- a/public/docs/ts/latest/guide/change-log.jade +++ b/public/docs/ts/latest/guide/change-log.jade @@ -7,6 +7,16 @@ block includes The Angular documentation is a living document with continuous improvements. This log calls attention to recent significant changes. + ## "Template Syntax" explains two-way data binding syntax (2016-10-20) + Demonstrates how to two-way data bind to a custom Angular component and + re-explains `[(ngModel)]` in terms of the basic `[()]` syntax. + + ## "Router" _preload_ syntax and _:enter_/_:leave_ animations (2016-10-19) + The router can lazily _preload_ modules _after_ the app starts and + _before_ the user navigates to them for improved perceived performance. + + New `:enter` and `:leave` aliases make animation more natural. + ## Sync with Angular v.2.1.0 (2016-10-12) Docs and code samples updated and tested with Angular v.2.1.0 diff --git a/public/docs/ts/latest/guide/template-syntax.jade b/public/docs/ts/latest/guide/template-syntax.jade index f226d451c0..9956bcf654 100644 --- a/public/docs/ts/latest/guide/template-syntax.jade +++ b/public/docs/ts/latest/guide/template-syntax.jade @@ -21,6 +21,7 @@ block includes * [Property binding](#property-binding) * [Attribute, class, and style bindings](#other-bindings) * [Event binding](#event-binding) + * [Two-way data binding](#two-way) * [Two-way data binding with `NgModel`](#ngModel) * [Built-in directives](#directives) * [NgClass](#ngClass) @@ -841,19 +842,64 @@ block style-property-name-dart-diff and the outer `
`, causing a double save. +makeExample('template-syntax/ts/app/app.component.html', 'event-binding-propagation')(format=".") - +#two-way +.l-main-section +:marked + ## Two-way binding + We often want to both display a data property and update that property when the user makes changes. + + On the element side that takes a combination of setting a specific element property + and listening for an element change event. + + Angular offers a special _two-way data binding_ syntax for this purpose, **`[(x)]`**. + The `[(x)]` syntax combines the brackets + of _Property Binding_, `[x]`, with the parentheses of _Event Binding_, `(x)`. +.callout.is-important + header [( )] = banana in a box + :marked + Visualize a *banana in a box* to remember that the parentheses go _inside_ the brackets. +:marked + The `[(x)]` syntax is easy to demonstrate when the element has a settable property called `x` + and a corresponding event named `xChange`. + Here's a `SizerComponent` that fits the pattern. + It has a `size` value property and a companion `sizeChange` event: ++makeExample('template-syntax/ts/app/sizer.component.ts', null, 'app/sizer.component.ts') +:marked + The initial `size` is an input value from a property binding. + Clicking the buttons increases or decreases the `size`, within min/max values constraints, + and then raises (_emits_) the `sizeChange` event with the adjusted size. + + Here's an example in which the `AppComponent.fontSize` is two-way bound to the `SizerComponent`: ++makeExample('template-syntax/ts/app/app.component.html', 'two-way-1')(format=".") +:marked + The `AppComponent.fontSize` establishes the initial `SizerComponent.size` value. + Clicking the buttons updates the `AppComponent.fontSize` via the two-way binding. + The revised `AppComponent.fontSize` value flows through to the _style_ binding, making the displayed text bigger or smaller. + Try it in the live example. + + The two-way binding syntax is really just syntactic sugar for a _property_ binding and an _event_ binding. + Angular _desugars_ the `SizerComponent` binding into this: ++makeExample('template-syntax/ts/app/app.component.html', 'two-way-2')(format=".") +:marked + The `$event` variable contains the payload of the `SizerComponent.sizeChange` event. + Angular assigns the `$event` value to the `AppComponent.fontSize` when the user clicks the buttons. + + Clearly the two-way binding syntax is a great convenience compared to separate property and event bindings. + + We'd like to use two-way binding with HTML form elements like `` and `` element's `value` property and `input` event. +makeExample('template-syntax/ts/app/app.component.html', 'without-NgModel')(format=".") :marked - That’s cumbersome. Who can remember which element property to set and what event reports user changes? + That’s cumbersome. Who can remember which element property to set and which element event emits user changes? How do we extract the currently displayed text from the input box so we can update the data property? Who wants to look that up each time? @@ -882,35 +928,29 @@ block style-property-name-dart-diff +makeExample('template-syntax/ts/app/app.component.html', 'NgModel-3')(format=".") .l-sub-section :marked - The `ngModel` input property sets the element's value property and the `ngModelChange` output property + The `ngModel` data property sets the element's value property and the `ngModelChange` event property listens for changes to the element's value. - The details are specific to each kind of element and therefore the `NgModel` directive only works for elements, + + The details are specific to each kind of element and therefore the `NgModel` directive only works for specific form elements, such as the input text box, that are supported by a [ControlValueAccessor](../api/forms/index/ControlValueAccessor-interface.html). - We can't apply `[(ngModel)]` to our custom components until we write a suitable *value accessor*, + + We can't apply `[(ngModel)]` to a custom component until we write a suitable *value accessor*, a technique that is beyond the scope of this chapter. + That's something we might want to do for an Angular component or a WebComponent whose API we can't control. + + It's completely unnecessary for an Angular component that we _do_ control ... because we can name the value and event properties + to suit Angular's basic [two-way binding syntax](#two-way) and skip `NgModel` altogether. :marked - Separate `ngModel` bindings is an improvement. We can do better. + Separate `ngModel` bindings is an improvement over binding to the element's native properties. We can do better. We shouldn't have to mention the data property twice. Angular should be able to capture the component’s data property and set it - with a single declaration — which it can with the `[( )]` syntax: + with a single declaration — which it can with the `[(ngModel)]` syntax: +makeExample('template-syntax/ts/app/app.component.html', 'NgModel-1')(format=".") - -.l-sub-section - :marked - `[(ngModel)]` is a specific example of a more general pattern in which Angular "de-sugars" the `[(x)]` syntax - into an `x` input property for property binding and an `xChange` output property for event binding. - Angular constructs the event property binding's template statement by appending `=$event` - to the literal string of the template expression. - - > [(_x_)]="_e_" <==> [_x_]="_e_" (xChange)="_e_=$event" - - We can write a two-way binding directive of our own to exploit this behavior. - :marked Is `[(ngModel)]` all we need? Is there ever a reason to fall back to its expanded form? - The `[( )]` syntax can only _set_ a data-bound property. + The `[(ngModel)]` syntax can only _set_ a data-bound property. If we need to do something more or something different, we need to write the expanded form ourselves. Let's try something silly like forcing the input value to uppercase: @@ -1201,7 +1241,7 @@ block remember-the-brackets :marked ### Expanding `*ngSwitch` - A similar transformation applies to `*ngSwitch`. We can de-sugar the syntax ourselves. + A similar transformation applies to `*ngSwitch`. We can unfold the syntax ourselves. Here's an example, first with `*ngSwitchCase` and `*ngSwitchDefault` and then again with `