Skip to content

Commit 162b01b

Browse files
committed
docs(template-syntax): explain basic two-way binding
closes issue angular#2598
1 parent 101265c commit 162b01b

File tree

7 files changed

+136
-35
lines changed

7 files changed

+136
-35
lines changed

public/docs/_examples/template-syntax/e2e-spec.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
'use strict'; // necessary for es6 output in node
1+
'use strict'; // necessary for es6 output in node
22

33
import { browser, element, by } from 'protractor';
44

@@ -30,4 +30,15 @@ describe('Template Syntax', function () {
3030
let specialButtonEle = element(by.cssContainingText('div.special~button', 'button'));
3131
expect(specialButtonEle.getAttribute('style')).toMatch('color: red');
3232
});
33+
34+
it('should two-way bind to sizer', function () {
35+
let buttons = element.all(by.css('div#two-way-1 my-sizer button'));
36+
let input = element(by.css('input#fontsize'));
37+
38+
input.getAttribute('value').then(size => {
39+
buttons.get(1).click();
40+
browser.waitForAngular();
41+
expect(input.getAttribute('value')).toEqual((+size + 1).toString());
42+
});
43+
});
3344
});

public/docs/_examples/template-syntax/ts/app/app.component.html

+19-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ <h1>Template Syntax</h1>
1414
</div>
1515
<br>
1616
<a href="#event-binding">Event Binding</a><br>
17-
17+
<a href="#two-way">Two-way Binding</a><br>
1818
<br>
1919
<div>Directives</div>
2020
<div style="margin-left:8px">
@@ -349,9 +349,26 @@ <h3>
349349
</div>
350350
<!-- #enddocregion event-binding-propagation -->
351351
<br><br>
352-
353352
<a class="to-toc" href="#toc">top</a>
354353

354+
<hr><h2 id="two-way">Two-way Binding</h2>
355+
<div id="two-way-1">
356+
<!-- #docregion two-way-1 -->
357+
<my-sizer [(size)]="fontSize"></my-sizer>
358+
<div [style.font-size.px]="fontSize">Resizable Text</div>
359+
<!-- #enddocregion two-way-1 -->
360+
<label>FontSize: <input id="fontsize" [(ngModel)]="fontSize"></label>
361+
</div>
362+
<br>
363+
<div id="two-way-2">
364+
<h3>De-sugared two-way binding</h3>
365+
<!-- #docregion two-way-2 -->
366+
<my-sizer [size]="fontSize" (sizeChange)="fontSize=$event"></my-sizer>
367+
<!-- #enddocregion two-way-2 -->
368+
</div>
369+
<br><br>
370+
371+
<a class="to-toc" href="#toc">top</a>
355372
<!-- Two way data binding unwound;
356373
passing the changed display value to the event handler via `$event` -->
357374
<hr><h2 id="ngModel">NgModel (two-way) Binding</h2>

public/docs/_examples/template-syntax/ts/app/app.component.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* tslint:disable forin */
1+
/* tslint:disable:forin member-ordering */
22
// #docplaster
33

44
import { AfterViewInit, Component, ElementRef, OnInit, QueryList, ViewChildren } from '@angular/core';
@@ -50,6 +50,8 @@ export class AppComponent implements AfterViewInit, OnInit {
5050
this.alert('Deleted hero: ' + (hero && hero.firstName));
5151
}
5252

53+
fontSize = 10;
54+
5355
// #docregion evil-title
5456
evilTitle = 'Template <script>alert("evil never sleeps")</script>Syntax';
5557
// #enddocregion evil-title

public/docs/_examples/template-syntax/ts/app/app.module.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { FormsModule } from '@angular/forms';
55
import { AppComponent } from './app.component';
66
import { BigHeroDetailComponent, HeroDetailComponent } from './hero-detail.component';
77
import { MyClickDirective, MyClickDirective2 } from './my-click.directive';
8+
import { SizerComponent } from './sizer.component';
89

910
@NgModule({
1011
imports: [
@@ -16,7 +17,8 @@ import { MyClickDirective, MyClickDirective2 } from './my-click.directive';
1617
BigHeroDetailComponent,
1718
HeroDetailComponent,
1819
MyClickDirective,
19-
MyClickDirective2
20+
MyClickDirective2,
21+
SizerComponent
2022
],
2123
bootstrap: [ AppComponent ]
2224
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// #docregion
2+
import { Component, EventEmitter, Input, Output } from '@angular/core';
3+
4+
@Component({
5+
selector: 'my-sizer',
6+
template: `
7+
<div>
8+
<button (click)="dec()" title="smaller">-</button>
9+
<button (click)="inc()" title="bigger">+</button>
10+
<label [style.font-size.px]="size">FontSize: {{size}}px</label>
11+
</div>`
12+
})
13+
export class SizerComponent {
14+
@Input() size: number;
15+
@Output() sizeChange = new EventEmitter<number>();
16+
17+
dec() { this.resize(-1); }
18+
inc() { this.resize(+1); }
19+
20+
resize(delta: number) {
21+
const size = +this.size + delta;
22+
this.size = Math.min(40, Math.max(8, size));
23+
this.sizeChange.emit(this.size);
24+
}
25+
}

public/docs/ts/latest/guide/change-log.jade

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ block includes
77
The Angular documentation is a living document with continuous improvements.
88
This log calls attention to recent significant changes.
99

10+
## "Template Syntax" explains two-way data binding syntax (2016-10-19)
11+
Demonstrates how to two-way data bind to a custom Angular component and
12+
re-explains `[(ngModel)]` in terms of the basic `[()]` syntax.
13+
1014
## Sync with Angular v.2.1.0 (2016-10-12)
1115
Docs and code samples updated and tested with Angular v.2.1.0
1216

public/docs/ts/latest/guide/template-syntax.jade

+70-30
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ block includes
2121
* [Property binding](#property-binding)
2222
* [Attribute, class, and style bindings](#other-bindings)
2323
* [Event binding](#event-binding)
24+
* [Two-way data binding](#two-way)
2425
* [Two-way data binding with `NgModel`](#ngModel)
2526
* [Built-in directives](#directives)
2627
* [NgClass](#ngClass)
@@ -841,19 +842,64 @@ block style-property-name-dart-diff
841842
and the outer `<div>`, causing a double save.
842843
+makeExample('template-syntax/ts/app/app.component.html', 'event-binding-propagation')(format=".")
843844
845+
#two-way
846+
.l-main-section
847+
:marked
848+
## Two-way binding
849+
We often want to both display a data property and update that property when the user makes changes.
850+
851+
On the element side that takes a combination of setting a specific element property
852+
and listening for an element change event.
853+
854+
Angular offers a special _two-way data binding_ syntax for this purpose, **`[(x)]`**.
855+
The `[(x)]` syntax combines the brackets
856+
of _Property Binding_, `[x]`, with the parentheses of _Event Binding_, `(x)`.
857+
.callout.is-important
858+
header [( )] = banana in a box
859+
:marked
860+
Visualize a *banana in a box* to remember that the parentheses go _inside_ the brackets.
861+
:marked
862+
The `[(x)]` syntax is easy to demonstrate when the element has a settable property called `x`
863+
and a corresponding event named `xChange`.
864+
Here's a `SizerComponent` that fits the pattern.
865+
It has a `size` value property and a companion `sizeChange` event:
866+
+makeExample('template-syntax/ts/app/sizer.component.ts', null, 'app/sizer.component.ts')
867+
:marked
868+
The initial `size` is an input value from a property binding.
869+
Clicking the buttons increases or decreases the `size`, within min/max values constraints,
870+
and then raises (_emits_) the `sizeChange` event with the adjusted size.
871+
872+
Here's an example in which the `AppComponent.fontSize` is two-way bound to the `SizerComponent`:
873+
+makeExample('template-syntax/ts/app/app.component.html', 'two-way-1')(format=".")
874+
:marked
875+
The `AppComponent.fontSize` establishes the initial `SizerComponent.size` value.
876+
Clicking the buttons updates the `AppComponent.fontSize` via the two-way binding.
877+
The revised `AppComponent.fontSize` value flows through to the _style_ binding, making the displayed text bigger or smaller.
878+
Try it in the <live-example>live example</live-example>.
879+
880+
The two-way binding syntax is really just syntactic sugar for a _property_ binding and an _event_ binding.
881+
Angular _desugars_ the `SizerComponent` binding into this:
882+
+makeExample('template-syntax/ts/app/app.component.html', 'two-way-2')(format=".")
883+
:marked
884+
The `$event` variable contains the payload of the `SizerComponent.sizeChange` event.
885+
Angular assigns the `$event` value to the `AppComponent.fontSize` when the user clicks the buttons.
886+
887+
Clearly the two-way binding syntax is a great convenience compared to separate property and event bindings.
888+
889+
We'd like to use two-way binding with HTML form elements like `<input>` and `<select>`.
890+
Sadly, no native HTML element follows the `x` value and `xChange` event pattern.
844891

892+
Fortunately, the Angular [_NgModel_](#ngModel) directive is a bridge that enables two-way binding to form elements.
893+
894+
a#ngModel
845895
.l-main-section
846896
:marked
847-
<a id="ngModel"></a>
848897
## Two-way binding with NgModel
849898
When developing data entry forms, we often want to both display a data property and update that property when the user makes changes.
850899

851-
The `[(ngModel)]` two-way data binding syntax makes that easy. Here's an example:
900+
Two-way data binding with the `NgModel` directive makes that easy. Here's an example:
852901
+makeExample('template-syntax/ts/app/app.component.html', 'NgModel-1')(format=".")
853-
.callout.is-important
854-
header [()] = banana in a box
855-
:marked
856-
To remember that the parentheses go inside the brackets, visualize a *banana in a box*.
902+
857903

858904
+ifDocsFor('ts|js')
859905
.callout.is-important
@@ -863,54 +909,48 @@ block style-property-name-dart-diff
863909
we must import the `FormsModule` and add it to the Angular module's `imports` list.
864910
Learn more about the `FormsModule` and `ngModel` in the
865911
[Forms](../guide/forms.html#ngModel) chapter.
866-
912+
:marked
913+
Here's how to import the `FormsModule` to make `[(ngModel)]` available.
867914
+makeExample('template-syntax/ts/app/app.module.1.ts', '', 'app.module.ts (FormsModule import)')
868915

869916
:marked
870-
There’s a story behind this construction, a story that builds on the property and event binding techniques we learned previously.
871-
872917
### Inside `[(ngModel)]`
873-
We could have achieved the same result with separate bindings to
918+
Looking back at the `firstName` binding, it's important to note that
919+
we could have achieved the same result with separate bindings to
874920
the `<input>` element's `value` property and `input` event.
875921
+makeExample('template-syntax/ts/app/app.component.html', 'without-NgModel')(format=".")
876922
:marked
877-
That’s cumbersome. Who can remember which element property to set and what event reports user changes?
923+
That’s cumbersome. Who can remember which element property to set and which element event emits user changes?
878924
How do we extract the currently displayed text from the input box so we can update the data property?
879925
Who wants to look that up each time?
880926

881927
That `ngModel` directive hides these onerous details behind its own `ngModel` input and `ngModelChange` output properties.
882928
+makeExample('template-syntax/ts/app/app.component.html', 'NgModel-3')(format=".")
883929
.l-sub-section
884930
:marked
885-
The `ngModel` input property sets the element's value property and the `ngModelChange` output property
931+
The `ngModel` data property sets the element's value property and the `ngModelChange` event property
886932
listens for changes to the element's value.
887-
The details are specific to each kind of element and therefore the `NgModel` directive only works for elements,
933+
934+
The details are specific to each kind of element and therefore the `NgModel` directive only works for specific form elements,
888935
such as the input text box, that are supported by a [ControlValueAccessor](../api/forms/index/ControlValueAccessor-interface.html).
889-
We can't apply `[(ngModel)]` to our custom components until we write a suitable *value accessor*,
936+
937+
We can't apply `[(ngModel)]` to a custom component until we write a suitable *value accessor*,
890938
a technique that is beyond the scope of this chapter.
939+
That's something we might want to do for an Angular component or a WebComponent whose API we can't control.
940+
941+
It's completely unnecessary for an Angular component that we _do_ control ... because we can name the value and event properties
942+
to suit Angular's basic [two-way binding syntax](#two-way) and skip `NgModel` altogether.
891943

892944
:marked
893-
Separate `ngModel` bindings is an improvement. We can do better.
945+
Separate `ngModel` bindings is an improvement over binding to the element's native properties. We can do better.
894946

895947
We shouldn't have to mention the data property twice. Angular should be able to capture the component’s data property and set it
896-
with a single declaration &mdash; which it can with the `[( )]` syntax:
948+
with a single declaration &mdash; which it can with the `[(ngModel)]` syntax:
897949
+makeExample('template-syntax/ts/app/app.component.html', 'NgModel-1')(format=".")
898-
899-
.l-sub-section
900-
:marked
901-
`[(ngModel)]` is a specific example of a more general pattern in which Angular "de-sugars" the `[(x)]` syntax
902-
into an `x` input property for property binding and an `xChange` output property for event binding.
903-
Angular constructs the event property binding's template statement by appending `=$event`
904-
to the literal string of the template expression.
905-
906-
> <span style="font-family:courier">[(_x_)]="_e_" &lt;==> [_x_]="_e_" (<i>x</i>Change)="_e_=$event"</span>
907-
908-
We can write a two-way binding directive of our own to exploit this behavior.
909-
910950
:marked
911951
Is `[(ngModel)]` all we need? Is there ever a reason to fall back to its expanded form?
912952

913-
The `[( )]` syntax can only _set_ a data-bound property.
953+
The `[(ngModel)]` syntax can only _set_ a data-bound property.
914954
If we need to do something more or something different, we need to write the expanded form ourselves.
915955

916956
Let's try something silly like forcing the input value to uppercase:
@@ -1201,7 +1241,7 @@ block remember-the-brackets
12011241

12021242
:marked
12031243
### Expanding `*ngSwitch`
1204-
A similar transformation applies to `*ngSwitch`. We can de-sugar the syntax ourselves.
1244+
A similar transformation applies to `*ngSwitch`. We can unfold the syntax ourselves.
12051245
Here's an example, first with `*ngSwitchCase` and `*ngSwitchDefault` and then again with `<template>` tags:
12061246
+makeExample('template-syntax/ts/app/app.component.html', 'NgSwitch-expanded')(format=".")
12071247
:marked

0 commit comments

Comments
 (0)