docs(template-syntax): explain basic two-way binding (#2639)

closes issue #2598
This commit is contained in:
Ward Bell 2016-10-20 10:56:17 -07:00 committed by GitHub
parent 101265cba4
commit 870ce124d2
7 changed files with 143 additions and 36 deletions

View File

@ -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());
});
});
});

View File

@ -14,7 +14,7 @@
</div>
<br>
<a href="#event-binding">Event Binding</a><br>
<a href="#two-way">Two-way Binding</a><br>
<br>
<div>Directives</div>
<div style="margin-left:8px">
@ -349,9 +349,26 @@ button</button>
</div>
<!-- #enddocregion event-binding-propagation -->
<br><br>
<a class="to-toc" href="#toc">top</a>
<hr><h2 id="two-way">Two-way Binding</h2>
<div id="two-way-1">
<!-- #docregion two-way-1 -->
<my-sizer [(size)]="fontSize"></my-sizer>
<div [style.font-size.px]="fontSize">Resizable Text</div>
<!-- #enddocregion two-way-1 -->
<label>FontSize: <input id="fontsize" [(ngModel)]="fontSize"></label>
</div>
<br>
<div id="two-way-2">
<h3>De-sugared two-way binding</h3>
<!-- #docregion two-way-2 -->
<my-sizer [size]="fontSize" (sizeChange)="fontSize=$event"></my-sizer>
<!-- #enddocregion two-way-2 -->
</div>
<br><br>
<a class="to-toc" href="#toc">top</a>
<!-- Two way data binding unwound;
passing the changed display value to the event handler via `$event` -->
<hr><h2 id="ngModel">NgModel (two-way) Binding</h2>

View File

@ -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 <script>alert("evil never sleeps")</script>Syntax';
// #enddocregion evil-title

View File

@ -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 ]
})

View File

@ -0,0 +1,25 @@
// #docregion
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'my-sizer',
template: `
<div>
<button (click)="dec()" title="smaller">-</button>
<button (click)="inc()" title="bigger">+</button>
<label [style.font-size.px]="size">FontSize: {{size}}px</label>
</div>`
})
export class SizerComponent {
@Input() size: number;
@Output() sizeChange = new EventEmitter<number>();
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);
}
}

View File

@ -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

View File

@ -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 `<div>`, 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>live example</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 `<input>` and `<select>`.
Sadly, no native HTML element follows the `x` value and `xChange` event pattern.
Fortunately, the Angular [_NgModel_](#ngModel) directive is a bridge that enables two-way binding to form elements.
a#ngModel
.l-main-section
:marked
<a id="ngModel"></a>
## Two-way binding with NgModel
When developing data entry forms, we often want to both display a data property and update that property when the user makes changes.
The `[(ngModel)]` two-way data binding syntax makes that easy. Here's an example:
Two-way data binding with the `NgModel` directive makes that easy. Here's an example:
+makeExample('template-syntax/ts/app/app.component.html', 'NgModel-1')(format=".")
.callout.is-important
header [()] = banana in a box
:marked
To remember that the parentheses go inside the brackets, visualize a *banana in a box*.
+ifDocsFor('ts|js')
.callout.is-important
@ -863,18 +909,18 @@ block style-property-name-dart-diff
we must import the `FormsModule` and add it to the Angular module's `imports` list.
Learn more about the `FormsModule` and `ngModel` in the
[Forms](../guide/forms.html#ngModel) chapter.
:marked
Here's how to import the `FormsModule` to make `[(ngModel)]` available.
+makeExample('template-syntax/ts/app/app.module.1.ts', '', 'app.module.ts (FormsModule import)')
:marked
Theres a story behind this construction, a story that builds on the property and event binding techniques we learned previously.
### Inside `[(ngModel)]`
We could have achieved the same result with separate bindings to
Looking back at the `firstName` binding, it's important to note that
we could have achieved the same result with separate bindings to
the `<input>` element's `value` property and `input` event.
+makeExample('template-syntax/ts/app/app.component.html', 'without-NgModel')(format=".")
:marked
Thats cumbersome. Who can remember which element property to set and what event reports user changes?
Thats 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 components data property and set it
with a single declaration &mdash; which it can with the `[( )]` syntax:
with a single declaration &mdash; 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.
> <span style="font-family:courier">[(_x_)]="_e_" &lt;==> [_x_]="_e_" (<i>x</i>Change)="_e_=$event"</span>
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 `<template>` tags:
+makeExample('template-syntax/ts/app/app.component.html', 'NgSwitch-expanded')(format=".")
:marked